Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

uniswap unwrapper #284

Merged
merged 1 commit into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
});
});
Loading