Uniswap
对接 Uniswap V2 兑换代币,并测试验证。
在本文中,我们将和正式的 Uniswap V2 交互,实现使用Uniswap 进行代币兑换(swap)并通过测试验证兑换功能;
通过测试验证智能合约的行为是一个很好的方式,测试让你相信代码以我们想要的方式执行,而不是以它不应该的方式执行。
在本文中,我们还将学习到如何 fork 主网,并冒充(模拟)一个链上账号进行交易,并编写测试。
关于Uniswap V2
但在深入研究之前,为了本文完整,让我们再次介绍一下 Uniswap,Uniswap是一个去中心化的交易所(DEX),运行在以太坊区块链上(主网和其他一些网络)。顾名思义,Uniswap是用来交易ERC20代币的。
Uniswap有3个主要功能:
添加代币对流动性,获得LP ERC-20流动性代币
销毁 LP ERC-20流动性代币,取回配对的ERC-20代币
在这篇文章中,我们将重点讨论使用fork 主网在不同的代币之间进行兑换。
所以让我们开始吧! 🥳🥳🥳
创建一个项目并初始化
在命令行(CLI)上使用以下命令来初始化项目。
Copy mkdir uni_swap && cd uni_swap
npm init -y
安装项目所需的依赖项,运行:
Copy npm install --save hardhat @nomiclabs/hardhat-ethers @nomiclabs/hardhat-waffle ethers @uniswap/v2-core dotenv
初始化Hardhat项目
要初始化你的Hardhat项目,在CLI中运行npx hardhat
命令,并创建一个空的config.js 文件。
并定制你的Hardhat配置,因为我们要fork主网来与Uniswap交互。因此,Hardhat配置应该看起来类似于这样:
注意:用你的自己Alchemy API密钥替换URL中的<key>
部分。
编写合约实现兑换
为合约、脚本和测试创建目录,以便更好地组织代码。
在你的CLI中使用以下代码创建目录:
Copy mkdir contracts && mkdir scripts && mkdir tests
为了编写兑换合约,在合约目录内创建一个文件,命名为testSwap.sol
。
在你的 testSwap.sol
中导入Uniswap 等接口,并创建一个名为testSwap 的合约。
它应该看起来像这样:
现在,在testSwap
中,我们需要包括Uniswap Router 的地址,我们使用它来完成代币兑换。
使用下面的代码:
Copy //address of the uniswap v2 router
address private constant UNISWAP_V2_ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D ;
现在,定义要用来兑换的函数:
Copy // 兑换函数
function swap (
address _tokenIn ,
address _tokenOut ,
uint256 _amountIn ,
address _to ,
uint256 _deadline
) external {}
函数命名为**swap,**里面有
_tokenOut :是我们想从这次交易中获得的代币的地址。
_deadline :是交易应该被执行的时间期限。如果超过了最后期限,交易就会失败。
在兑换函数里面,我们要做的第一件事是在合约里面把所需数量的_tokenIn 转移到合约里,使用msg.sender
:
Copy // 把 token 从用户转移到合约
IERC20 (_tokenIn). transferFrom (msg.sender , address ( this ) , _amountIn);
一旦调用执行,_amountIn 数量的 _tokenIn 就会转入到testSwap
合约中
接下来,通过调用IERC20 授权,允许Uniswap合约花费testSwap
合约中_amountIn 数量的代币。
Copy // by calling IERC20 approve you allow the uniswap contract to spend the tokens in this contract
IERC20 (_tokenIn). approve (UNISWAP_V2_ROUTER , _amountIn);
在使用 Uniswap Router 兑换,需要为兑换代币的设置路径 ,路径上第一“站”是使用的代币,最后一“站”期望收到的代币。
所以,我们将声明一个名为path
的地址数组,填入 _tokenIn 的地址和 _tokenOut 的地址。
Copy address [] memory path;
path = new address []( 2 );
path[ 0 ] = _tokenIn; // DAI
path[ 1 ] = _tokenOut; // WETH
接下来,我们调用函数getAmountsOut ,以预估可以兑换代币数量,对真实兑换之前预知可兑换数量是很有用的。getAmountsOut 函数需要一个输入金额和一个代币地址的路径数组:
Copy uint256 [] memory amountsExpected = IUniswapV2Router (UNISWAP_V2_ROUTER). getAmountsOut (
_amountIn ,
path
);
最后,我们调用Uniswap Router的函数swapExactTokensforTokens ,并传入参数。
Copy uint256 [] memory amountsReceived = IUniswapV2Router (UNISWAP_V2_ROUTER). swapExactTokensForTokens (
amountsExpected[ 0 ] ,
(amountsExpected[ 1 ] * 990 ) / 1000 , // 接受 1% 的滑点
path ,
_to ,
_deadline
);
恭喜你 ! 我们的的兑换合约已经准备好了。🎉
完整的看起来应该类似是这样:
使用命令npx hardhat compile
来检查我们的智能合约中是否有错误。
现在,是时候为我们的合约运行一些测试了
编写测试脚本
在tests 文件夹中创建一个文件,并将其命名为***sample-test.js
***。
首先,要从Uniswap导入ERC20合约的ABI,同时,定义测试的结构和我们要使用的合约的地址。
Copy const ERC20ABI = require ( "@uniswap/v2-core/build/ERC20.json" ).abi;
describe ( "Test Swap" , function () {
const DAIAddress = "0x6B175474E89094C44Da98b954EedeAC495271d0F" ;
const WETHAddress = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" ;
const MyAddress = "0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B" ;
const DAIHolder = "0x5d38b4e4783e34e2301a2a36c39a03c45798c4dd" ;
}
这里,我们使用了4个地址:
DAIAddress 和WETHAddress 分别是Dai 合约和WETH 合约的地址,它们将在交易中使用
现在,在编写测试脚本之前,我们将部署testSwap 智能合约。为此,我们使用以下代码:
Copy let TestSwapContract;
beforeEach ( async () => {
const TestSwapFactory = await ethers .getContractFactory ( "testSwap" );
TestSwapContract = await TestSwapFactory .deploy ();
await TestSwapContract .deployed ();
})
beforeEach ( async () => {
const TestSwapFactory = await ethers .getContractFactory ( "testSwap" );
TestSwapContract = await TestSwapFactory .deploy ();
await TestSwapContract .deployed ();
})
为测试脚本创建一个测试用例,并“冒充”我们之前定义的DAIHolder 地址。
Copy it ( "should swap" , async () => {
await hre . network . provider .request ({
method : "hardhat_impersonateAccount" ,
params : [DAIHolder] ,
});
const impersonateSigner = await ethers .getSigner (DAIHolder);
在下一步,我们将通过使用冒充的账户获得其DAI代币 的初始余额。之后,我们将使用该余额进行兑换交易。
同样,我们也获取WETH代币 的余额,以便观察代币的兑换情况。
Copy const DAIContract = new ethers .Contract (DAIAddress , ERC20ABI , impersonateSigner)
const DAIHolderBalance = await DAIContract .balanceOf ( impersonateSigner .address)
const WETHContract = new ethers .Contract (WETHAddress , ERC20ABI , impersonateSigner)
const myBalance = await WETHContract .balanceOf (MyAddress);
console .log ( "Initial WETH Balance:" , ethers . utils .formatUnits ( myBalance .toString ()));
然后,我们将使用DAI合约来批准(授权)TestSwap 可使用兑换的金额:
Copy await DAIContract .approve ( TestSwapContract .address , DAIHolderBalance)
对于最后兑换截止时间,先获取最新区块的当前时间戳:
Copy // getting current timestamp
const latestBlock = await ethers . provider .getBlockNumber ();
const timestamp = ( await ethers . provider .getBlock (latestBlock)).timestamp;
通过调用我们编写的swap 函数进行交易。传入我们在上面配置的参数:
这个交易将从通过DAIHolder 发起:
Copy await TestSwapContract .connect (impersonateSigner) .swap (
DAIAddress ,
WETHAddress ,
DAIHolderBalance ,
MyAddress ,
timestamp + 1000 // adding 100 milliseconds to the current blocktime
)
最后,验证兑换交易:
Copy const myBalance_updated = await WETHContract .balanceOf (MyAddress);
console .log ( "Balance after Swap:" , ethers . utils .formatUnits ( myBalance_updated .toString ()));
const DAIHolderBalance_updated = await DAIContract .balanceOf ( impersonateSigner .address);
在这里,检查了兑换功能执行后我们账户的余额。
在这下面,我们写了一些测试以检查交易是否真实完成:
Copy expect ( DAIHolderBalance_updated .eq ( BigNumber .from ( 0 ))). to . be .true
expect ( myBalance_updated .gt (myBalance)). to . be .true;
由于我们使用了所有的余额进行交易,因此在第一个测试中,我们期望DAI代币余额应该等于0。
在第二个测试中,检查我们账户中的余额 是否比之前的大。
因此,这就是我们要进行的两个测试。
sample-test.js 应该类似于下面的样子,请注意文件开头的 require
语句:
当然,请自由探索,用它们尝试更多的测试。
现在,我们要用npx hardhat test
命令来运行这些测试。
结果应该是这样的:
正如你所看到的,我们的初始余额在兑换完成后有所增加。
而我们编写的测试也成功了!!。🎉🎉🎉
如果你一直跟到最后,那么恭喜你,你已经做得很好了。