Overview
다음은 실제코드와 함께 플래시론을 이용하여 아비트라지를 하는 예제를 설명합니다.
본 예제에서는 클레이스왑과 클레임스왑에 존재하는 KUSDT↔KETH 페어간 아비트라지 기회를 포착하고 실행하는 방법 및 코드를 설명합니다.
아비트라지를 하기 위해서는,
두가지가 필요합니다.
컨트랙트는 세개의 컨트랙트로 구성되어있으며 각각,
기회를 포착한 봇이 트랜잭션을 날려 시작점이 되는 컨트랙트 (A)
플래시론을 이용해 클레이스왑 → 클레임스왑 순서의 차액거래를 하는 컨트랙트 (B)
플래시론을 이용해 클레임스왑 → 클레이스왑 순서의 차액거래를 하는 컨트랙트 (C)
로 구성됩니다. 아래의 설명에서는 각 컨트랙트의 주소를 (A),(B),(C)로 표기하여 설명합니다.
Contract
Arbitrager Contract(A)는 아비트라지 기회를 포착후 Tx를 받는 허브역할을 하게됩니다.
Copy 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) 컨트랙트를 호출하는 플래시론 동작을 수행합니다.
Copy 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
는
과정의 아비트라지를 수행해주는 함수입니다. 따라서 플래시론을 통해 빌릴 토큰 파라미터인 assets
에 tokenA 주소를, amounts
에 파라미터로 받은 amount를 넣고 플래시론을 실행해줍니다.
플래시론 인터페이스 및 파라미터 에 대한 설명은 여기 를 참조해주세요.
LENDING_POOL.flashLoan(...)
을 통해 플래시론을 실행해주고, 플래시론을 통해 로직을 수행할 컨트랙트인
ArbitragerClaimToKlaySwap
에 (C) 주소를 입력합니다.
Copy 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가 되게됩니다. 함수의 동작은 다음과같은 순서로 진행됩니다.
클레임 스왑의 getAmountsOut 과 클레이스왑의 estimate를 이용하여 거래를했을때 수익이 나는지 확인합니다.(require(estimateOut2 > amounts[0]);
)
_swapClaimSwap
를 통해 클레임스왑에서 A→B 토큰으로 교환을 하고
_swapKlaySwap
를 통해 클레이스왑에서 B→A 토큰으로 교환을 합니다.
마지막으로 클레이뱅크에 빌린 자산을 돌려주기위해 approve 를 확인해주는 checkApprove
함수를 실행합니다.
이로써 A→B→A 토큰 교환을 마치고 대출금액 및 플래시론 수수료를 상환한 뒤 남은 차액을 얻게됩니다.
swapKlaySwapToClaimSwap
를 사용하는 ArbitragerKlaySwapToClaim
컨트랙트역시 로직은 동일하며 순서만 다릅니다.
Arbitrage Bot
Arbitrage Bot의 전체 소스코드는 http://github.com/klaybank/arbitrager 에 업로드 되어있습니다.
Copy 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들을 추가해줍니다.
Copy 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를 기준으로 설명합니다.
Copy 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와 주소를 이용해 초기화해줍니다.
Copy 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를 이용하여 키링을 설정해줍니다.
Copy 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);
블록마다 아비트라지기회를 포착하고 실행하는 코드부분입니다.
Copy const subscription = caver . rpc . klay .subscribe (
'newBlockHeaders' ,
async (error , result) => {
if (error) {
console .error (error);
return ;
}
...
...
부분에서 newBlockHeaders
를 subscribe하여 매 블록마다 이벤트를 받을 수 있도록 구성합니다. 이 subscribtion은 블록이 생성될때마다 async (error, result) => {...}
함수 콜백을 실행시켜줍니다.
따라서 콜백함수 내부에서 매 블록마다 클레이스왑 → 클레임스왑 에서 기회가 있는지, 클레임스왑 → 클레이스왑 에서 기회가 있는지 체크하게 됩니다.
Copy 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
함수를 통해 플래시론 및 아비트라지를 수행하게 됩니다.
Copy 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에 쌓인 토큰을 꺼내는 함수 및 보안을 위한 사항들이 고려되어있지 않으므로 사용시에는 적절한 수정을 통해 사용하는 것을 권장합니다.