feat: Add transfer out for Uniswap V4

Add transfer in Executor and pass receiver address in encoding
This is done by using the TAKE action instead of TAKE_ALL

--- don't change below this line ---
ENG-4315 Took 1 hour 36 minutes

Took 2 minutes
This commit is contained in:
Diana Carvalho
2025-04-15 16:21:23 +01:00
parent 328a281a44
commit ec87969aa6
7 changed files with 43 additions and 27 deletions

View File

@@ -46,6 +46,7 @@ contract UniswapV4Executor is IExecutor, V4Router, ICallback, TokenTransfer {
address tokenOut, address tokenOut,
bool zeroForOne, bool zeroForOne,
TransferType transferType, TransferType transferType,
address receiver,
UniswapV4Executor.UniswapV4Pool[] memory pools UniswapV4Executor.UniswapV4Pool[] memory pools
) = _decodeData(data); ) = _decodeData(data);
@@ -72,7 +73,7 @@ contract UniswapV4Executor is IExecutor, V4Router, ICallback, TokenTransfer {
bytes memory actions = abi.encodePacked( bytes memory actions = abi.encodePacked(
uint8(Actions.SWAP_EXACT_IN_SINGLE), uint8(Actions.SWAP_EXACT_IN_SINGLE),
uint8(Actions.SETTLE_ALL), uint8(Actions.SETTLE_ALL),
uint8(Actions.TAKE_ALL) uint8(Actions.TAKE)
); );
bytes[] memory params = new bytes[](3); bytes[] memory params = new bytes[](3);
@@ -87,7 +88,7 @@ contract UniswapV4Executor is IExecutor, V4Router, ICallback, TokenTransfer {
}) })
); );
params[1] = abi.encode(tokenIn, amountIn); // currency to settle params[1] = abi.encode(tokenIn, amountIn); // currency to settle
params[2] = abi.encode(tokenOut, uint256(0)); // currency to take params[2] = abi.encode(tokenOut, receiver, uint256(0)); // currency to take. 0 means to take the full amount
swapData = abi.encode(actions, params); swapData = abi.encode(actions, params);
} else { } else {
PathKey[] memory path = new PathKey[](pools.length); PathKey[] memory path = new PathKey[](pools.length);
@@ -104,7 +105,7 @@ contract UniswapV4Executor is IExecutor, V4Router, ICallback, TokenTransfer {
bytes memory actions = abi.encodePacked( bytes memory actions = abi.encodePacked(
uint8(Actions.SWAP_EXACT_IN), uint8(Actions.SWAP_EXACT_IN),
uint8(Actions.SETTLE_ALL), uint8(Actions.SETTLE_ALL),
uint8(Actions.TAKE_ALL) uint8(Actions.TAKE)
); );
bytes[] memory params = new bytes[](3); bytes[] memory params = new bytes[](3);
@@ -119,22 +120,22 @@ contract UniswapV4Executor is IExecutor, V4Router, ICallback, TokenTransfer {
}) })
); );
params[1] = abi.encode(currencyIn, amountIn); params[1] = abi.encode(currencyIn, amountIn);
params[2] = abi.encode(Currency.wrap(tokenOut), uint256(0)); params[2] = abi.encode(Currency.wrap(tokenOut), receiver, uint256(0));
swapData = abi.encode(actions, params); swapData = abi.encode(actions, params);
} }
uint256 tokenOutBalanceBefore; uint256 tokenOutBalanceBefore;
tokenOutBalanceBefore = tokenOut == address(0) tokenOutBalanceBefore = tokenOut == address(0)
? address(this).balance ? receiver.balance
: IERC20(tokenOut).balanceOf(address(this)); : IERC20(tokenOut).balanceOf(receiver);
executeActions(swapData); executeActions(swapData);
uint256 tokenOutBalanceAfter; uint256 tokenOutBalanceAfter;
tokenOutBalanceAfter = tokenOut == address(0) tokenOutBalanceAfter = tokenOut == address(0)
? address(this).balance ? receiver.balance
: IERC20(tokenOut).balanceOf(address(this)); : IERC20(tokenOut).balanceOf(receiver);
calculatedAmount = tokenOutBalanceAfter - tokenOutBalanceBefore; calculatedAmount = tokenOutBalanceAfter - tokenOutBalanceBefore;
@@ -155,10 +156,11 @@ contract UniswapV4Executor is IExecutor, V4Router, ICallback, TokenTransfer {
address tokenOut, address tokenOut,
bool zeroForOne, bool zeroForOne,
TransferType transferType, TransferType transferType,
address receiver,
UniswapV4Pool[] memory pools UniswapV4Pool[] memory pools
) )
{ {
if (data.length < 67) { if (data.length < 88) {
revert UniswapV4Executor__InvalidDataLength(); revert UniswapV4Executor__InvalidDataLength();
} }
@@ -166,10 +168,11 @@ contract UniswapV4Executor is IExecutor, V4Router, ICallback, TokenTransfer {
tokenOut = address(bytes20(data[20:40])); tokenOut = address(bytes20(data[20:40]));
zeroForOne = (data[40] != 0); zeroForOne = (data[40] != 0);
transferType = TransferType(uint8(data[41])); transferType = TransferType(uint8(data[41]));
receiver = address(bytes20(data[42:62]));
uint256 poolsLength = (data.length - 42) / 26; // 26 bytes per pool object uint256 poolsLength = (data.length - 62) / 26; // 26 bytes per pool object
pools = new UniswapV4Pool[](poolsLength); pools = new UniswapV4Pool[](poolsLength);
bytes memory poolsData = data[42:]; bytes memory poolsData = data[62:];
uint256 offset = 0; uint256 offset = 0;
for (uint256 i = 0; i < poolsLength; i++) { for (uint256 i = 0; i < poolsLength; i++) {
address intermediaryToken; address intermediaryToken;

View File

@@ -423,6 +423,7 @@ contract TychoRouterSplitSwapTest is TychoRouterTestSetup {
USDT_ADDR, USDT_ADDR,
true, true,
TokenTransfer.TransferType.TRANSFER_PERMIT2_TO_ROUTER, TokenTransfer.TransferType.TRANSFER_PERMIT2_TO_ROUTER,
ALICE,
pools pools
); );
@@ -471,7 +472,7 @@ contract TychoRouterSplitSwapTest is TychoRouterTestSetup {
}); });
bytes memory protocolData = UniswapV4Utils.encodeExactInput( bytes memory protocolData = UniswapV4Utils.encodeExactInput(
USDE_ADDR, WBTC_ADDR, true, TokenTransfer.TransferType.NONE, pools USDE_ADDR, WBTC_ADDR, true, TokenTransfer.TransferType.NONE, ALICE, pools
); );
bytes memory swap = encodeSplitSwap( bytes memory swap = encodeSplitSwap(
@@ -483,7 +484,7 @@ contract TychoRouterSplitSwapTest is TychoRouterTestSetup {
tychoRouter.exposedSplitSwap(amountIn, 2, pleEncode(swaps)); tychoRouter.exposedSplitSwap(amountIn, 2, pleEncode(swaps));
assertEq(IERC20(WBTC_ADDR).balanceOf(tychoRouterAddr), 102718); assertEq(IERC20(WBTC_ADDR).balanceOf(ALICE), 102718);
} }
function testSplitInputCyclicSwapInternalMethod() public { function testSplitInputCyclicSwapInternalMethod() public {

View File

@@ -22,6 +22,7 @@ contract UniswapV4ExecutorExposed is UniswapV4Executor {
address tokenOut, address tokenOut,
bool zeroForOne, bool zeroForOne,
TokenTransfer.TransferType transferType, TokenTransfer.TransferType transferType,
address receiver,
UniswapV4Pool[] memory pools UniswapV4Pool[] memory pools
) )
{ {
@@ -68,7 +69,7 @@ contract UniswapV4ExecutorTest is Test, Constants {
}); });
bytes memory data = UniswapV4Utils.encodeExactInput( bytes memory data = UniswapV4Utils.encodeExactInput(
USDE_ADDR, USDT_ADDR, zeroForOne, transferType, pools USDE_ADDR, USDT_ADDR, zeroForOne, transferType, ALICE, pools
); );
( (
@@ -76,6 +77,7 @@ contract UniswapV4ExecutorTest is Test, Constants {
address tokenOut, address tokenOut,
bool zeroForOneDecoded, bool zeroForOneDecoded,
TokenTransfer.TransferType transferTypeDecoded, TokenTransfer.TransferType transferTypeDecoded,
address receiver,
UniswapV4Executor.UniswapV4Pool[] memory decodedPools UniswapV4Executor.UniswapV4Pool[] memory decodedPools
) = uniswapV4Exposed.decodeData(data); ) = uniswapV4Exposed.decodeData(data);
@@ -83,6 +85,7 @@ contract UniswapV4ExecutorTest is Test, Constants {
assertEq(tokenOut, USDT_ADDR); assertEq(tokenOut, USDT_ADDR);
assertEq(zeroForOneDecoded, zeroForOne); assertEq(zeroForOneDecoded, zeroForOne);
assertEq(uint8(transferTypeDecoded), uint8(transferType)); assertEq(uint8(transferTypeDecoded), uint8(transferType));
assertEq(receiver, ALICE);
assertEq(decodedPools.length, 2); assertEq(decodedPools.length, 2);
assertEq(decodedPools[0].intermediaryToken, USDT_ADDR); assertEq(decodedPools[0].intermediaryToken, USDT_ADDR);
assertEq(decodedPools[0].fee, pool1Fee); assertEq(decodedPools[0].fee, pool1Fee);
@@ -108,7 +111,7 @@ contract UniswapV4ExecutorTest is Test, Constants {
}); });
bytes memory data = UniswapV4Utils.encodeExactInput( bytes memory data = UniswapV4Utils.encodeExactInput(
USDE_ADDR, USDT_ADDR, true, TokenTransfer.TransferType.NONE, pools USDE_ADDR, USDT_ADDR, true, TokenTransfer.TransferType.NONE, ALICE, pools
); );
uint256 amountOut = uniswapV4Exposed.swap(amountIn, data); uint256 amountOut = uniswapV4Exposed.swap(amountIn, data);
@@ -117,7 +120,7 @@ contract UniswapV4ExecutorTest is Test, Constants {
USDE.balanceOf(address(uniswapV4Exposed)), USDE.balanceOf(address(uniswapV4Exposed)),
usdeBalanceBeforeSwapExecutor - amountIn usdeBalanceBeforeSwapExecutor - amountIn
); );
assertTrue(USDT.balanceOf(address(uniswapV4Exposed)) == amountOut); assertTrue(USDT.balanceOf(ALICE) == amountOut);
} }
function testSingleSwapIntegration() public { function testSingleSwapIntegration() public {
@@ -134,10 +137,10 @@ contract UniswapV4ExecutorTest is Test, Constants {
uint256 amountOut = uniswapV4Exposed.swap(amountIn, protocolData); uint256 amountOut = uniswapV4Exposed.swap(amountIn, protocolData);
assertEq(USDE.balanceOf(poolManager), usdeBalanceBeforePool + amountIn); assertEq(USDE.balanceOf(poolManager), usdeBalanceBeforePool + amountIn);
assertEq( assertEq(
USDE.balanceOf(address(uniswapV4Exposed)), USDE.balanceOf(ALICE),
usdeBalanceBeforeSwapExecutor - amountIn usdeBalanceBeforeSwapExecutor - amountIn
); );
assertTrue(USDT.balanceOf(address(uniswapV4Exposed)) == amountOut); assertTrue(USDT.balanceOf(ALICE) == amountOut);
} }
function testMultipleSwap() public { function testMultipleSwap() public {
@@ -162,7 +165,7 @@ contract UniswapV4ExecutorTest is Test, Constants {
}); });
bytes memory data = UniswapV4Utils.encodeExactInput( bytes memory data = UniswapV4Utils.encodeExactInput(
USDE_ADDR, WBTC_ADDR, true, TokenTransfer.TransferType.NONE, pools USDE_ADDR, WBTC_ADDR, true, TokenTransfer.TransferType.NONE, ALICE, pools
); );
uint256 amountOut = uniswapV4Exposed.swap(amountIn, data); uint256 amountOut = uniswapV4Exposed.swap(amountIn, data);
@@ -172,7 +175,7 @@ contract UniswapV4ExecutorTest is Test, Constants {
usdeBalanceBeforeSwapExecutor - amountIn usdeBalanceBeforeSwapExecutor - amountIn
); );
assertTrue( assertTrue(
IERC20(WBTC_ADDR).balanceOf(address(uniswapV4Exposed)) == amountOut IERC20(WBTC_ADDR).balanceOf(ALICE) == amountOut
); );
} }
@@ -196,7 +199,7 @@ contract UniswapV4ExecutorTest is Test, Constants {
usdeBalanceBeforeSwapExecutor - amountIn usdeBalanceBeforeSwapExecutor - amountIn
); );
assertTrue( assertTrue(
IERC20(WBTC_ADDR).balanceOf(address(uniswapV4Exposed)) == amountOut IERC20(WBTC_ADDR).balanceOf(ALICE) == amountOut
); );
} }
} }

