-
Notifications
You must be signed in to change notification settings - Fork 27
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #284 from iotubeproject/uniswap_unwrapper
uniswap unwrapper
- Loading branch information
Showing
2 changed files
with
219 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
}); | ||
}); |