• Duel 오픈 Robinhood Chain에서
  • 대결 런칭하기 토큰 두 개, 풀 세 개, 트랜잭션 한 번
  • 라운드 마감까지 --:--:--
  • 4시간 단위 라운드 승자 바이백에는 가격 한도가 있어요
  • 유동성 잠금 영구히, 팀 물량 없음

Duel 위에서 개발하기

Duel의 풀은 훅이 달린 표준 Uniswap V4 풀이며, 앱이 보여 주는 모든 숫자는 공개된 뷰와 이벤트에서 나옵니다. 봇, 터미널, 대시보드, 애그리게이터는 허락 없이 그 위에서 개발할 수 있습니다.

대결 읽기

DuelLens는 한 번의 호출로 대결에 관한 모든 것을 모아 줍니다.

function getPair(uint256 pairId) external view returns (PairView memory);
function getPairs(uint256 offset, uint256 limit) external view returns (PairView[] memory);

PairView에는 대결의 ID, 크리에이터, 메타데이터 URI, 두 진영(토큰, 이름, 심볼, 공급량, 풀 가격과 틱, 토큰당 ETH, 풀 안의 실제 ETH), 세 풀의 키, 실시간 비율(gaugeBps, A 진영의 몫을 베이시스 포인트로), 지금까지의 라운드 평균(roundGaugeBps), 팟, 크리에이터 수수료, 누적 거래량과 바이백, 현재의 안티 스나이프 세금, 라운드의 번호와 시작 및 종료 시각, 라운드에 거래가 있었는지(roundTraded), 런칭 시각과 틱(launchTime, startTick), 서포터 대결인지 여부, 카테고리(category, 앱이 매긴 번호이며 없으면 0), 수수료 조건(fees), 그리고 프로토콜이 지금 자기 몫 중 가져가는 부분(protocolRateBps, 전부는 10,000)이 담깁니다. 프로토콜은 스왑 수수료의 fees.protocolShareBps × protocolRateBps / 10,000을 받고, 그 몫의 나머지는 팟으로 갑니다. 컨트랙트와 ABI에서 B 진영은 D입니다: d, tokenD, keyD.

훅에는 더 작은 뷰도 있습니다. pairCount(), getPair(pairId), poolKeys(pairId), gaugeBps(pairId), snipeTaxBps(pairId), protocolRateBps(), protocolFees(), firstRoundEnd(launchTime)입니다.

거래하기

어떤 Uniswap V4 라우터로도 대결의 풀을 거래할 수 있습니다. poolKeys(pairId)로 풀 키를 만드세요.

풀currency0currency1feetickSpacing
ETH / AETH (address(0))토큰 A0200
ETH / BETH (address(0))토큰 B0200
A / B주소가 작은 토큰주소가 큰 토큰060

훅은 return delta로 수수료를 받으므로, 견적은 V4 Quoter로 받으세요. 그 응답에는 모든 수수료와 세금이 포함됩니다. ETH를 거치는 전환은 토큰 → ETH → 다른 토큰 경로로 quoteExactInput을 호출해 견적합니다.

훅이 실행 전에 수수료를 받는 스왑(정확한 입력량의 매수나 전환, 정확한 출력량의 매도)은 전부 체결되어야 합니다. 가격 한도나 ETH가 부족한 ETH 풀 때문에 중간에 멈출 스왑은 PartialFill로 되돌려집니다. PoolManager는 훅의 오류를 감쌉니다. PartialFill과 ArbitrageStarved는 WrappedError(address target, bytes4 selector, bytes reason, bytes details) 안에 담겨 오고, V4 Quoter가 보고할 때는 UnexpectedRevertBytes(bytes revertData) 안에 담겨 옵니다. 훅의 오류를 읽으려면 reason을 디코딩하세요.

모든 거래는 훅의 재정렬로 끝나며, 재정렬에는 별도의 가스가 필요합니다. 재정렬에 가스를 너무 적게 남기는 스왑은 재정렬을 건너뛰지 않고 ArbitrageStarved로 되돌려집니다. 스왑은 추정된 가스보다 절대 적지 않게, 여유를 두고 보내세요. 라운드 마감 후의 첫 스왑은 그 라운드도 정산하는데(바이백, 이어서 재정렬로 가스가 약 170,000 더 듭니다), 마감 전에 받은 추정에는 이것이 들어 있지 않습니다. 앱은 추정마다 가스 500,000을 더합니다.

DuelRouter는 한 풀에서의 스왑을 위한 호출과,

function swapExactIn(
    PoolKey calldata key,
    bool zeroForOne,
    uint256 amountIn,
    uint256 minAmountOut,
    address recipient,
    uint256 deadline
) external payable returns (uint256 amountOut);

ETH를 거치는 전환, 즉 한 진영을 그 ETH 풀에 팔고 받은 ETH 전부를 다른 진영의 풀에 쓰는 호출을 제공합니다.

