Resolver용

Aqua 유동성을 상대로 스왑을 체결하기: Resolver 역할에 필요한 것, 온체인에서 체결이 진행되는 방식, SwapVM SDK로 견적을 내고 실행하는 방법.

10분 읽기업데이트: 2026년 7월

Aqua에서 Resolver가 하는 일

1inch Network 전반에서는 Aqua 유동성을 체결하는 스왑을 1inch Resolver가 라우팅합니다. 1inch 검증 절차를 완료한 마켓 메이커와 차익거래 트레이더입니다.

Aqua 자체는 탐색도 매칭도 하지 않습니다. 스왑 호출마다 하나의 포지션을 하나의 상대방과 가격을 매겨 실행할 뿐입니다. 어떤 포지션을 얼마 규모로 체결할지는 Resolver의 몫입니다. 1inch 라우팅이 네트워크에 스왑을 전달하고, 자체 시스템이 컨트랙트를 직접 호출할 수도 있습니다.

기본 구조가 처음이라면 Aqua의 작동 방식부터 시작하세요.

시작하기 전에

  • 1inch Business 포털을 통한 Resolver 검증. 체인별 액세스 토큰이 부여됩니다
  • 스왑 트랜잭션을 보내는 운영 EOA에 있는 액세스 토큰
  • 대상 체인의 노드 접근, 그리고 체결에 쓸 입력 토큰과 가스
  • 표준 컨트랙트 주소. 지원되는 모든 체인에서 동일하며 아래에 나열되어 있습니다
  • Aqua API 탐색 경로에 쓸 Business portal 발급 API 키

컨트랙트 주소

Aqua와 SwapVM 라우터는 결정론적 배포로, 지원되는 모든 체인에서 주소가 동일합니다. 이 두 컨트랙트와만 상호작용하세요. 그 외에는 Aqua가 아닙니다.

Aqua(레지스트리)
0x1111113ccf1426a8e30e2bff5e005d929bf6a90a
SwapVM 라우터
0x111111338c5091e8440b67b168bae16a668ac0de

체결이 진행되는 방식

  1. 1

    검증 받기

    Business 포털에서 Resolver 온보딩을 완료하고, 체결할 각 체인의 액세스 토큰을 받으세요.

  2. 2

    포지션 찾기

    Aqua API에서 열린 포지션을 가져오거나, Shipped와 Docked 이벤트로 라이브 세트를 재구성하거나, 네트워크 리졸버로서 1inch 라우팅에서 스왑을 받습니다.

  3. 3

    제출 직전에 견적 내기

    디코딩한 주문으로 라우터의 quote 함수를 스태틱 콜로 호출하세요. 견적은 실시간 지갑 뒷받침을 읽기 때문에 다른 체결이 들어오면 달라집니다.

  4. 4

    한도 설정하기

    방금 받은 견적에서 최소 아웃풋을 도출하고 짧은 기한을 설정하세요. 곡선이 움직이면 더 나쁜 환율로 체결되는 대신 리버트됩니다.

  5. 5

    스왑하고 확인하기

    액세스 토큰을 보유한 EOA에서 스왑을 보내고, 성공 영수증을 요구하며, 실행된 수량을 Swapped 이벤트에서 읽으세요.

퀵스타트: 포지션 하나를 견적 내고 체결하기

아래 샘플은 Aqua API로 열린 포지션을 찾아 견적을 받고 보호된 체결을 제출합니다. Node 22와 TypeScript SDK에서 실행됩니다. 컨트랙트, 이벤트, SDK, API 모두 포지션을 strategy라고 부르므로 코드도 그렇게 부릅니다.

SDK 설치bash
pnpm add @1inch/aqua-sdk @1inch/swap-vm-sdk viem
Aqua API로 열린 포지션 찾기typescript
import assert from 'node:assert'
import { HexString, Order } from '@1inch/swap-vm-sdk'

