From 7914aba17047842b1c76e8ae21b6fbf4abd370ba Mon Sep 17 00:00:00 2001 From: zhi Date: Wed, 11 Dec 2024 00:18:25 +0800 Subject: [PATCH] uniswap unwrapper --- contracts/UniswapUnwrapper.sol | 78 ++++++++++++++++++ test/UniswapUnwrapper.test.js | 141 +++++++++++++++++++++++++++++++++ 2 files changed, 219 insertions(+) create mode 100644 contracts/UniswapUnwrapper.sol create mode 100644 test/UniswapUnwrapper.test.js diff --git a/contracts/UniswapUnwrapper.sol b/contracts/UniswapUnwrapper.sol new file mode 100644 index 0000000..8670d32 --- /dev/null +++ b/contracts/UniswapUnwrapper.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT +pragma solidity = 0.8.20; + +import "./interfaces/IUniswapV2Router02.sol"; + +interface IERC20 { + function transfer(address to, uint256 value) external returns (bool); + function approve(address spender, uint256 value) external returns (bool); +} + +contract UniswapUnwrapper { + event Swap(address indexed tokenIn, address indexed tokenOut, uint256 amountIn, uint256 amountOut, address to); + + struct SwapData { + address tokenOut; + uint256 amountOutMin; + address to; + uint256 deadline; + } + address immutable public router; + + constructor(address _router) { + router = _router; + } + + receive() external payable { + } + + function transfer(IERC20 _token, address _to, uint256 _amount) external { + require(_token.transfer(_to, _amount), "UniswapUnwrapper: transfer failed"); + emit Swap(address(_token), address(0), _amount, 0, _to); + } + + function onReceive(address _sender, IERC20 _token, uint256 _amount, bytes calldata _payload) external { + address recipient = _sender; + SwapData memory swapData = abi.decode(_payload, (SwapData)); + if (swapData.to != address(0)) { + recipient = swapData.to; + } + if (swapData.deadline < block.timestamp) { + this.transfer(_token, recipient, _amount); + return; + } + address weth = IUniswapV2Router02(router).WETH(); + address[] memory path; + if (swapData.tokenOut == address(0)) { + path = new address[](2); + } else if (swapData.tokenOut == weth) { + path = new address[](2); + } else { + path = new address[](3); + path[2] = swapData.tokenOut; + } + path[0] = _token; + path[1] = weth; + + uint256[] memory amounts; + try IUniswapV2Router02(router).getAmountsOut(_amount, path) returns (uint256[] memory _amounts) { + amounts = _amounts; + } catch { + this.transfer(_token, _amount, recipient); + return; + } + if (amounts[amounts.length - 1] < swapData.amountOutMin) { + this.transfer(_token, _amount, recipient); + return; + } + require(_token.approve(router, _amount), "UniswapUnwrapper: approve failed"); + if (swapData.tokenOut == address(0)) { + amounts = IUniswapV2Router02(router).swapExactTokensForETH(_amount, swapData.amountOutMin, path, recipient, swapData.deadline); + } else if (swapData.tokenOut == weth) { + amounts = IUniswapV2Router02(router).swapExactTokensForETH(_amount, swapData.amountOutMin, path, recipient, swapData.deadline); + } else { + amounts = IUniswapV2Router02(router).swapExactTokensForTokens(_amount, swapData.amountOutMin, path, recipient, swapData.deadline); + } + emit Swap(_token, swapData.tokenOut, _amount, amounts[amounts.length - 1], recipient); + } +} diff --git a/test/UniswapUnwrapper.test.js b/test/UniswapUnwrapper.test.js new file mode 100644 index 0000000..ea58456 --- /dev/null +++ b/test/UniswapUnwrapper.test.js @@ -0,0 +1,141 @@ +const MockERC20 = artifacts.require("MockERC20"); +const MockUniswapV2Router02 = artifacts.require("MockUniswapV2Router02"); +const UniswapUnwrapper = artifacts.require("UniswapUnwrapper"); +const {ethers, AbiCoder} = require("ethers"); + +const expectSwap = function(log, tokenIn, tokenOut, amountIn, amountOut, to) { + assert.equal(log.event, "Swap"); + assert.equal(log.args.tokenIn, tokenIn); + assert.equal(log.args.tokenOut, tokenOut); + assert.equal(log.args.amountIn, amountIn); + assert.equal(log.args.amountOut, amountOut); + assert.equal(log.args.to, to); +} + +contract("UniswapUnwrapper", function([owner, recipient]) { + let unwrapper; + let router; + let token; + let weth; + + beforeEach(async function() { + // Deploy mock WETH + weth = await MockERC20.new("Wrapped ETH", "WETH"); + + // Deploy mock router + router = await MockUniswapV2Router02.new(weth.address); + + // Deploy mock token + token = await MockERC20.new("Test Token", "TEST"); + + // Deploy unwrapper + unwrapper = await UniswapUnwrapper.new(router.address); + + // Approve tokens + await token.approve(unwrapper.address, ethers.MaxUint256); + await token.approve(router.address, ethers.MaxUint256); + }); + + it("should swap tokens for ETH", async function() { + const amountIn = ethers.parseEther("100"); + const amountOutMin = ethers.parseEther("90"); + const deadline = Math.floor(Date.now() / 1000) + 3600; + await router.sendTransaction({value: Number(amountOutMin)}); + + const swapData = { + tokenIn: token.address, + tokenOut: ethers.ZeroAddress, // ETH + amountIn: amountIn, + amountOutMin: amountOutMin, + to: recipient, + deadline: deadline + }; + + const encodedData = AbiCoder.defaultAbiCoder().encode( + ["tuple(address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOutMin, address to, uint256 deadline)"], + [swapData] + ); + + await router.setAmountOut(amountOutMin); + await token.transfer(unwrapper.address, amountIn); + const tx = await unwrapper.onReceive(owner, token.address, amountIn, encodedData); + expectSwap(tx.logs[0], token.address, ethers.ZeroAddress, amountIn, amountOutMin, recipient); + }); + + it("should swap tokens for tokens", async function() { + const amountIn = ethers.parseEther("100"); + const amountOutMin = ethers.parseEther("90"); + const deadline = Math.floor(Date.now() / 1000) + 3600; + + const token2 = await MockERC20.new("Test Token 2", "TEST2"); + await token2.transfer(router.address, amountOutMin); + + const swapData = { + tokenIn: token.address, + tokenOut: token2.address, + amountIn: amountIn, + amountOutMin: amountOutMin, + to: recipient, + deadline: deadline + }; + + const encodedData = AbiCoder.defaultAbiCoder().encode( + ["tuple(address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOutMin, address to, uint256 deadline)"], + [swapData] + ); + + await router.setAmountOut(amountOutMin); + await token.transfer(unwrapper.address, amountIn); + const tx = await unwrapper.onReceive(owner, token.address, amountIn, encodedData); + expectSwap(tx.logs[0], token.address, token2.address, amountIn, amountOutMin, recipient); + }); + + it("should transfer token in if deadline passed", async function() { + const amountIn = ethers.parseEther("100"); + const amountOutMin = ethers.parseEther("90"); + const deadline = Math.floor(Date.now() / 1000) - 3600; // Passed deadline + + const swapData = { + tokenIn: token.address, + tokenOut: ethers.ZeroAddress, + amountIn: amountIn, + amountOutMin: amountOutMin, + to: recipient, + deadline: deadline + }; + + const encodedData = AbiCoder.defaultAbiCoder().encode( + ["tuple(address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOutMin, address to, uint256 deadline)"], + [swapData] + ); + + await token.transfer(unwrapper.address, amountIn); + const tx = await unwrapper.onReceive(owner, token.address, amountIn, encodedData); + expectSwap(tx.logs[0], token.address, ethers.ZeroAddress, amountIn, 0, recipient); + }); + + it("should transfer token in if slippage too high", async function() { + const amountIn = ethers.parseEther("100"); + const amountOutMin = ethers.parseEther("90"); + const deadline = Math.floor(Date.now() / 1000) + 3600; + + const swapData = { + tokenIn: token.address, + tokenOut: ethers.ZeroAddress, + amountIn: amountIn, + amountOutMin: amountOutMin, + to: recipient, + deadline: deadline + }; + + const encodedData = AbiCoder.defaultAbiCoder().encode( + ["tuple(address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOutMin, address to, uint256 deadline)"], + [swapData] + ); + + await router.setAmountOut(amountOutMin - ethers.toBigInt(1)); // Set amount less than minimum + await token.transfer(unwrapper.address, amountIn); + const tx = await unwrapper.onReceive(owner, token.address, amountIn, encodedData); + expectSwap(tx.logs[0], token.address, ethers.ZeroAddress, amountIn, 0, recipient); + }); +});