Klaybank (KR)
Search
K

플래시론을 활용한 아비트라지(2)

Overview

다음은 실제코드와 함께 플래시론을 이용하여 아비트라지를 하는 예제를 설명합니다.
본 예제에서는 클레이스왑과 클레임스왑에 존재하는 KUSDT↔KETH 페어간 아비트라지 기회를 포착하고 실행하는 방법 및 코드를 설명합니다.
아비트라지를 하기 위해서는,
  1. 1.
    플래시론을 활용하여 스왑하는 컨트랙트
  2. 2.
    기회를 포착하여 트랜잭션을 전송하는 봇
두가지가 필요합니다.
컨트랙트는 세개의 컨트랙트로 구성되어있으며 각각,
  • 기회를 포착한 봇이 트랜잭션을 날려 시작점이 되는 컨트랙트 (A)
  • 플래시론을 이용해 클레이스왑 → 클레임스왑 순서의 차액거래를 하는 컨트랙트 (B)
  • 플래시론을 이용해 클레임스왑 → 클레이스왑 순서의 차액거래를 하는 컨트랙트 (C)
로 구성됩니다. 아래의 설명에서는 각 컨트랙트의 주소를 (A),(B),(C)로 표기하여 설명합니다.

Contract

Arbitrager Contract(A)는 아비트라지 기회를 포착후 Tx를 받는 허브역할을 하게됩니다.
contract Arbitrager {
ILendingPool LENDING_POOL;
IKlaySwapProtocol KLAYSWAP;
IClaimSwapRouter CLAIMSWAP;
...
function swapKlaySwapToClaimSwap(address tokenA, address tokenB, uint256 amount) public {
...
}
function swapClaimSwapToKlaySwap(address tokenA, address tokenB, uint256 amount) public {
...
}
...
}
클레이스왑 → 클레임스왑 순서의 아비트라지를 수행하는 함수(swapKlaySwapToClaimSwap) 와 클레임스왑 → 클레이스왑 순서의 아비트라지를 수행하는 함수(swapClaimSwapToKlaySwap) 가 존재합니다. 각각 상황에 맞추어 호출하면 (B), (C) 컨트랙트를 호출하는 플래시론 동작을 수행합니다.
function swapClaimSwapToKlaySwap(address tokenA, address tokenB, uint256 amount) public {
address[] memory assets = new address[](1);
assets[0] = tokenA;
uint256[] memory amounts = new uint256[](1);
amounts[0] = amount;
uint256[] memory modes = new uint256[](1);
modes[0] = 0;
bytes memory params = addressToBytes(tokenB);
LENDING_POOL.flashLoan(
ArbitragerClaimToKlaySwap,
assets,
amounts,
modes,
address(this),
params,
0
);
}
두가지의 함수중 swapClaimSwapToKlaySwap 를 기준으로 설명합니다.
swapClaimSwapToKlaySwap
  1. 1.
    KUSDT → KETH ( 클레임 스왑)
  2. 2.
    KETH → KUSDT ( 클레이 스왑)