// Every currently open position from every LP, newest first. Auth is the
// same API key the other 1inch APIs use - issued in the Business portal.
const response = await fetch('https://api.1inch.com/aqua/v1.0/strategies/opened?chainIds=1&limit=50', {
  headers: { Authorization: `Bearer ${process.env.ONEINCH_API_KEY}` },
})
assert(response.ok, `Aqua API responded ${response.status}`)
const { items, nextCursor } = await response.json()

// Pick by your own criteria. Per-token balance and allowance in each item are
// useful pre-filters, but they are indexed with a delay - only a fresh
// on-chain quote() is authoritative.
const strategy = items[0]
const order = Order.decode(new HexString(strategy.strategyBytes))
console.log('candidate:', strategy.strategyHash, 'tokens:', strategy.tokens.length, 'more:', nextCursor !== null)
견적, 체결, 검증typescript
import assert from 'node:assert'
import { ABI, Address, HexString, Order, SwappedEvent, SwapVMContract, TakerTraits } from '@1inch/swap-vm-sdk'
import { createPublicClient, createWalletClient, decodeFunctionResult, erc20Abi, http, isHex } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { mainnet } from 'viem/chains'

// The SwapVM router is a deterministic deployment - the same address on every
// supported chain (see the contract addresses above). The strategy bytes come
// from the Aqua API or a Shipped event. The operator key belongs to the EOA
// holding the resolver credential - it must be the transaction origin.
const router = new Address('0x111111338c5091e8440b67b168bae16a668ac0de')
const strategyBytes = process.env.STRATEGY_BYTES
const operatorPrivateKey = process.env.OPERATOR_PRIVATE_KEY
assert(isHex(strategyBytes) && isHex(operatorPrivateKey))

const order = Order.decode(new HexString(strategyBytes))

const USDC = new Address('0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48')
const WETH = new Address('0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2')
const amountIn = 3_000n * 10n ** 6n

const operator = privateKeyToAccount(operatorPrivateKey)
const publicClient = createPublicClient({ chain: mainnet, transport: http() })
const wallet = createWalletClient({ chain: mainnet, transport: http(), account: operator })

// 1. Preview the swap with a static quote() call, right before you submit.
const quoteTx = SwapVMContract.buildQuoteTx(router, {
  order,
  tokenIn: USDC,
  tokenOut: WETH,
  amount: amountIn,
  takerTraits: TakerTraits.new({ exactIn: true }),
})
const quoted = await publicClient.call({ to: quoteTx.to, data: quoteTx.data })
assert(quoted.data, 'quote call returned no data')
const [, quotedOut] = decodeFunctionResult({ abi: ABI.SWAP_VM_ABI, functionName: 'quote', data: quoted.data })
console.log('quoted output:', quotedOut)

// 2. Approve the router to move the input token (once per token and chain).
const approveHash = await wallet.writeContract({
  address: USDC.toString(),
  abi: erc20Abi,
  functionName: 'approve',
  args: [router.toString(), amountIn],
})
await publicClient.waitForTransactionReceipt({ hash: approveHash })

// 3. Swap with a minimum-output floor and a short deadline. Balances and
// curves move between quote and fill - never submit unprotected traits.
const swapTx = SwapVMContract.buildSwapTx(router, {
  order,
  tokenIn: USDC,
  tokenOut: WETH,
  amount: amountIn,
  takerTraits: TakerTraits.new({
    exactIn: true,
    threshold: (quotedOut * 995n) / 1_000n,
    deadline: BigInt(Math.floor(Date.now() / 1_000) + 60),
  }),
})
const fillHash = await wallet.sendTransaction(swapTx)
const receipt = await publicClient.waitForTransactionReceipt({ hash: fillHash })
assert(receipt.status === 'success', 'fill reverted')

// 4. Verify the executed amounts from the router's Swapped event.
const swappedLog = receipt.logs.find((log) => log.topics[0] === SwappedEvent.TOPIC.toString())
assert(swappedLog, 'no Swapped event in the receipt')
const swapped = SwappedEvent.fromLog(swappedLog)
console.log('filled:', swapped.amountIn, '->', swapped.amountOut)

