# 区块链开发课第五讲 智能合约开发(2) **Published by:** [yueying007](https://paragraph.com/@yueying007/) **Published on:** 2022-04-21 **URL:** https://paragraph.com/@yueying007/2 ## Content 这节课,我们继续完善SimpleArbi.sol,在合约内部完成Curve和Uniswap的swap操作。 github:GitHub - yueying007/blockchainclassContribute to yueying007/blockchainclass development by creating an account on GitHub.https://github.comDexCurve和Uniswap是以太坊上排名第一和第二的去中心化交易所(DEX),它们分别都进行过几个版本的迭代升级,从最初的AMM(automatic market maker)发展到后来的 CLMM(concentrated liquidity market maker)模式,具体的swap算法不是本文的讨论重点,我们今天只通过接口来认识这两个协议。Curve首先来看Curve的一个池子:Curve: USDT/WBTC/WETH Pool | Address: 0xD51a44d3...A1bfAAE46 | EtherscanContract: 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它提供了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 | EtherscanContract: Verified | Balance: $161,439,404.51 across 1 Chain | Transactions: 109 | As at Oct-24-2025 09:47:22 PM (UTC)https://etherscan.io它提供了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 ## Publication Information - [yueying007](https://paragraph.com/@yueying007/): Publication homepage - [All Posts](https://paragraph.com/@yueying007/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@yueying007): Subscribe to updates