과정의 아비트라지를 수행해주는 함수입니다. 따라서 플래시론을 통해 빌릴 토큰 파라미터인 assets 에 tokenA 주소를, amounts에 파라미터로 받은 amount를 넣고 플래시론을 실행해줍니다.
플래시론 인터페이스 및 파라미터 에 대한 설명은 여기 를 참조해주세요.
LENDING_POOL.flashLoan(...) 을 통해 플래시론을 실행해주고, 플래시론을 통해 로직을 수행할 컨트랙트인
ArbitragerClaimToKlaySwap 에 (C) 주소를 입력합니다.
contract ArbitragerClaimToKlaySwap is IFlashLoanReceiver {
function executeOperation(
address[] calldata assets,
uint256[] calldata amounts,
uint256[] calldata premiums,
address initiator,
bytes calldata params
) external returns (bool){
uint estimateOut;
uint estimateOut2;
address[] memory path = new address[](2);
// tokenA
path[0] = assets[0];
// tokenB
path[1] = bytesToAddress(params);
estimateOut = CLAIMSWAP.getAmountsOut(amounts[0], path)[1];
IKlaySwapExchange klaySwapPool = IKlaySwapExchange(KLAYSWAP.tokenToPool(path[0], path[1]));
estimateOut2 = klaySwapPool.estimatePos(path[1], estimateOut);
require(estimateOut2 > amounts[0]);
_swapClaimSwap(path[0], path[1], amounts[0]);
_swapKlaySwap(path[1], path[0], estimateOut);
for (uint i = 0; i < assets.length; i++) {
checkApprove(assets[i], address(LENDING_POOL));
}
IKIP7(assets[i]).transfer(msg.sender,estimateOut2-amounts[0]-premiums[0]);
return true;
}
function _swapKlaySwap(address tokenA, address tokenB, uint256 amount) private {
checkApprove(tokenA, address(KLAYSWAP));
KLAYSWAP.exchangeKctPos(tokenA, amount, tokenB, 10, new address[](0));
}
function _swapClaimSwap(address tokenA, address tokenB, uint256 amount) private {
checkApprove(tokenA, address(CLAIMSWAP));
address[] memory path = new address[](2);
path[0] = tokenA;
path[1] = tokenB;
CLAIMSWAP.swapExactTokensForTokens(amount, 10, path, address(this), 4230424911);
}
function checkApprove(address token, address spender) public {
if (token == address(0x0000000000000000000000000000000000000000)) {
return;
}
if (!approvedTokens[spender][token]) {
IKIP7(token).approve(spender, uint(2 ** 256 - 1));
approvedTokens[spender][token] = true;
}
}
...
}
위 코드는 ArbitragerClaimToKlaySwap 컨트랙트의 일부분입니다. 플래시론을 통해 대출이 실행되었을때 수행할 코드를 작성하기위해 IFlashLoanReceiver 를 상속 받습니다. ArbitragerClaimToKlaySwap 는 클레임스왑 → 클레이스왑의 아비트라지를 수행하는 컨트랙트입니다. 따라서 function executeOperation(...)에서 실질적인 아비트라지를 진행합니다. 빌린 자산인 asset[0] 가 tokenA, 파라미터로 넘어온 데이터인 params가 tokenB가 되게됩니다. 함수의 동작은 다음과같은 순서로 진행됩니다.
  1. 1.
    클레임 스왑의 getAmountsOut 과 클레이스왑의 estimate를 이용하여 거래를했을때 수익이 나는지 확인합니다.(require(estimateOut2 > amounts[0]);)
  2. 2.
    _swapClaimSwap 를 통해 클레임스왑에서 A→B 토큰으로 교환을 하고
  3. 3.
    _swapKlaySwap 를 통해 클레이스왑에서 B→A 토큰으로 교환을 합니다.
  4. 4.
    마지막으로 클레이뱅크에 빌린 자산을 돌려주기위해 approve 를 확인해주는 checkApprove 함수를 실행합니다.
이로써 A→B→A 토큰 교환을 마치고 대출금액 및 플래시론 수수료를 상환한 뒤 남은 차액을 얻게됩니다.
swapKlaySwapToClaimSwap 를 사용하는 ArbitragerKlaySwapToClaim 컨트랙트역시 로직은 동일하며 순서만 다릅니다.

Arbitrage Bot