먼저 포크에서 루프를 시험하세요

위의 모든 것은 로컬 포크에서 안전하게 예행할 수 있습니다. Foundry의 anvil은 특정 블록에서 체인을 복제하므로 실제 컨트랙트와 실제 라이브 포지션을 아무 위험 없이 다룹니다. 무키 전송을 켜면 접근 토큰을 이미 보유한 아무 EOA로 행동하며, 첫 실제 트랜잭션 전에 탐색, 견적, 체결, 검증의 전체 루프를 밟아볼 수 있습니다.

메인넷 포크에서 드라이런bash
# Real Aqua bytecode and live positions at the forked block, zero real spend
anvil --fork-url $YOUR_RPC_URL --auto-impersonate

# Point the quickstart at http://127.0.0.1:8545 and send from any EOA that
# already holds the per-chain access token - no key needed on a fork

체결이 온체인에서 실행되는 방식

체결은 SwapVM 라우터로 보내는 단일 트랜잭션입니다. 라우터는 포지션의 명령 프로그램을 실행합니다. 먼저 접근 검사, 다음 가격 곡선, 마지막으로 토큰 이동입니다. 같은 트랜잭션 안에서 입력 토큰은 여러분의 EOA에서 LP 지갑으로 가고 출력 토큰은 여러분에게 돌아오며, Aqua 레지스트리는 포지션 잔액을 갱신하고 Pushed와 Pulled를 발행합니다. 어느 시점에도 풀에 놓이는 것은 없습니다.

모든 것은 전부 아니면 무입니다. 실행 시 어떤 검사라도 실패하면, 접근 토큰 부재, threshold 미달, deadline 초과, 지갑 뒷받침 부족 어느 것이든 트랜잭션 전체가 revert되고 토큰은 전혀 움직이지 않습니다. revert된 체결의 비용은 가스뿐입니다.

라우터는 quote와 swap을 제공하며 둘 다 같은 가격 경로를 지납니다. 둘 다 view 함수가 아니므로 quote는 eth_call로 정적 호출하세요. 라이브 상태를 읽어 전달한 스왑을 정확히 가격 매기고 아무것도 바꾸지 않습니다. 그 미리보기는 약속이 아닙니다. 실행을 구속하는 것은 swap과 함께 보내는 traits뿐입니다.

TakerTraits는 실행 한계를 트랜잭션에 실어 보냅니다. 이를 강제하는 것은 체인이지 SDK가 아닙니다. 가장 자주 쓰는 것들:

exactIn
어느 쪽을 고정할지. 정확한 입력에서는 보내는 양을 정하고 받는 양을 제한합니다. 정확한 출력에서는 받는 양을 정하고 보내는 양을 제한합니다.
threshold
한계 그 자체. 정확한 입력에서는 체결이 반드시 전달해야 하는 최소 출력, 정확한 출력에서는 지출 상한입니다. threshold가 0이면 검사가 꺼지므로 항상 새 견적에서 도출하세요.
deadline
unix 초 단위 만료. 그 이후 채굴된 트랜잭션은 revert되므로 멤풀에 갇힌 체결이 낡은 곡선에 실행될 수 없습니다.
customReceiver
출력을 전송 EOA가 아닌 다른 주소, 예를 들어 트레저리 주소로 보냅니다. 접근 검사는 여전히 트랜잭션 원점에 대해 실행됩니다.
shouldUnwrap
출력이 래핑된 네이티브 토큰일 때 래핑 형태 대신 네이티브 토큰으로 받습니다.

결과를 자신의 계산으로 믿지 마세요. 성공한 영수증을 요구한 뒤 라우터의 Swapped 이벤트를 디코딩하세요. orderHash, maker, taker, tokenIn, tokenOut, amountIn, amountOut입니다. 이것이 실행된 수량이며 회계에 기록해야 할 값입니다.

