misc: fix _swapChecked for cyclic swap, update encoder swap path validation, add cyclic sequesntial swap integration test
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"ethereum": {
|
"ethereum": {
|
||||||
"uniswap_v2": "0xf6c5be66FFf9DC69962d73da0A617a827c382329",
|
"uniswap_v2": "0xf6c5be66FFf9DC69962d73da0A617a827c382329",
|
||||||
"uniswap_v3": "0xdD8559c917393FC8DD2b4dD289c52Ff445fDE1B0",
|
"uniswap_v3": "0x2e234DAe75C793f67A35089C9d99245E1C58470b",
|
||||||
"uniswap_v4": "0x042C0ebBEAb9d9987c2f64Ee05f2B3aeB86eAf70",
|
"uniswap_v4": "0x042C0ebBEAb9d9987c2f64Ee05f2B3aeB86eAf70",
|
||||||
"vm:balancer_v2": "0x00BE8EfAE40219Ff76287b0F9b9e497942f5BC91"
|
"vm:balancer_v2": "0x00BE8EfAE40219Ff76287b0F9b9e497942f5BC91"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import "@permit2/src/interfaces/IAllowanceTransfer.sol";
|
|||||||
import "./Dispatcher.sol";
|
import "./Dispatcher.sol";
|
||||||
import {LibSwap} from "../lib/LibSwap.sol";
|
import {LibSwap} from "../lib/LibSwap.sol";
|
||||||
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
|
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
|
||||||
|
import "forge-std/console.sol";
|
||||||
|
|
||||||
// ✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷
|
// ✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷
|
||||||
// ✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷
|
// ✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷
|
||||||
@@ -259,7 +260,6 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
|
|||||||
: IERC20(tokenIn).balanceOf(address(this));
|
: IERC20(tokenIn).balanceOf(address(this));
|
||||||
|
|
||||||
amountOut = _swap(amountIn, nTokens, swaps);
|
amountOut = _swap(amountIn, nTokens, swaps);
|
||||||
|
|
||||||
uint256 currentBalance = tokenIn == address(0)
|
uint256 currentBalance = tokenIn == address(0)
|
||||||
? address(this).balance
|
? address(this).balance
|
||||||
: IERC20(tokenIn).balanceOf(address(this));
|
: IERC20(tokenIn).balanceOf(address(this));
|
||||||
@@ -323,6 +323,10 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
|
|||||||
revert TychoRouter__EmptySwaps();
|
revert TychoRouter__EmptySwaps();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("amountIn", amountIn);
|
||||||
|
console.log("nTokens", nTokens);
|
||||||
|
console.logBytes(swaps_);
|
||||||
|
|
||||||
uint256 currentAmountIn;
|
uint256 currentAmountIn;
|
||||||
uint256 currentAmountOut;
|
uint256 currentAmountOut;
|
||||||
uint8 tokenInIndex = 0;
|
uint8 tokenInIndex = 0;
|
||||||
|
|||||||
@@ -1192,7 +1192,7 @@ contract TychoRouterTest is TychoRouterTestSetup {
|
|||||||
assertEq(IERC20(WBTC_ADDR).balanceOf(tychoRouterAddr), 102718);
|
assertEq(IERC20(WBTC_ADDR).balanceOf(tychoRouterAddr), 102718);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testCyclicSequentialSwap() public {
|
function testCyclicSequentialSwap1() public {
|
||||||
// This test has start and end tokens that are the same
|
// This test has start and end tokens that are the same
|
||||||
// The flow is:
|
// The flow is:
|
||||||
// USDC -> WETH -> USDC using two pools
|
// USDC -> WETH -> USDC using two pools
|
||||||
@@ -1332,6 +1332,23 @@ contract TychoRouterTest is TychoRouterTestSetup {
|
|||||||
assertEq(IERC20(USDC_ADDR).balanceOf(tychoRouterAddr), 99525908);
|
assertEq(IERC20(USDC_ADDR).balanceOf(tychoRouterAddr), 99525908);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function testCyclicSequentialSwapIntegration() public {
|
||||||
|
deal(USDC_ADDR, ALICE, 100 * 10 ** 6);
|
||||||
|
|
||||||
|
// Approve permit2
|
||||||
|
vm.startPrank(ALICE);
|
||||||
|
IERC20(USDC_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max);
|
||||||
|
// Encoded solution generated using `test_cyclic_sequential_swap`
|
||||||
|
// but manually replacing the executor address
|
||||||
|
// `dD8559c917393FC8DD2b4dD289c52Ff445fDE1B0` with the one in this test
|
||||||
|
// `2e234DAe75C793f67A35089C9d99245E1C58470b`
|
||||||
|
(bool success,) = tychoRouterAddr.call(
|
||||||
|
hex"d499aa880000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f4308e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000067f67a8b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067cef493000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041c07077fc73bb0f5129006061288fa0583c101631307377281d6b8f3feb50aa2d564f9948c92e0e4abc3771d592bd2f22ebb18ccf21b270459b05f272251ce1c71b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de006d00010000002e234dae75c793f67a35089c9d99245e1c58470ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f43ede3eca2a72b3aecc820e955b36f38437d0139588e6a0c2ddd26feeb64f039a2c41296fcb3f564001006d01000000002e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000bb83ede3eca2a72b3aecc820e955b36f38437d013958ad599c3a0ff1de082011efddc58f1908eb6e6d8000000"
|
||||||
|
);
|
||||||
|
|
||||||
|
vm.stopPrank();
|
||||||
|
}
|
||||||
|
|
||||||
// Base Network Tests
|
// Base Network Tests
|
||||||
// Make sure to set the RPC_URL to base network
|
// Make sure to set the RPC_URL to base network
|
||||||
function testSwapSingleBase() public {
|
function testSwapSingleBase() public {
|
||||||
@@ -1354,3 +1371,11 @@ contract TychoRouterTest is TychoRouterTestSetup {
|
|||||||
assertGt(IERC20(BASE_MAG7).balanceOf(tychoRouterAddr), 1379830606);
|
assertGt(IERC20(BASE_MAG7).balanceOf(tychoRouterAddr), 1379830606);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//amountIn 100000000
|
||||||
|
//nTokens 2
|
||||||
|
//0x006d00010000002e234dae75c793f67a35089c9d99245e1c58470ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f43ede3eca2a72b3aecc820e955b36f38437d0139588e6a0c2ddd26feeb64f039a2c41296fcb3f564001006d01000000002e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000bb83ede3eca2a72b3aecc820e955b36f38437d013958ad599c3a0ff1de082011efddc58f1908eb6e6d800
|
||||||
|
|
||||||
|
//amountIn 100000000
|
||||||
|
//nTokens 2
|
||||||
|
//0x006d00010000002e234dae75c793f67a35089c9d99245e1c58470ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f43ede3eca2a72b3aecc820e955b36f38437d0139588e6a0c2ddd26feeb64f039a2c41296fcb3f564001006d01000000002e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000bb83ede3eca2a72b3aecc820e955b36f38437d013958ad599c3a0ff1de082011efddc58f1908eb6e6d800
|
||||||
|
|||||||
@@ -206,6 +206,11 @@ impl StrategyEncoder for SplitSwapStrategyEncoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let encoded_swaps = self.ple_encode(swaps);
|
let encoded_swaps = self.ple_encode(swaps);
|
||||||
|
let tokens_len = if &solution.given_token == &solution.checked_token {
|
||||||
|
tokens.len() - 1
|
||||||
|
} else {
|
||||||
|
tokens.len()
|
||||||
|
};
|
||||||
let method_calldata = if let Some(permit2) = self.permit2.clone() {
|
let method_calldata = if let Some(permit2) = self.permit2.clone() {
|
||||||
let (permit, signature) = permit2.get_permit(
|
let (permit, signature) = permit2.get_permit(
|
||||||
&solution.router_address,
|
&solution.router_address,
|
||||||
@@ -220,7 +225,7 @@ impl StrategyEncoder for SplitSwapStrategyEncoder {
|
|||||||
biguint_to_u256(&min_amount_out),
|
biguint_to_u256(&min_amount_out),
|
||||||
wrap,
|
wrap,
|
||||||
unwrap,
|
unwrap,
|
||||||
U256::from(tokens.len()),
|
U256::from(tokens_len),
|
||||||
bytes_to_address(&solution.receiver)?,
|
bytes_to_address(&solution.receiver)?,
|
||||||
permit,
|
permit,
|
||||||
signature.as_bytes().to_vec(),
|
signature.as_bytes().to_vec(),
|
||||||
@@ -235,7 +240,7 @@ impl StrategyEncoder for SplitSwapStrategyEncoder {
|
|||||||
biguint_to_u256(&min_amount_out),
|
biguint_to_u256(&min_amount_out),
|
||||||
wrap,
|
wrap,
|
||||||
unwrap,
|
unwrap,
|
||||||
U256::from(tokens.len()),
|
U256::from(tokens_len),
|
||||||
bytes_to_address(&solution.receiver)?,
|
bytes_to_address(&solution.receiver)?,
|
||||||
encoded_swaps,
|
encoded_swaps,
|
||||||
)
|
)
|
||||||
@@ -1212,4 +1217,91 @@ mod tests {
|
|||||||
let hex_calldata = encode(&calldata);
|
let hex_calldata = encode(&calldata);
|
||||||
println!("{}", hex_calldata);
|
println!("{}", hex_calldata);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cyclic_sequential_swap() {
|
||||||
|
// This test has start and end tokens that are the same
|
||||||
|
// The flow is:
|
||||||
|
// USDC -> WETH -> USDC using two pools
|
||||||
|
|
||||||
|
// Set up a mock private key for signing (Alice's pk in our router tests)
|
||||||
|
let private_key =
|
||||||
|
"0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string();
|
||||||
|
|
||||||
|
let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap();
|
||||||
|
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
|
||||||
|
|
||||||
|
// Create two Uniswap V3 pools for the cyclic swap
|
||||||
|
// USDC -> WETH (Pool 1)
|
||||||
|
let swap_usdc_weth = Swap {
|
||||||
|
component: ProtocolComponent {
|
||||||
|
id: "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640".to_string(), /* USDC-WETH USV3
|
||||||
|
* Pool 1 */
|
||||||
|
protocol_system: "uniswap_v3".to_string(),
|
||||||
|
static_attributes: {
|
||||||
|
let mut attrs = HashMap::new();
|
||||||
|
attrs.insert(
|
||||||
|
"fee".to_string(),
|
||||||
|
Bytes::from(BigInt::from(500).to_signed_bytes_be()),
|
||||||
|
);
|
||||||
|
attrs
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
token_in: usdc.clone(),
|
||||||
|
token_out: weth.clone(),
|
||||||
|
split: 0f64,
|
||||||
|
};
|
||||||
|
|
||||||
|
// WETH -> USDC (Pool 2)
|
||||||
|
let swap_weth_usdc = Swap {
|
||||||
|
component: ProtocolComponent {
|
||||||
|
id: "0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8".to_string(), /* USDC-WETH USV3
|
||||||
|
* Pool 2 */
|
||||||
|
protocol_system: "uniswap_v3".to_string(),
|
||||||
|
static_attributes: {
|
||||||
|
let mut attrs = HashMap::new();
|
||||||
|
attrs.insert(
|
||||||
|
"fee".to_string(),
|
||||||
|
Bytes::from(BigInt::from(3000).to_signed_bytes_be()),
|
||||||
|
);
|
||||||
|
attrs
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
token_in: weth.clone(),
|
||||||
|
token_out: usdc.clone(),
|
||||||
|
split: 0f64,
|
||||||
|
};
|
||||||
|
|
||||||
|
let swap_encoder_registry = get_swap_encoder_registry();
|
||||||
|
let encoder =
|
||||||
|
SplitSwapStrategyEncoder::new(eth_chain(), swap_encoder_registry, Some(private_key))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let solution = Solution {
|
||||||
|
exact_out: false,
|
||||||
|
given_token: usdc.clone(),
|
||||||
|
given_amount: BigUint::from_str("100000000").unwrap(), // 100 USDC (6 decimals)
|
||||||
|
checked_token: usdc.clone(),
|
||||||
|
expected_amount: None,
|
||||||
|
checked_amount: Some(BigUint::from_str("99889294").unwrap()), /* Expected output from
|
||||||
|
* test */
|
||||||
|
slippage: None,
|
||||||
|
swaps: vec![swap_usdc_weth, swap_weth_usdc],
|
||||||
|
router_address: Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(),
|
||||||
|
sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(),
|
||||||
|
receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let (calldata, _) = encoder
|
||||||
|
.encode_strategy(solution)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
println!("{}", hex::encode(&calldata));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,6 +150,13 @@ impl SplitSwapValidator {
|
|||||||
.insert(&swap.token_out);
|
.insert(&swap.token_out);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Collect all unique tokens from the swaps
|
||||||
|
let mut all_tokens = HashSet::new();
|
||||||
|
for swap in swaps {
|
||||||
|
all_tokens.insert(&swap.token_in);
|
||||||
|
all_tokens.insert(&swap.token_out);
|
||||||
|
}
|
||||||
|
|
||||||
// BFS from validation_given
|
// BFS from validation_given
|
||||||
let mut visited = HashSet::new();
|
let mut visited = HashSet::new();
|
||||||
let mut queue = VecDeque::new();
|
let mut queue = VecDeque::new();
|
||||||
@@ -160,8 +167,8 @@ impl SplitSwapValidator {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Early success check
|
// Early success check - if we've reached the checked token and visited all tokens
|
||||||
if token == checked_token && visited.len() == graph.len() + 1 {
|
if token == checked_token && visited.len() == all_tokens.len() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,6 +181,13 @@ impl SplitSwapValidator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// After BFS completes, check if both conditions are met:
|
||||||
|
// 1. The checked token is in the visited set
|
||||||
|
// 2. All unique tokens from the swaps are visited
|
||||||
|
if visited.contains(checked_token) && visited.len() == all_tokens.len() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
// If we get here, either checked_token wasn't reached or not all tokens were visited
|
// If we get here, either checked_token wasn't reached or not all tokens were visited
|
||||||
if !visited.contains(checked_token) {
|
if !visited.contains(checked_token) {
|
||||||
Err(EncodingError::InvalidInput(
|
Err(EncodingError::InvalidInput(
|
||||||
@@ -291,6 +305,41 @@ mod tests {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_validate_path_cyclic_swap() {
|
||||||
|
let validator = SplitSwapValidator;
|
||||||
|
let eth = Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap();
|
||||||
|
let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap();
|
||||||
|
let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
|
||||||
|
|
||||||
|
let cyclic_swaps = vec![
|
||||||
|
Swap {
|
||||||
|
component: ProtocolComponent {
|
||||||
|
id: "pool1".to_string(),
|
||||||
|
protocol_system: "uniswap_v2".to_string(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
token_in: usdc.clone(),
|
||||||
|
token_out: weth.clone(),
|
||||||
|
split: 0.5,
|
||||||
|
},
|
||||||
|
Swap {
|
||||||
|
component: ProtocolComponent {
|
||||||
|
id: "pool2".to_string(),
|
||||||
|
protocol_system: "uniswap_v2".to_string(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
token_in: weth.clone(),
|
||||||
|
token_out: usdc.clone(),
|
||||||
|
split: 0.5,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Test with USDC as both given token and checked token
|
||||||
|
let result = validator.validate_swap_path(&cyclic_swaps, &usdc, &usdc, &None, ð, &weth);
|
||||||
|
assert_eq!(result, Ok(()));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_validate_path_unreachable_checked_token() {
|
fn test_validate_path_unreachable_checked_token() {
|
||||||
let validator = SplitSwapValidator;
|
let validator = SplitSwapValidator;
|
||||||
|
|||||||
Reference in New Issue
Block a user