Skip to content

Commit a2aa254

Browse files
committed
feat: add swapExactInputSingleOfficial (uniswap v4)
1 parent b94c1c0 commit a2aa254

File tree

2 files changed

+144
-18
lines changed

2 files changed

+144
-18
lines changed

src/ProxyUniswapV4.sol

Lines changed: 81 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
1313
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
1414
import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
1515
import {TransferHelper} from "./libraries/TransferHelper.sol";
16+
import {ActionConstants} from "@uniswap/v4-periphery/src/libraries/ActionConstants.sol";
1617

1718
contract ProxyUniswapV4 is Ownable {
1819
using StateLibrary for IPoolManager;
@@ -21,20 +22,19 @@ contract ProxyUniswapV4 is Ownable {
2122
IPoolManager public immutable poolManager;
2223
IPermit2 public immutable permit2;
2324

24-
uint256 public feePercent = 50;
25+
uint256 public feePercent = 5_000;
2526

26-
uint256 public feeBase = 100;
27+
uint256 public constant FEE_DENOMINATOR = 10_000;
2728

2829
constructor(address _router, address _permit2, address _initialOwner) Ownable(_initialOwner) {
2930
router = IUniversalRouter(_router);
3031
permit2 = IPermit2(_permit2);
3132
// poolManager = IPoolManager(_poolManager);
3233
}
3334

34-
function setFeePercent(uint256 _percent, uint256 _base) external onlyOwner {
35-
require(_percent <= _base, "Fee cannot exceed feeBase%");
35+
function setFeePercent(uint256 _percent) external onlyOwner {
36+
require(_percent <= FEE_DENOMINATOR, "Fee cannot exceed feeBase%");
3637
feePercent = _percent;
37-
feeBase = _base;
3838
}
3939

4040
function swapExactInputSingle(
@@ -44,6 +44,17 @@ contract ProxyUniswapV4 is Ownable {
4444
uint128 amountOutMin,
4545
address recipient
4646
) external payable returns (uint256 amountOut) {
47+
(Currency currencyIn, Currency currencyOut) =
48+
zeroForOne ? (key.currency0, key.currency1) : (key.currency1, key.currency0);
49+
50+
// Transfer the input token to Proxy Router
51+
if (!currencyIn.isAddressZero()) {
52+
address tokenIn = Currency.unwrap(currencyIn);
53+
TransferHelper.safeTransferFrom(tokenIn, msg.sender, address(this), amountIn);
54+
TransferHelper.safeApprove(tokenIn, address(permit2), amountIn);
55+
permit2.approve(tokenIn, address(router), amountIn, uint48(block.timestamp + 10));
56+
}
57+
4758
// Encode the Universal Router command
4859
bytes memory commands = abi.encodePacked(uint8(Commands.V4_SWAP));
4960
bytes[] memory inputs = new bytes[](1);
@@ -63,22 +74,13 @@ contract ProxyUniswapV4 is Ownable {
6374
hookData: bytes("")
6475
})
6576
);
66-
(Currency currencyIn, Currency currencyOut) =
67-
zeroForOne ? (key.currency0, key.currency1) : (key.currency1, key.currency0);
77+
6878
params[1] = abi.encode(currencyIn, amountIn);
6979
params[2] = abi.encode(currencyOut, amountOutMin);
7080

7181
// Combine actions and params into inputs
7282
inputs[0] = abi.encode(actions, params);
7383

74-
// Transfer the input token to Proxy Router
75-
if (!currencyIn.isAddressZero()) {
76-
address tokenIn = Currency.unwrap(currencyIn);
77-
TransferHelper.safeTransferFrom(tokenIn, msg.sender, address(this), amountIn);
78-
TransferHelper.safeApprove(tokenIn, address(permit2), amountIn);
79-
permit2.approve(tokenIn, address(router), amountIn, uint48(block.timestamp + 10));
80-
}
81-
8284
uint256 amountOut_before = currencyOut.balanceOf(address(this));
8385

8486
// Execute the swap
@@ -87,7 +89,7 @@ contract ProxyUniswapV4 is Ownable {
8789

8890
// Verify and return the output amount
8991
amountOut = currencyOut.balanceOf(address(this)) - amountOut_before;
90-
uint256 feeAmount = (amountOut * feePercent) / feeBase;
92+
uint256 feeAmount = (amountOut * feePercent) / FEE_DENOMINATOR;
9193
uint256 userAmount = amountOut - feeAmount;
9294
require(userAmount >= amountOutMin, "AmountOutMin not met");
9395

@@ -97,5 +99,68 @@ contract ProxyUniswapV4 is Ownable {
9799
return userAmount;
98100
}
99101

102+
function swapExactInputSingleOfficial(
103+
PoolKey calldata key,
104+
bool zeroForOne,
105+
uint128 amountIn,
106+
uint128 amountOutMin,
107+
address recipient
108+
) external payable returns (uint256 amountOut) {
109+
(Currency currencyIn, Currency currencyOut) =
110+
zeroForOne ? (key.currency0, key.currency1) : (key.currency1, key.currency0);
111+
112+
// Transfer the input token to Proxy Router
113+
if (!currencyIn.isAddressZero()) {
114+
address tokenIn = Currency.unwrap(currencyIn);
115+
TransferHelper.safeTransferFrom(tokenIn, msg.sender, address(this), amountIn);
116+
TransferHelper.safeApprove(tokenIn, address(permit2), amountIn);
117+
permit2.approve(tokenIn, address(router), amountIn, uint48(block.timestamp + 10));
118+
}
119+
120+
// Encode the Universal Router command
121+
bytes memory commands =
122+
abi.encodePacked(uint8(Commands.V4_SWAP), uint8(Commands.PAY_PORTION), uint8(Commands.SWEEP));
123+
bytes[] memory inputs = new bytes[](3);
124+
125+
// Commands.V4_SWAP
126+
bytes memory actions =
127+
abi.encodePacked(uint8(Actions.SWAP_EXACT_IN_SINGLE), uint8(Actions.SETTLE), uint8(Actions.TAKE));
128+
129+
// Prepare parameters for each action
130+
bytes[] memory params = new bytes[](3);
131+
132+
// SWAP_EXACT_IN_SINGLE calldata
133+
params[0] = abi.encode(
134+
IV4Router.ExactInputSingleParams({
135+
poolKey: key,
136+
zeroForOne: zeroForOne,
137+
amountIn: amountIn,
138+
amountOutMinimum: amountOutMin,
139+
hookData: bytes("")
140+
})
141+
);
142+
143+
// SETTLE (Currency currency, uint256 amount, bool payerIsUser)
144+
params[1] = abi.encode(currencyIn, ActionConstants.OPEN_DELTA, true);
145+
146+
// TAKE (Currency currency, address recipient, uint256 amount)
147+
params[2] = abi.encode(currencyOut, ActionConstants.ADDRESS_THIS, ActionConstants.OPEN_DELTA);
148+
149+
// TODO:Commands.V4_SWAP
150+
inputs[0] = abi.encode(actions, params);
151+
152+
// TODO:Commands.PAY_PORTION encode(token, recipient, bips)
153+
inputs[1] = abi.encode(currencyOut, owner(), feePercent);
154+
155+
// TODO:Commands.SWEEP encode(token, recipient, amountMinimum)
156+
inputs[2] = abi.encode(currencyOut, recipient, amountOutMin);
157+
158+
uint256 amountOut_before = currencyOut.balanceOf(recipient);
159+
160+
router.execute{value: amountIn}(commands, inputs, block.timestamp + 20);
161+
162+
amountOut = currencyOut.balanceOf(recipient) - amountOut_before;
163+
}
164+
100165
receive() external payable {}
101166
}

test/ProxyUniswapV4.t.sol

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,13 @@ contract ProxyUniswapV4Test is Test {
3434
proxy = new ProxyUniswapV4(universalRouter, permit2, owner);
3535

3636
// set user balance
37+
deal(owner, 100 ether);
3738
deal(user, 100 ether);
3839
deal(USDT, user, 10000 * 1e6);
3940

41+
vm.prank(owner);
42+
proxy.setFeePercent(5000); // 10_000
43+
4044
PoolKey memory poolKey = PoolKey({
4145
currency0: Currency.wrap(ZERO),
4246
currency1: Currency.wrap(USDT),
@@ -77,7 +81,34 @@ contract ProxyUniswapV4Test is Test {
7781

7882
proxy.swapExactInputSingle{value: amountIn}(poolKey, zeroForOne, uint128(amountIn), 0, recipient);
7983

80-
console.log("Deduct %d % Proxy Fees After:", proxy.feePercent());
84+
console.log("Deduct %d/%d Proxy Fees After:", proxy.feePercent(), proxy.FEE_DENOMINATOR());
85+
86+
uint256 balance_recipient = IERC20(USDT).balanceOf(recipient);
87+
88+
console.log("recipient Get USDT: ", balance_recipient / 1e6, ".", balance_recipient % 1e6);
89+
vm.stopPrank();
90+
}
91+
92+
function test_SwapExactETHForTokenOfficialWithFee() public {
93+
vm.startPrank(user);
94+
95+
uint256 amountIn = 1 ether;
96+
97+
console.log("user Pay 1 ETH");
98+
99+
PoolKey memory poolKey = PoolKey({
100+
currency0: Currency.wrap(ZERO),
101+
currency1: Currency.wrap(USDT),
102+
fee: 500,
103+
tickSpacing: 10,
104+
hooks: IHooks(address(0))
105+
});
106+
107+
bool zeroForOne = true;
108+
109+
proxy.swapExactInputSingleOfficial{value: amountIn}(poolKey, zeroForOne, uint128(amountIn), 0, recipient);
110+
111+
console.log("Deduct %d/%d Proxy Fees After:", proxy.feePercent(), proxy.FEE_DENOMINATOR());
81112

82113
uint256 balance_recipient = IERC20(USDT).balanceOf(recipient);
83114

@@ -107,7 +138,37 @@ contract ProxyUniswapV4Test is Test {
107138

108139
proxy.swapExactInputSingle(poolKey, zeroForOne, uint128(amountIn), 0, recipient);
109140

110-
console.log("Deduct %d % Proxy Fees After:", proxy.feePercent());
141+
console.log("Deduct %d/%d Proxy Fees After:", proxy.feePercent(), proxy.FEE_DENOMINATOR());
142+
143+
uint256 balance_recipient = recipient.balance;
144+
console.log("recipient Get ETH: ", balance_recipient / 1e18, ".", balance_recipient % 1e18);
145+
146+
vm.stopPrank();
147+
}
148+
149+
function test_SwapExactTokenForETHOfficialWithFee() public {
150+
vm.startPrank(user);
151+
152+
uint256 amountIn = 2000 * 1e6;
153+
154+
console.log("user Pay 2000 USDT");
155+
156+
// approve USDT to proxy
157+
TransferHelper.safeApprove(USDT, address(proxy), amountIn);
158+
159+
PoolKey memory poolKey = PoolKey({
160+
currency0: Currency.wrap(ZERO),
161+
currency1: Currency.wrap(USDT),
162+
fee: 500,
163+
tickSpacing: 10,
164+
hooks: IHooks(address(0))
165+
});
166+
167+
bool zeroForOne = false;
168+
169+
proxy.swapExactInputSingleOfficial(poolKey, zeroForOne, uint128(amountIn), 0, recipient);
170+
171+
console.log("Deduct %d/%d Proxy Fees After:", proxy.feePercent(), proxy.FEE_DENOMINATOR());
111172

112173
uint256 balance_recipient = recipient.balance;
113174
console.log("recipient Get ETH: ", balance_recipient / 1e18, ".", balance_recipient % 1e18);

0 commit comments

Comments
 (0)