처리해야 할 실패 유형

닫힌 포지션
LP는 언제든 포지션을 닫을 수 있고, 닫힌 뒤의 잔액 조회는 revert됩니다. API의 opened 목록에 없는 포지션은 닫힌 것이지만, 폴링 사이에는 온체인 revert가 진실의 근원입니다. 체결 전마다 다시 확인하세요.
지갑 뒷받침 부족
포지션은 공유 지갑 잔액으로 견적을 냅니다. 경쟁 체결이 먼저 잔액을 소진하면 견적이 정상으로 보였더라도 전송이 revert됩니다. API의 balance와 allowance 필드는 사전 선별에 유용하지만 지연되어 인덱싱됩니다. 신뢰할 수 있는 것은 새로 받은 온체인 견적뿐입니다.
가격 변동
체결이 쌓이고 시간이 지나면 견적은 낡습니다. 최소 아웃풋과 기한이 나쁜 체결을 깔끔한 리버트로 바꿔 줍니다.
액세스 토큰 없음
트랜잭션 오리진에 자격 증명이 없으면 dApp에서 만든 포지션은 가격 로직 이전에 스왑을 리버트합니다.
비표준 주소
위의 두 표준 주소만 Aqua입니다. 다른 곳으로 보낸 체결은 라이브 포지션에 닿지 않습니다. 체인별로 검증할 것은 없으며 배포는 어디서나 동일합니다.

프로덕션 운영

오퍼레이터 EOA를 단순한 지갑이 아니라 자격 증명으로 다루세요. 체인별 접근 토큰을 보유하므로 키를 전용 서명자에 격리하고, 주소에는 운용 재고와 가스만 두고, 다른 용도로 재사용하지 마세요.

가스 추정을 마지막 관문으로 삼으세요. 지갑 라이브러리는 브로드캐스트 전에 가스를 추정하며, 실패하는 추정은 무언가 전송되기 전에 일찍 드러난 revert입니다. 건너뛴 체결로 처리하고 맹목적으로 재시도하지 말고 다음 후보로 넘어가세요.

세 가지 신호를 지켜보세요. 탐색 데이터가 낡거나 규모가 뒷받침에 너무 붙으면 오르는 revert 비율. 각 체결 뒤 견적의 나이로, 수 초 이내여야 합니다. 그리고 Swapped의 실행 수량 대 견적 수량으로, 드리프트를 손실 전에 잡아냅니다.

프로덕션에서 포지션 찾기

라이브 세트를 얻는 길은 두 가지입니다. 가장 빠른 출발점은 Aqua API입니다. 이벤트로 재구성하는 방식은 트러스트리스이며 외부 의존성이 없습니다. 둘 다 동일한 견적과 체결 흐름으로 이어집니다.

Aqua API는 모든 LP의 현재 열린 포지션을 나열합니다. api.1inch.com/aqua의 GET /v1.0/strategies/opened로, 페이지당 최대 500개 커서 페이지네이션과 체인·app 필터를 지원합니다. 각 항목에는 Order.decode에 전달할 strategyBytes와 함께, 후보를 사전 선별할 수 있는 토큰별 balance와 allowance가 담겨 있습니다. 요청에는 다른 1inch API와 동일한 API 키가 필요하며 Business portal에서 발급됩니다. 인덱스는 체인보다 약간 늦으므로 체결 전에는 항상 온체인에서 새 견적을 받으세요.

프로토콜의 핵심 컨트랙트인 Aqua 레지스트리는 포지션이 열리면 Shipped를, 닫히면 Docked를 발행합니다. 라이브 집합은 Shipped에서 Docked를 뺀 것으로, 각 체인의 배포 블록부터 로그를 읽어 재구성합니다. 이벤트 필드는 topics가 아니라 로그 data에 있으므로, 컨트랙트 주소와 이벤트 시그니처로 필터링한 뒤 클라이언트에서 디코딩하고 대조하세요.

