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

By [yueying007](https://paragraph.com/@yueying007) · 2022-04-21

---

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

github:

[

GitHub - yueying007/blockchainclass
-----------------------------------

Contribute to yueying007/blockchainclass development by creating an account on GitHub.

https://github.com

![](https://storage.googleapis.com/papyrus_images/14df3d9367ff4dd1c02c6b37edbeeaaeecccdec76b9a3ce88a3b9144b747b31c.png)

](https://github.com/yueying007/blockchainclass.git)

Dex
---

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

Curve
-----

首先来看Curve的一个池子：

[

Curve: USDT/WBTC/WETH Pool | Address: 0xD51a44d3...A1bfAAE46 | Etherscan
------------------------------------------------------------------------

Contract: Verified | Balance: $16,643,787.06 across 1 Chain | Transactions: 44,685 | As at Oct-24-2025 09:47:20 PM (UTC)

https://etherscan.io

![](https://storage.googleapis.com/papyrus_images/c3ceb1ec55f2d6c2b4eeb2df764e4bc8936caf048a981c893b5f220aa9d207a0.jpg)

](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的池子:

[

Uniswap V3: USDT | Address: 0x4e68Ccd3...3960dFa36 | Etherscan
--------------------------------------------------------------

Contract: Verified | Balance: $161,439,404.51 across 1 Chain | Transactions: 109 | As at Oct-24-2025 09:47:22 PM (UTC)

https://etherscan.io

![](https://storage.googleapis.com/papyrus_images/c3ceb1ec55f2d6c2b4eeb2df764e4bc8936caf048a981c893b5f220aa9d207a0.jpg)

](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_

---

*Originally published on [yueying007](https://paragraph.com/@yueying007/2)*