Arbitrage Bot의 전체 소스코드는 http://github.com/klaybank/arbitrager 에 업로드 되어있습니다.
const Caver = require('caver-js')
const BigNumber = require("bignumber.js");
const IKIP7ABI = require('./build/contracts/IKIP7.json');
const IKlayswapProtocolABI = require('./build/contracts/IKlaySwapProtocol.json');
const IKlayswapExchangeABI = require('./build/contracts/IKlaySwapExchange.json');
const IClaimSwapRouterABI = require('./build/contracts/IClaimSwapRouter.json');
const ArbitragerABI = require('./build/contracts/Arbitrager.json')
먼저 봇 실행에 필요한 기본적인 Caver, BigNumber 및 컨트랙트의 ABI들을 추가해줍니다.
const KETHAddress = "0x34d21b1e550d73cee41151c77f3c73359527a396"
const KUSDTAddress = "0xcee8faf64bb97a73bb51e115aa89c17ffa8dd167"
const KlayswapFactoryAddress = "0xc6a2ad8cc6e4a7e08fc37cc5954be07d499e7654"
const ClaimSwapRouterAddress = "0xEf71750C100f7918d6Ded239Ff1CF09E81dEA92D"
const Klayswap_KUSDT_KETH_ExchangeAddress = "0x029e2a1b2bb91b66bd25027e1c211e5628dbcb93"
const ArbitragerAddress = "Deployed Contract Address"
const tryKUSDTAmount = new BigNumber("10000" + "000000") // 10000 KUSDT
위에는 각 컨트랙트들의 주소를 나타내며, ArbitragerAddress의 경우 직접 배포한 컨트랙트의 주소가 위치하게 됩니다.
tryKUSDTAmount 는 한번 아비트라지를 시도할때 사용할 금액을 나타내며, 이 예시에서는 10,000 KUSDT를 기준으로 설명합니다.
const caver = new Caver(new Caver.providers.WebsocketProvider("wss://public-node-api.klaytnapi.com/v1/cypress/ws"));
const KUSDTContract = new caver.klay.Contract(IKIP7ABI, KUSDTAddress)
const KETHContract = new caver.klay.Contract(IKIP7ABI, KETHAddress)
const klayswapFactoryContract = new caver.klay.Contract(IKlayswapProtocolABI, KlayswapFactoryAddress)
const klayswap_USDT_ETH_ExchangeContract = new caver.klay.Contract(IKlayswapExchangeABI, Klayswap_KUSDT_KETH_ExchangeAddress)
const claimSwapRouterContract = new caver.klay.Contract(IClaimSwapRouterABI, ClaimSwapRouterAddress)
const arbitragerContract = new caver.klay.Contract(ArbitragerABI, ArbitragerAddress)
매 블록마다 기회를 포착하기위해 소켓을 이용하며, 각 컨트랙트들을 ABI와 주소를 이용해 초기화해줍니다.
const keyring = caver.wallet.keyring.createFromPrivateKey('Input your private key here')
caver.wallet.add(keyring)
caver.klay.accounts.wallet.add(
caver.klay.accounts.createWithAccountKey(keyring.address, keyring.key.privateKey))
또한 아비트라지 트랜잭션을 보낼 EOA의 private key를 이용하여 키링을 설정해줍니다.
const subscription = caver.rpc.klay.subscribe(
'newBlockHeaders',
async (error, result) => {
if (error) {
console.error(error);
return;
}
let estimateUsdtToETHKlayswap = await klayswap_USDT_ETH_ExchangeContract.methods.estimatePos(KUSDTAddress, tryKUSDTAmount).call();
let estimateETHToUsdtClaimSwap = (await claimSwapRouterContract.methods.getAmountsOut(estimateUsdtToETHKlayswap, [KETHAddress, KUSDTAddress]).call())[1]
let KlayswapToClaimRes = new BigNumber(estimateETHToUsdtClaimSwap)
console.log(`diff1 : ${KlayswapToClaimRes.toString()}`)
if (KlayswapToClaimRes.isGreaterThan(tryKUSDTAmount)) {
let txRes = await arbitragerContract.methods.swapKlaySwapToClaimSwap(KUSDTAddress, KETHAddress, tryKUSDTAmount).send(
{
from: keyring.address,
gas: 5000000
}
)
console.log(`try arbitrage klayswap -> claimSwap tx : ` + txRes.hash)
}
let estimateUsdtToETHClaimSwap = (await claimSwapRouterContract.methods.getAmountsOut(tryKUSDTAmount, [KUSDTAddress, KETHAddress]).call())[1];
let estimateETHToUsdtKlayswap = await klayswap_USDT_ETH_ExchangeContract.methods.estimatePos(KETHAddress, estimateUsdtToETHClaimSwap).call()
let ClaimSwapToKlayswapRes = new BigNumber(estimateETHToUsdtKlayswap)
console.log(`diff2 : ${ClaimSwapToKlayswapRes.toString()}`)
if (ClaimSwapToKlayswapRes.isGreaterThan(tryKUSDTAmount)) {
let txRes = await arbitragerContract.methods.swapClaimSwapToKlaySwap(KUSDTAddress, KETHAddress, tryKUSDTAmount).send(
{
from: keyring.address,
gas: 5000000
}
)
console.log(`try arbitrage claimSwap -> klayswap tx : ` + txRes.hash)
}
})
.on('connected', console.log)
.on('error', console.error);
블록마다 아비트라지기회를 포착하고 실행하는 코드부분입니다.
const subscription = caver.rpc.klay.subscribe(
'newBlockHeaders',
async (error, result) => {
if (error) {
console.error(error);
return;
}
...
...
부분에서 newBlockHeaders 를 subscribe하여 매 블록마다 이벤트를 받을 수 있도록 구성합니다. 이 subscribtion은 블록이 생성될때마다 async (error, result) => {...} 함수 콜백을 실행시켜줍니다.
따라서 콜백함수 내부에서 매 블록마다 클레이스왑 → 클레임스왑 에서 기회가 있는지, 클레임스왑 → 클레이스왑 에서 기회가 있는지 체크하게 됩니다.
let estimateUsdtToETHKlayswap = await klayswap_USDT_ETH_ExchangeContract.methods.estimatePos(KUSDTAddress, tryKUSDTAmount).call();
let estimateETHToUsdtClaimSwap = (await claimSwapRouterContract.methods.getAmountsOut(estimateUsdtToETHKlayswap, [KETHAddress, KUSDTAddress]).call())[1]
let KlayswapToClaimRes = new BigNumber(estimateETHToUsdtClaimSwap)
console.log(`diff1 : ${KlayswapToClaimRes.toString()}`)
if (KlayswapToClaimRes.isGreaterThan(tryKUSDTAmount)) {
let txRes = await arbitragerContract.methods.swapKlaySwapToClaimSwap(KUSDTAddress, KETHAddress, tryKUSDTAmount).send(
{
from: keyring.address,
gas: 5000000
}
)
console.log(`try arbitrage klayswap -> claimSwap tx : ` + txRes.hash)
}
콜백함수 내 위 코드는 클레이스왑 → 클레임스왑 에서의 KUSDT→KETH→KUSDT 경로의 아비트라지 기회가 있는지 포착합니다.
클레이 스왑에서 10,000 KUSDT를 KETH로 바꾼 예측값(estimateUsdtToETHKlayswap)를 구합니다. 이 예측값을 다시 이용하여 클레임 스왑에서 KETH를 KUSDT로 바꾼 예측값(estimateETHToUsdtClaimSwap) 를 구합니다.
이때 estimateETHToUsdtClaimSwap 의 값이 tryKUSDTAmount 보다 크다면 아비트라지 기회가 생긴 것 이므로, Contract 섹션에서 설명했던 arbitragerContract의 swapKlaySwapToClaimSwap 함수를 통해 플래시론 및 아비트라지를 수행하게 됩니다.
let estimateUsdtToETHClaimSwap = (await claimSwapRouterContract.methods.getAmountsOut(tryKUSDTAmount, [KUSDTAddress, KETHAddress]).call())[1];
let estimateETHToUsdtKlayswap = await klayswap_USDT_ETH_ExchangeContract.methods.estimatePos(KETHAddress, estimateUsdtToETHClaimSwap).call()
let ClaimSwapToKlayswapRes = new BigNumber(estimateETHToUsdtKlayswap)
console.log(`diff2 : ${ClaimSwapToKlayswapRes.toString()}`)
if (ClaimSwapToKlayswapRes.isGreaterThan(tryKUSDTAmount)) {
let txRes = await arbitragerContract.methods.swapClaimSwapToKlaySwap(KUSDTAddress, KETHAddress, tryKUSDTAmount).send(
{
from: keyring.address,
gas: 5000000
}
)
console.log(`try arbitrage claimSwap -> klayswap tx : ` + txRes.hash)
}
클레임스왑 → 클레이스왑 의 경우 역시 마찬가지로 클레임 스왑에서 10,000 KUSDT를 KETH로 바꾼 예측값(estimateUsdtToETHClaimSwap) 를 구합니다. 이 예측값을 다시 이용하여 클레이 스왑에서 KETH를 KUSDT로 바꾼 예측값(estimateETHToUsdtKlayswap) 를 구합니다. 그 후 과정은 arbitragerContract의 swapClaimSwapToKlaySwap를 이용하는 것 외에 이전 설명과 동일합니다.

마치며

아비트라지 기회는 위에서 언급한 클레이스왑↔클레임스왑 사이에만 존재하는것은 아닙니다. 또한 KUSDT↔KETH 페어 사이에서만 발생하는 것 역시 아닙니다. 다양한 Dex(클레이스왑, 클레임스왑, 팔라, UFO, …) 과 다양한 페어(KUSDT↔KETH, KLAY↔KUSDT, CLA↔KUSDT, …)를 이용하여 여러 기회를 포착할 수 있습니다.
따라서 봇 코드를 고도화 다양한 경로를 감시 및 아비트라지 할 수 있으며, 더욱 고도화 한다면 고정된 시도금액 ( 예시에서 10,000 KUSDT ) 이 아닌 더 큰 금액 또는 작은 금액으로 가변적으로 조절할 수 있습니다.
플래시론은 이러한 아비트라지 기회를 포착하고 실행할때, 무담보 대출을 실행하여 자본이 없거나 부족하더라도 아비트라지를 안정적으로 성공하도록 도와줍니다.
위 예시 코드에서는, Arbitrager contract에 쌓인 토큰을 꺼내는 함수 및 보안을 위한 사항들이 고려되어있지 않으므로 사용시에는 적절한 수정을 통해 사용하는 것을 권장합니다.