이벤트 구조는 작습니다. Shipped는 maker, app, strategyHash와 전체 전략 바이트를 담습니다. Docked는 maker, app, strategyHash를 담습니다. SDK는 둘 모두에 타입 헬퍼를 제공하며, ShippedEvent와 DockedEvent에 fromLog 디코더와 TOPIC 상수가 있어 ABI를 손으로 쓸 일이 없습니다.

이벤트로 라이브 세트 재구성typescript
import { DockedEvent, ShippedEvent } from '@1inch/aqua-sdk'
import { createPublicClient, http } from 'viem'
import { mainnet } from 'viem/chains'

const AQUA = '0x1111113ccf1426a8e30e2bff5e005d929bf6a90a'
const publicClient = createPublicClient({ chain: mainnet, transport: http() })

// Shipped and Docked carry every field in the log data, none in topics, so
// pull the registry's logs and match on the event signature client-side.
const logs = await publicClient.getLogs({
  address: AQUA,
  fromBlock: 23_000_000n, // the per-chain deployment block, or your last checkpoint
  toBlock: 'latest',
})

const live = new Map<string, ShippedEvent>()
for (const log of logs) {
  if (log.topics[0] === ShippedEvent.TOPIC.toString()) {
    const shipped = ShippedEvent.fromLog(log)
    live.set(shipped.strategyHash.toString(), shipped)
  } else if (log.topics[0] === DockedEvent.TOPIC.toString()) {
    live.delete(DockedEvent.fromLog(log).strategyHash.toString())
  }
}

// live now maps strategyHash -> Shipped payload: maker, app and the strategy
// bytes you decode with Order.decode before quoting.
console.log('open positions:', live.size)

나머지는 온체인 읽기로 얻습니다. 레지스트리의 rawBalances와 safeBalances 뷰는 포지션 뒤의 지갑 뒷받침을 알려주고, 라우터의 Swapped 이벤트는 실행된 스왑을 담습니다. 견적은 캐시하지 말고 신선하게 유지하세요.

체결 규모는 견적이 아니라 뒷받침에서 시작합니다. 레지스트리 뷰로 포지션의 뒷받침을 읽고, 규모를 그보다 충분히 안쪽으로 잡은 뒤, 제출 직전에 정확히 그 규모로 새 견적을 받으세요. API의 balance와 allowance 필드는 많은 후보를 거르는 저렴한 1차 필터이고, 하나에 대한 진실은 뷰와 견적입니다.

리졸버 FAQ

아니요. 접근 검사는 트랜잭션 원점이 접근 토큰을 보유한 EOA일 것을 요구하므로 컨트랙트 지갑과 번들러는 통과할 수 없습니다. 체결은 검증된 오퍼레이터 EOA에서 보내세요.

아니요. quote 함수는 라이브 상태의 정적 미리보기이며 아무것도 움직이지 않습니다. 실행을 구속하는 것은 swap과 함께 보내는 TakerTraits뿐이며, threshold와 deadline이 온체인에서 강제됩니다.

가스뿐입니다. 어떤 검사든 실패하면 트랜잭션 전체가 revert되어 양쪽 모두 토큰이 움직이지 않습니다.

1inch Business portal 검증은 체인별 접근 토큰을 부여합니다. 체결하는 모든 체인에서 오퍼레이터 EOA에 그 토큰이 있어야 합니다.

Aqua API 레퍼런스 포지션 탐색과 통계를 위한 엔드포인트, 파라미터, 응답 형식. Business portal에 있습니다. Aqua SDK & SwapVM SDK 체결 흐름을 위한 타입 지원 빌더, 디코더, 이벤트 파서. 1inch/sdks 저장소. Resolver 온보딩 1inch Business 포털의 검증 절차와 Resolver 문서. Aqua 백서 공유 유동성 레이어의 전체 설계.

Aqua 유동성을 체결할 준비가 되셨나요?

1inch Business 포털에서 검증을 시작하고 팀에 연락하세요.

Business 포털 열기