View File

@@ -9,6 +9,7 @@ library UniswapV4Utils {
address tokenOut, address tokenOut,
bool zeroForOne, bool zeroForOne,
UniswapV4Executor.TransferType transferType, UniswapV4Executor.TransferType transferType,
address receiver,
UniswapV4Executor.UniswapV4Pool[] memory pools UniswapV4Executor.UniswapV4Pool[] memory pools
) public pure returns (bytes memory) { ) public pure returns (bytes memory) {
bytes memory encodedPools; bytes memory encodedPools;
@@ -23,7 +24,7 @@ library UniswapV4Utils {
} }
return abi.encodePacked( return abi.encodePacked(
tokenIn, tokenOut, zeroForOne, transferType, encodedPools tokenIn, tokenOut, zeroForOne, transferType, receiver, encodedPools
); );
} }
} }

View File

@@ -1433,6 +1433,7 @@ mod tests {
"6982508145454ce325ddbe47a25d4ec3d2311933", // group token in "6982508145454ce325ddbe47a25d4ec3d2311933", // group token in
"00", // zero2one "00", // zero2one
"04", // transfer type (transfer to router) "04", // transfer type (transfer to router)
"cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2", // receiver
// First pool params // First pool params
"0000000000000000000000000000000000000000", // intermediary token (ETH) "0000000000000000000000000000000000000000", // intermediary token (ETH)
"000bb8", // fee "000bb8", // fee

View File

@@ -207,6 +207,7 @@ impl SwapEncoder for UniswapV4SwapEncoder {
group_token_out_address, group_token_out_address,
zero_to_one, zero_to_one,
(encoding_context.transfer_type as u8).to_be_bytes(), (encoding_context.transfer_type as u8).to_be_bytes(),
bytes_to_address(&encoding_context.receiver)?,
pool_params, pool_params,
); );
@@ -766,9 +767,8 @@ mod tests {
split: 0f64, split: 0f64,
}; };
let encoding_context = EncodingContext { let encoding_context = EncodingContext {
// The receiver address was taken from `address(uniswapV4Exposed)` in the // The receiver is ALICE to match the solidity tests
// UniswapV4Executor.t.sol receiver: Bytes::from("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2"),
receiver: Bytes::from("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f"),
exact_out: false, exact_out: false,
// Same as the executor address // Same as the executor address
router_address: Some(Bytes::from("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f")), router_address: Some(Bytes::from("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f")),
@@ -800,6 +800,8 @@ mod tests {
"01", "01",
// transfer type // transfer type
"00", "00",
// receiver
"cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2",
// pool params: // pool params:
// - intermediary token // - intermediary token
"dac17f958d2ee523a2206206994597c13d831ec7", "dac17f958d2ee523a2206206994597c13d831ec7",
@@ -878,11 +880,11 @@ mod tests {
let usdt_address = Bytes::from("0xdAC17F958D2ee523a2206206994597C13D831ec7"); let usdt_address = Bytes::from("0xdAC17F958D2ee523a2206206994597C13D831ec7");
let wbtc_address = Bytes::from("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"); let wbtc_address = Bytes::from("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599");
let router_address = Bytes::from("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f"); let router_address = Bytes::from("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f");
let receiver_address = router_address.clone();
// The context is the same for both swaps, since the group token in and out are the same // The context is the same for both swaps, since the group token in and out are the same
let context = EncodingContext { let context = EncodingContext {
receiver: receiver_address.clone(), // The receiver is ALICE to match the solidity tests
receiver: Bytes::from("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2"),
exact_out: false, exact_out: false,
router_address: Some(router_address.clone()), router_address: Some(router_address.clone()),
group_token_in: usde_address.clone(), group_token_in: usde_address.clone(),
@@ -968,6 +970,8 @@ mod tests {
"01", "01",
// transfer type // transfer type
"00", "00",
// receiver
"cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2",
// pool params: // pool params:
// - intermediary token USDT // - intermediary token USDT
"dac17f958d2ee523a2206206994597c13d831ec7", "dac17f958d2ee523a2206206994597c13d831ec7",
@@ -983,6 +987,7 @@ mod tests {
"00003c" "00003c"
)) ))
); );
println!("{}", combined_hex)
} }
mod ekubo { mod ekubo {

View File

@@ -1050,6 +1050,8 @@ mod tests {
"00", "00",
// transfer type // transfer type
"00", "00",
// receiver
"cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2",
// first pool intermediary token (ETH) // first pool intermediary token (ETH)
"0000000000000000000000000000000000000000", "0000000000000000000000000000000000000000",
// fee // fee