yueying007

Posted on Apr 20, 2022Read on Mirror.xyz

区块链开发课第五讲 智能合约开发(2)

这节课,我们继续完善SimpleArbi.sol,在合约内部完成Curve和Uniswap的swap操作。

github:

https://github.com/yueying007/blockchainclass.git

Dex

Curve和Uniswap是以太坊上排名第一和第二的去中心化交易所(DEX),它们分别都进行过几个版本的迭代升级,从最初的AMM(automatic market maker)发展到后来的 CLMM(concentrated liquidity market maker)模式,具体的swap算法不是本文的讨论重点,我们今天只通过接口来认识这两个协议。

Curve

首先来看Curve的一个池子:

https://etherscan.io/address/0xD51a44d3FaE010294C616388b506AcdA1bfAAE46

它提供了USDT/WETH/WBTC三种token的兑换,每个token都有一个唯一的编号(0/1/2),编号和token的对应关系可以从合约的coins()方法获得。来看看它的接口:

interface ICurveCrypto {
    function exchange(uint256 from, uint256 to, uint256 from_amount, uint256 min_to_amount) external payable;
    function get_dy(uint256 from, uint256 to, uint256 from_amount) external view returns(uint256);
}

通过exchange()方法,实现两个token的兑换,参数分别表示:

from: 输入token的编号

to: 输出token的编号

from_amount: 输入token的数量

min_to_amount: 输出token的最小数量

通过get_dy()方法,获得给定from_amount下可以获得的输出token数量。

注意get_dy()的view关键字,表明这是一个只读函数,将来我们会它来计算token之间的兑换比例。

Uniswap

在来看一个UniswapV3的池子:

https://etherscan.io/address/0x4e68Ccd3E89f51C3074ca5072bbAC773960dFa36

它提供了WETH/USDT的兑换:

interface IUniswapV3Pair {
    function swap(
        address recipient,
        bool zeroForOne,
        int256 amountSpecified,
        uint160 sqrtPriceLimitX96,
        bytes calldata data
    ) external returns (int256 amount0, int256 amount1);
    function fee() external view returns(uint24);
}

通过swap()函数,实现两个token之间的兑换,这是一种闪电兑(flash swap)的模式,比如用WETH兑换USDT,它会先把USDT转给我的合约,然后在调用我的回调函数uniswapV3SwapCallback(),在回调函数中我把WETH还给它。参数分别表示:

recipient: 接收地址(我的合约)

zeroForOne: 标志位(用来决定兑换的方向)

amountSpecified: 输入token 的数量

sqrtPriceLimitX96: 限价范围

data: 回调信息

封装Curve

接下来我们封装一个函数实现Curve池子的兑换:

// CurveCrypto
function CurveCryptoExchange(address pool, uint256 token_in_id, uint256 token_out_id, address token_in,
    uint256 amount_in) internal {
    ApproveToken(token_in, pool, amount_in);
    ICurveCrypto(pool).exchange(token_in_id, token_out_id, amount_in, 0);
}

pool: Curve池子的地址

token_in_id: 输入token的编号

token_out_id: 输出token的编号

token_in: 输入token的地址

amount_in: 输入token的数量

注意在调用Curve池子的exchange()函数之前,我们需要先向Curve授权:

// approve
function ApproveToken(address token, address spender, uint256 amount) internal {
    uint256 alowance = IERC20(token).allowance(address(this), spender);
    if (alowance < amount) {
        IERC20(token).safeApprove(spender, 0);
        IERC20(token).safeApprove(spender, MAX_INT);
    }
}

Curve获得了我的合约的授权后,才可以通过transferFrom()方法,从我的合约把输入token转走。然后就可以调用exchange()进行兑换了。

封装Uniswap

接下来我们封装一个函数实现Uniswap的兑换:

// UniSwapV3
function UniswapV3Swap(address pool, address token_in, address token_out, uint256 amount_in) internal {
    bool zeroForOne = token_in < token_out;
    RepayData memory repay_data = RepayData(token_in, amount_in, pool);
    IUniswapV3Pair(pool).swap(address(this), zeroForOne, int256(amount_in),
        (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1), abi.encode(repay_data));
}