function swapThroughEth(
    PoolKey calldata keyIn,
    PoolKey calldata keyOut,
    uint256 amountIn,
    uint256 minAmountOut,
    address recipient,
    uint256 deadline
) external returns (uint256 amountOut);

Duel ETH 풀에 대한 매도가 ETH를 다 써서 풀의 런칭 가격에서 멈추면, 훅의 재정렬이 두 진영 사이의 풀이 받아들이는 만큼을 풀에서 되산 뒤 같은 호출 안에서 나머지에 대해 다시 전송됩니다. 풀의 가격이 스왑 한도 아래에 있는 동안 최대 SALE_PASSES()(8)번의 스왑입니다. 각 스왑은 자기 ETH에 대해 훅 수수료를 내고, minAmountOut은 그 전부의 합계에 적용됩니다. Uniswap의 V4 Quoter는 스왑 한 번을 견적하며, 그것이 체결하는 양을 넘으면 NotEnoughLiquidity로 응답합니다. 앱은 라우터를 트레이더로 실행해 이런 매도를 견적합니다(상태 오버라이드를 쓴 eth_call, DuelSimulator).

재정렬이 더는 되사지 못하는 만큼은 sell이 이어받아, 두 진영 사이의 풀을 거쳐 다른 진영의 ETH 풀에 팝니다. 그 값을 치를 ETH는 그동안의 재정렬이 그 풀로 옮겨 두었습니다.

function sell(
    PoolKey calldata keyIn,
    PoolKey calldata keyCross,
    PoolKey calldata keyOther,
    uint256 amountIn,
    uint256 minAmountOut,
    address recipient,
    uint256 deadline
) external returns (uint256 amountOut);

sell은 swapExactIn처럼 keyIn에 판 뒤, 남은 것을 keyCross로 넘기고(keyCross는 그 전부를 받아 주며, 여느 전환처럼 전환 수수료는 소각됩니다), 이어서 keyOther에도 여러 번에 나눠 팝니다. minAmountOut은 두 풀의 ETH 합계에 적용됩니다. keyOther도 되사지 못하는 만큼은 그 풀의 토큰으로 recipient에게 지급되며, 최소 수령량에는 들어가지 않습니다. 앱은 V4 Quoter가 매도 견적을 거부하고, DuelSimulator.simulateSale이 sell로는 전부가 ETH로 팔린다고 보여 줄 때 sell로 팝니다.

다른 V4 라우터는 스왑을 한 번만 합니다. 훅은 풀의 ETH가 지불할 수 있는 양을 넘는 정확한 입력량의 매도를 거부하지 않습니다(수수료는 스왑 뒤에, 지급된 ETH에서 받습니다). 매도는 풀의 런칭 가격까지 일부만 체결되고, 나머지는 매도자에게 남습니다. 최소 수령량은 체결될 수 있는 만큼에 맞춰 정하고, 나머지는 이후 트랜잭션에서 재정렬이 되산 만큼에 대해 파세요.

매수하려면 swapExactIn에 ETH를 함께 보내세요. 매도하거나 전환하려면 해당 토큰에 대해 라우터를 승인하거나, 같은 트랜잭션 안에서 permit(EIP-2612)으로 승인하세요. swapExactInWithPermit, swapThroughEthWithPermit, sellWithPermit은 같은 인자에 더해, 라우터를 위해 서명한 호출자의 정확히 amountIn만큼의 permit을 받습니다.

struct Permit {
    uint256 deadline;
    uint8 v;
    bytes32 r;
    bytes32 s;
}

사용자의 지갑은 각 Duel 토큰의 주소를 사용하는, 이름이 Duel, 버전이 1인 EIP-712 도메인으로 permit에 서명합니다(ERC-5267의 eip712Domain()이 이를 알려 줍니다). 라우터는 permit을 먼저 사용하고 그 실패는 무시합니다. 누군가 이미 보낸 permit은 승인을 설정해 두었고, 실패한 permit은 아무것도 바꾸지 않습니다. 이때 스왑은 이미 있는 승인이 허용하는 만큼만 가져가며, 승인이 모자라면 되돌려집니다. 일부만 체결된 스왑은 사용하지 않은 승인 한도를 남깁니다. permit의 기한은 서명을 제출할 수 있는 시점을 제한할 뿐, 이미 부여된 승인을 만료시키지 않습니다. 라우터는 호출자에게서만 토큰을 가져갑니다. 전환의 ETH는 두 번째 풀이 쓰도록 PoolManager에 남습니다. Duel 풀은 이를 전부 쓰고, 그러지 못하면 훅이 스왑을 되돌립니다(PartialFill). 그 밖의 풀에서는 쓰지 못한 ETH가 호출자에게 돌아갑니다.

런칭하기

