Uniswap Ethereum tests validate the behavior of your smart contract. They give you confidence that your code performs in the ways that you intend, and does not perform in the ways that it should not.
In the previous article, we learned aboutĀ mainnet forkingĀ and played with Vitalikās account usingĀ impersonate account.
Now we have decided to take this further and test out theĀ UniswapāsĀ swap implementation. Yes! You heard it correct š
But before diving into it, let us understand more about Uniswap
Uniswap is a Decentralized Exchange, running on the Ethereum Blockchain (Mainnet and a few more). As the name suggests, Uniswap is used forĀ trading ERC20 tokens.
There are 3 main functionalities of Uniswap:
Swap between different tokensAdd liquidity to the market and get rewarded with pair exchange ERC-20 liquidity tokensBurn ERC-20 liquidity tokens and get back the ERC-20 tokens that the pair exchange allows traders to exchange
In this article, we are going to focus on swapping between different tokens using forking.
NOTE: We recommend you go through theĀ previous partĀ of this article first and then follow along with this article to get a better grip on whatās happening.
So letās get started!Ā š„³š„³
Use the following commands on your CLI to initialize your project.
mkdir uni_swap && cd uni_swap
npm init -y
Install the required dependencies for the project, run
npm install --save hardhat @nomiclabs/hardhat-ethers @nomiclabs/hardhat-waffle ethers @uniswap/v2-core dotenv
To initialize your Hardhat project, runĀ npx hardhatĀ command in your CLI, and create an emptyĀ config.jsĀ file.
Customize your hardhat config:
Because we are going to fork the mainnet to test the Uniswap. Therefore, your hardhat config should look something similar to this:
Note: Replace theĀ <key>Ā component of the URL with your personalĀ AlchemyĀ API key.
Create directories for contracts, scripts, and tests for better code organization.
Use the following code in your CLI.
mkdir contracts && mkdir scripts && mkdir tests
In order to write the swap contract, create a file inside the contracts directory and name itĀ testSwap.sol
For the purpose of our contract, we need to include an interface: Uniswap, to use their functions
Writing the smart contract:
Import the interfaces inside yourĀ testSwap.solĀ and create a contract namedĀ testSwap
It should look like this:
Now, insideĀ testSwap, we need to include the address of theĀ Uniswap Router.Ā It is required for us to do the trade between the tokens.
Use the following code:
//address of the uniswap v2 router
address private constant UNISWAP_V2_ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D;
Now, define the function we are going to use for swapping:
// swap function
function swap(
address _tokenIn,
address _tokenOut,
uint256 _amountIn,
address _to,
uint256 _deadline
) external {
}
We have named our function asĀ swap,Ā inside that we have
Inside the swap function, the first thing that weāre going to do is transfer theĀ _tokenInĀ addressā amount inside the contract, usingĀ msg.sender.
// transfer the amount in tokens from msg.sender to this contract
IERC20(_tokenIn).transferFrom(msg.sender, address(this), _amountIn);
Once this is called, theĀ _tokenInĀ address would have the amount present insideĀ _amountIn.
Next, by callingĀ IERC20Ā approve you allow the Uniswap contract to spend theĀ _amountInĀ tokens in this contract
//by calling IERC20 approve you allow the uniswap contract to spend the tokens in this contract
IERC20(_tokenIn).approve(UNISWAP_V2_ROUTER, _amountIn);
Now, one of the parameters we need to call for swapping the tokens isĀ path.
So, we will declare an array of addresses namedĀ path.
Address of _tokenIn and the address of _tokenOut.
address[] memory path;
path = new address[](2);
path[0] = _tokenIn; // DAI
path[1] = _tokenOut; // WETH
Next, we will call the functionĀ getAmountsOut, which is useful for calculating the amount of tokens we should be expecting on doing a swap. It takes an input amount and an array of token addresses. The array, as you would have guessed it, is theĀ pathĀ that we have defined above.
uint256[] memory amountsExpected = IUniswapV2Router(UNISWAP_V2_ROUTER).getAmountsOut(
_amountIn,
path
);
Then finally, we are going to call the functionĀ swapExactTokensforTokensĀ on Uniswap Router, and pass in the parameters.
uint256[] memory amountsReceived = IUniswapV2Router(UNISWAP_V2_ROUTER).swapExactTokensForTokens(
amountsExpected[0],
(amountsExpected[1]*990)/1000, // accepting a slippage of 1%
path,
_to,
_deadline
);
CONGRATULATIONS! Our contract is ready. š
It should look something similar to this: š
Use the commandĀ npx hardhat compileĀ to check for any error in our smart contract.
Now, itās time ā to run some tests for our contract!
Create a file insideĀ testsĀ folder and name itĀ sample-test.js.
First, we are going to import the ERC20 contractās abi from Uniswap.
Also, define the structure of the test that weāre going to use with the addresses of the contracts that weāre going to use.
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";
}
Here, we have used 4 addresses:
Now, before writing the test script, we will deploy theĀ testSwapĀ smart contract. For that we have the following code:
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();
})
Create a structure for the test script. AndĀ impersonateĀ theĀ DAIHolderĀ address which we have defined earlier.
it("should swap", async () => {
await hre.network.provider.request({
method: "hardhat_impersonateAccount",
params: [DAIHolder],
});
const impersonateSigner = await ethers.getSigner(DAIHolder);
In the next step, we will get the initial balance ofĀ DAI tokenĀ by using the impersonated account. Later, we will swap the total balance present at the address.
Similarly, we will also get the balance of theĀ WETH token, to observe the swapping of the tokens.
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()));
Then, we will use the DAI contract to approve the swap of the total balance present in it.
await DAIContract.approve(TestSwapContract.address, DAIHolderBalance)
For the deadline, we will use the current timestamp of the block.
// getting current timestamp
const latestBlock = await ethers.provider.getBlockNumber();
const timestamp = (await ethers.provider.getBlock(latestBlock)).timestamp;
We will do the trade by calling theĀ swapĀ function that we wrote. Passing in the parameters that we have configured above.
And this transaction will be sent from theĀ DAIHolder.
await TestSwapContract.connect(impersonateSigner).swap(
DAIAddress,
WETHAddress,
DAIHolderBalance,
MyAddress,
timestamp + 1000 // adding 100 milliseconds to the current blocktime
)
Finally, itās time to test the swap transaction! š¬
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);
Here, we have first checked the balance of our account after the execution of the swap function.
Below this, we have written some tests to check whether the transaction was true or not!
expect(DAIHolderBalance_updated.eq(BigNumber.from(0))).to.be.true
expect(myBalance_updated.gt(myBalance)).to.be.true;
Since we have swapped the total balance, therefore in the first test we expect theĀ balanceĀ ofĀ DAI addressĀ should be equal toĀ 0. In the second test, we are checking whether theĀ balanceĀ in our account is nowĀ greaterĀ than earlier or not.
Therefore, these are the two tests we are going to run.
The sample-test.js should look similar to the following. It is critical you note theĀ requireĀ statements at the start of the file.
Of course, feel free to explore and try out more tests with them.
For now, we are going to run these tests using the commandĀ npx hardhat test
The results should look like this:
As you can see, our initial balance has increased after the swapping is done.
And the test we wrote came out successful!!! ššš
If you have followed along till the end then congratulations, you have done great.
Follow us on Twitter, if you liked our content.
Authors (open to feedback): š
Amateur-DevĀ andĀ Pari Tomar