Skip to content

Commit

Permalink
Merge pull request #284 from iotubeproject/uniswap_unwrapper
Browse files Browse the repository at this point in the history
uniswap unwrapper
  • Loading branch information
CoderZhi authored Dec 12, 2024
2 parents d3b52e6 + 7914aba commit ec1e424
Show file tree
Hide file tree
Showing 2 changed files with 219 additions and 0 deletions.
78 changes: 78 additions & 0 deletions contracts/UniswapUnwrapper.sol
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);
}
}
141 changes: 141 additions & 0 deletions test/UniswapUnwrapper.test.js
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);
});
});

0 comments on commit ec1e424

Please sign in to comment.