首先通过zeroForOne标志确定兑换的方向。

然后定义一个RepayData结构来存储还款信息(还款token、还款数量和还款地址),然后使用abi.encode()加密成一段bytes类型的data。然后调用Unisawp池子的swap()函数进行兑换。这里的address(this)表示我的合约地址。

回调

在Uniswap发起兑换后,它会把输出token转给我的合约,然后调用我的uniswapV3SwapCallback()函数:

function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata _data) public {
    receiveLoan(_data);
}

在函数的实现中,我们继续调用receiveLoan(),并在其中进行还款操作:

// callback
function receiveLoan(bytes memory data) public {
    require(!lock, "Locked");
    RepayData memory _repay_data = abi.decode(data, (RepayData));
    IERC20(_repay_data.repay_token).safeTransfer(_repay_data.recipient, _repay_data.repay_amount);
}

这里我们加了一个require条件要求合约是非锁定状态,来防止receiveLoan()被其他人恶意调用。

然后将还款信息解码,将token转给Uniswap池子。

注意在生产环境下尽量减少使用IERC20的low level call,因此我们把approve()和transfer()替换成了更加安全的safeApprove()和safeTransfer()。

封装Swapbase

为了方便,我们需要有一个统一的入口进行兑换,因此我们封装一个SwapBase()函数,通过function_id来识别兑换的协议:

// SwapBase
function SwapBase(address pool, uint256 function_id, uint256 amount_in, uint256 token_in_id, uint256 token_out_id,
    address token_in, address token_out) public returns(uint256) {
    uint256 balance = IERC20(token_out).balanceOf(address(this));
    if (function_id == 1) {
        UniswapV3Swap(pool, token_in, token_out, amount_in);
    } else if (function_id == 2) {
        CurveCryptoExchange(pool, token_in_id, token_out_id, token_in, amount_in);
    }
    return IERC20(token_out).balanceOf(address(this)) - balance;
}

根据function_id参数的不同,我们选择调用Curve还是Uniswap。同时还增加了一个返回值,用来返回最终得到的输出token的数量。

测试

下面就可以在truffle中进行编译和测试了(测试环境搭建参考上一讲)。

进入truffle控制台:

truffle console

定义一些基本地址:

WETH = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2';
USDT = '0xdAC17F958D2ee523a2206206994597C13D831ec7';
Curvepool = '0xD51a44d3FaE010294C616388b506AcdA1bfAAE46';
Uniswappool = '0x4e68Ccd3E89f51C3074ca5072bbAC773960dFa36';

定义合约实例:

instance = await SimpleArbi.deployed();

向合约转入10个ETH,并将其中5个兑换为WETH:

instance.send(web3.utils.toWei('10', 'ether'));
instance.ETHtoWETH(web3.utils.toWei('5', 'ether'));

把合约锁打开:

instance.setLock(false);

通过Curve把1个WETH兑换为USDT:

instance.SwapBase(Curvepool, 2, web3.utils.toWei('1'), 2, 0, WETH, USDT);

检查一下合约中的USDT数量:

usdt = await instance.getTokenBalance(USDT, instance.address);
usdt.toString();

再把USDT全部换回WETH:

instance.SwapBase(Curvepool, 2, usdt, 0, 2, USDT, WETH);

通过Uniswap把1个WETH兑换为USDT:

instance.SwapBase(Uniswappool, 1, web3.utils.toWei('1'), 0, 0, WETH, USDT);

检查一下合约中的USDT数量:

usdt = await instance.getTokenBalance(USDT, instance.address);
usdt.toString();

再把USDT全部换回WETH:

instance.SwapBase(Uniswappool, 1, usdt, 0, 0, USDT, WETH);

结语

至此,我们在合约中分别使用Curve和Uniswap进行了Swap操作,下一讲我们继续把两个操作串联起来,实现一个套利操作。

欢迎来即刻App与我互动,即刻账号: 月影007