DuelFactory.createPair(CreateParams)가 대결을 런칭합니다. 런칭 수수료와 런칭 매수 금액을 함께 보내면 되고, 남는 금액은 환불됩니다. CreateParams의 항목은 nameA, symbolA, nameD, symbolD(B 진영), metadataURI, buyA, buyD(런칭 때 각 진영에 쓰는 ETH로, 안티 스나이프 세금이 없습니다), supporter, category(앱의 카테고리 번호, 없으면 0), 그리고 config입니다. config는 읽은 시점의 팩토리 configHash()이며, 그 뒤 조건이 바뀌었다면 런칭은 ConfigChanged로 되돌려집니다. 이름은 1128바이트, 티커는 140바이트, 메타데이터 URI는 최대 4,096바이트입니다(InvalidMetadata). 진영 공급량의 5%를 넘는 런칭 매수는 되돌려집니다(CreatorBuyTooLarge).

팩토리는 런칭이 일시 중지된 상태로 시작합니다. paused()가 true인 동안에는 owner()만 대결을 런칭할 수 있으며, 다른 호출자는 CreationPaused로 되돌려집니다. 소유자는 setPaused(false)로 런칭을 엽니다. 기존 대결은 계속 거래됩니다.

이벤트

컨트랙트이벤트시점
DuelFactoryPairCreated(pairId, creator, tokenA, tokenD)대결이 런칭됨
DuelHookPairRegistered(pairId, tokenA, tokenD, creator, category)같은 트랜잭션에서 그 풀들이 훅에 등록됨. category는 대결의 주제를 나타내는 앱의 카테고리 번호(없으면 0)
DuelHookFeeTaken(pairId, fee, snipeTax)ETH와의 거래가 수수료를 냄
DuelHookSwitchFeeBurned(pairId, token, amount)전환이 수수료를 소각함
DuelHookArbitrageCaptured(pairId, profit)재정렬이 팟을 채움
DuelHookRoundSettled(pairId, roundIndex, averageTick, winner, ethSpent, tokensBurned)라운드가 마감됨
DuelHookCreatorFeesClaimed(pairId, to, amount)크리에이터에게 지급됨
DuelFactoryCreatorTransferStarted(pairId, creator, newCreator)크리에이터가 역할을 제안함(0 주소로 보내면 제안 철회)
DuelHookCreatorChanged(pairId, from, to)대결의 크리에이터 역할이 넘어감
DuelHookCreatorFeesGivenUp(pairId, creator)크리에이터가 앞으로의 수수료에서 자기 몫을 팟으로 영구히 돌림
DuelHookProtocolFeesClaimed(to, amount)재무 주소에 지급됨
DuelHookProtocolRateSet(rateBps)모든 대결의 스왑 수수료에서 프로토콜 몫이 즉시 바뀜. 프로토콜은 그중 rateBps를 가져가고(전부는 10,000), 나머지는 팟으로 감
DuelFactoryConfigProposed(config, eta), ConfigApplied(config), ConfigCancelled()런칭 조건이 48시간 뒤를 기한으로 제안됨, 적용됨, 철회됨
DuelFactoryPausedSet(paused)런칭이 중단되거나 재개됨
PoolManagerSwap(id, sender, amount0, amount1, sqrtPriceX96, liquidity, tick, fee)훅 자신의 재정렬을 포함한 모든 거래(그때의 sender는 훅)

거래 목록은 세 풀 모두의 스왑을 읽지만, 가격과 비율 차트는 두 ETH 풀만 읽습니다. 보유자 목록은 토큰 Transfer 이벤트를 사용합니다. 서브그래프가 스왑, 잔액, 보유자, 정산된 라운드를 인덱싱하므로 런칭 이후의 체인을 다시 읽지 않고 기록을 불러올 수 있습니다.

정산과 청구

  • settle(pairId)는 끝난 라운드를 정산하며 누구나 호출할 수 있습니다.
  • claimCreatorFees(pairId)는 대결의 크리에이터(런칭한 지갑 또는 역할을 넘겨받은 주소)에게만, claimProtocolFees()는 청구 시 설정된 재무 주소로 지급합니다. 누구나 청구를 실행할 수 있습니다. 재무 주소는 팩토리의 48시간 지연 후 바뀔 수 있지만 호출자가 다른 수령인을 고를 수는 없습니다.
  • 팩토리의 transferCreator(pairId, newCreator)는 대결의 크리에이터 역할을 제안하고, newCreator가 호출하는 acceptCreator(pairId)가 이를 받습니다. pendingCreator(pairId)로 제안을 읽을 수 있습니다. 제안은 크리에이터만 할 수 있고, 역할은 제안받은 주소만 받을 수 있습니다.
  • 훅의 giveUpCreatorFees(pairId)는 앞으로 발생할 수수료의 크리에이터 몫을 팟으로 영구히 보냅니다. 현재 크리에이터만 호출할 수 있습니다. 이미 쌓인 수수료는 크리에이터가 계속 청구할 수 있고, 크리에이터 역할도 계속 넘길 수 있습니다.