Skip to content
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
76 changes: 35 additions & 41 deletions src/Router.sol
Original file line number Diff line number Diff line change
Expand Up @@ -369,48 +369,42 @@ contract Router is

settlementInfo.actualAmount = settlementInfo.wantedAmount;

// Check if source and target ZRC20 are the same
if (intentInfo.zrc20 == settlementInfo.targetZRC20) {
// No swap needed, use original amounts
settlementInfo.amountWithTipOut = intentInfo.amountWithTip;
settlementInfo.tipAfterSwap = wantedTip;
} else {
// Approve swap module to spend tokens
IERC20(intentInfo.zrc20).approve(swapModule, 0);
IERC20(intentInfo.zrc20).approve(swapModule, intentInfo.amountWithTip);

// Perform swap through swap module
settlementInfo.amountWithTipOut = ISwap(swapModule).swap(
intentInfo.zrc20,
settlementInfo.targetZRC20,
intentInfo.amountWithTip,
settlementInfo.gasZRC20,
settlementInfo.gasFee,
zrc20ToTokenName[intentInfo.zrc20]
);
// Always call swap module to handle gas fees and potential swaps
// Approve swap module to spend tokens
IERC20(intentInfo.zrc20).approve(swapModule, 0);
IERC20(intentInfo.zrc20).approve(swapModule, intentInfo.amountWithTip);

// Perform swap through swap module
settlementInfo.amountWithTipOut = ISwap(swapModule).swap(
intentInfo.zrc20,
settlementInfo.targetZRC20,
intentInfo.amountWithTip,
settlementInfo.gasZRC20,
settlementInfo.gasFee,
zrc20ToTokenName[intentInfo.zrc20]
);

// Calculate slippage difference and adjust tip accordingly
// If swap returns more than expected (surplus), slippageAndFeeCost will be 0
// Note: Surplus amounts are currently ignored as they are too small to handle
// This will be addressed in future updates
uint256 slippageAndFeeCost = wantedAmountWithTip > settlementInfo.amountWithTipOut
? wantedAmountWithTip - settlementInfo.amountWithTipOut
: 0;

// Check if tip covers the slippage and fee costs
if (wantedTip > slippageAndFeeCost) {
// Tip covers all costs, subtract from tip only
settlementInfo.tipAfterSwap = wantedTip - slippageAndFeeCost;
} else {
// Tip doesn't cover costs, use it all and reduce the amount
settlementInfo.tipAfterSwap = 0;
// Calculate how much remaining slippage to cover from the amount
uint256 remainingCost = slippageAndFeeCost - wantedTip;
// Ensure the amount is greater than the remaining cost, otherwise fail
require(settlementInfo.wantedAmount > remainingCost, "Amount insufficient to cover costs after tip");
// Reduce the actual amount by the remaining cost
settlementInfo.actualAmount = settlementInfo.wantedAmount - remainingCost;
}
// Calculate slippage difference and adjust tip accordingly
// If swap returns more than expected (surplus), slippageAndFeeCost will be 0
// Note: Surplus amounts are currently ignored as they are too small to handle
// This will be addressed in future updates
uint256 slippageAndFeeCost = wantedAmountWithTip > settlementInfo.amountWithTipOut
? wantedAmountWithTip - settlementInfo.amountWithTipOut
: 0;

// Check if tip covers the slippage and fee costs
if (wantedTip > slippageAndFeeCost) {
// Tip covers all costs, subtract from tip only
settlementInfo.tipAfterSwap = wantedTip - slippageAndFeeCost;
} else {
// Tip doesn't cover costs, use it all and reduce the amount
settlementInfo.tipAfterSwap = 0;
// Calculate how much remaining slippage to cover from the amount
uint256 remainingCost = slippageAndFeeCost - wantedTip;
// Ensure the amount is greater than the remaining cost, otherwise fail
require(settlementInfo.wantedAmount > remainingCost, "Amount insufficient to cover costs after tip");
// Reduce the actual amount by the remaining cost
settlementInfo.actualAmount = settlementInfo.wantedAmount - remainingCost;
}

return settlementInfo;
Expand Down
67 changes: 37 additions & 30 deletions src/swapModules/SwapAlgebra.sol
Original file line number Diff line number Diff line change
Expand Up @@ -227,47 +227,54 @@ contract SwapAlgebra is ISwap, Ownable {

// Now perform the main swap using Algebra pool for the remaining amount
if (amountInForMainSwap > 0) {
// Check if a direct pool exists
bool directPoolExists = algebraPoolExists(tokenIn, tokenOut);

if (directPoolExists) {
// Swap directly using the Algebra pool
amountOut = swapThroughAlgebraPool(tokenIn, tokenOut, amountInForMainSwap);
// Check if input and output tokens are the same
if (tokenIn == tokenOut) {
// No swap needed, just transfer the remaining amount back to sender
amountOut = amountInForMainSwap;
IERC20(tokenOut).safeTransfer(msg.sender, amountOut);
} else {
// Get the intermediary token for this token name
address intermediaryToken = intermediaryTokens[tokenName];
require(intermediaryToken != address(0), "No intermediary token set for this token name");
// Check if a direct pool exists
bool directPoolExists = algebraPoolExists(tokenIn, tokenOut);

// Check if both pools exist
bool poolInToInterExists = algebraPoolExists(tokenIn, intermediaryToken);
bool poolInterToOutExists = algebraPoolExists(intermediaryToken, tokenOut);
if (directPoolExists) {
// Swap directly using the Algebra pool
amountOut = swapThroughAlgebraPool(tokenIn, tokenOut, amountInForMainSwap);
} else {
// Get the intermediary token for this token name
address intermediaryToken = intermediaryTokens[tokenName];
require(intermediaryToken != address(0), "No intermediary token set for this token name");

require(poolInToInterExists && poolInterToOutExists, "Required Algebra pools do not exist");
// Check if both pools exist
bool poolInToInterExists = algebraPoolExists(tokenIn, intermediaryToken);
bool poolInterToOutExists = algebraPoolExists(intermediaryToken, tokenOut);

// First hop: tokenIn -> intermediaryToken
uint256 intermediaryAmount = swapThroughAlgebraPool(tokenIn, intermediaryToken, amountInForMainSwap);
require(poolInToInterExists && poolInterToOutExists, "Required Algebra pools do not exist");

// Second hop: intermediaryToken -> tokenOut
amountOut = swapThroughAlgebraPool(intermediaryToken, tokenOut, intermediaryAmount);
// First hop: tokenIn -> intermediaryToken
uint256 intermediaryAmount = swapThroughAlgebraPool(tokenIn, intermediaryToken, amountInForMainSwap);

emit SwapWithIntermediary(tokenIn, intermediaryToken, tokenOut, amountInForMainSwap, amountOut);
}
// Second hop: intermediaryToken -> tokenOut
amountOut = swapThroughAlgebraPool(intermediaryToken, tokenOut, intermediaryAmount);

// Calculate actual balance change to account for any existing balance
uint256 finalOutBalance = IERC20(tokenOut).balanceOf(address(this));
uint256 actualAmountOut = finalOutBalance - initialOutBalance;
emit SwapWithIntermediary(tokenIn, intermediaryToken, tokenOut, amountInForMainSwap, amountOut);
}

// Calculate actual balance change to account for any existing balance
uint256 finalOutBalance = IERC20(tokenOut).balanceOf(address(this));
uint256 actualAmountOut = finalOutBalance - initialOutBalance;

// Apply slippage check using constant MAX_SLIPPAGE (1%)
uint256 minRequiredAmount = calculateMinAmountOutWithSlippage(amountOut);
// Apply slippage check using constant MAX_SLIPPAGE (1%)
uint256 minRequiredAmount = calculateMinAmountOutWithSlippage(amountOut);

// Verify we received at least the minimum amount expected
require(actualAmountOut >= minRequiredAmount, "Slippage tolerance exceeded");
// Verify we received at least the minimum amount expected
require(actualAmountOut >= minRequiredAmount, "Slippage tolerance exceeded");

// Transfer output tokens to sender
IERC20(tokenOut).safeTransfer(msg.sender, actualAmountOut);
// Transfer output tokens to sender
IERC20(tokenOut).safeTransfer(msg.sender, actualAmountOut);

// Update the return value to match what was actually received and transferred
amountOut = actualAmountOut;
// Update the return value to match what was actually received and transferred
amountOut = actualAmountOut;
}
}

return amountOut;
Expand Down
32 changes: 7 additions & 25 deletions test/Router.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -1295,7 +1295,7 @@ contract RouterTest is Test {
// No need for additional assertions as the mock would fail if the wrong gas limit was used
}

function test_OnCall_SameTokenNoSwap() public {
function test_OnCall_SameTokenWithGasFee() public {
// Setup intent contract
uint256 sourceChainId = 1;
uint256 targetChainId = 2;
Expand Down Expand Up @@ -1336,9 +1336,8 @@ contract RouterTest is Test {
inputToken.mint(address(router), amount + tip);
// Gas token goes to the swap module for gas fee
gasZRC20.mint(address(swapModule), gasFee);

// Setup spy on swap module to verify it's not called
vm.record();
// Input token also goes to swap module for the main swap (same token case)
inputToken.mint(address(swapModule), amount + tip - gasFee);

// Setup context
IGateway.ZetaChainMessageContext memory context = IGateway.ZetaChainMessageContext({
Expand All @@ -1355,30 +1354,13 @@ contract RouterTest is Test {
targetChainId,
address(inputToken),
amount + tip,
tip // Tip should remain unchanged since no swap/slippage
tip - gasFee // Tip should be reduced by gas fee since gas fee is deducted from tip
);

// Call onCall
vm.prank(address(gateway));
router.onCall(context, address(inputToken), amount + tip, intentPayloadBytes);

// Verify the swap function was not called
(bytes32[] memory reads, bytes32[] memory writes) = vm.accesses(address(swapModule));

// Verify no swap was performed - the swap function should not be called
bool swapCalled = false;
bytes4 swapSelector = bytes4(keccak256("swap(address,address,uint256,address,uint256,string)"));

for (uint256 i = 0; i < reads.length; i++) {
// Check if any read access matches the swap selector storage slot
if (uint256(reads[i]) < 4 && bytes4(uint32(uint256(reads[i]))) == swapSelector) {
swapCalled = true;
break;
}
}

assertFalse(swapCalled, "Swap function should not be called when source and target ZRC20s are the same");

// Verify approvals were made to the gateway
assertTrue(
inputToken.allowance(address(router), address(gateway)) > 0, "Router should approve input token to gateway"
Expand All @@ -1387,9 +1369,9 @@ contract RouterTest is Test {
gasZRC20.allowance(address(router), address(gateway)) > 0, "Router should approve gas ZRC20 to gateway"
);

// Verify actual amount equals wanted amount (no reduction)
// This is implicit in the event emission check above but would be
// even better with an explicit check of the settlement payload
// Verify that the swap function was called (for gas fee handling)
// The swap module should have received the input tokens and handled gas fees
// even though the main swap was skipped due to same token
}

function test_OnCall_SwapSurplusHandling() public {
Expand Down
94 changes: 61 additions & 33 deletions test/mocks/MockSwapModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,25 +29,39 @@ contract MockSwapModule is ISwap {
// Transfer tokens from sender to this contract
IERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn);

// Calculate amount out based on slippage settings
uint256 slippageCost = (amountIn * slippage) / 10000; // slippage in basis points (e.g., 100 = 1%)

// Only account for gas fee if gasZRC20 is provided
uint256 totalCost = slippageCost;
if (gasZRC20 != address(0)) {
totalCost += gasFee;
// Handle gas fee first (similar to SwapAlgebra)
uint256 amountInForMainSwap = amountIn;

if (gasFee > 0 && gasZRC20 != address(0)) {
// If tokenIn is not the gas token, we need to account for gas fee
if (tokenIn != gasZRC20) {
// In mock, we just reduce the main swap amount by gas fee
amountInForMainSwap = amountIn - gasFee;
} else {
// If tokenIn is already the gas token, just set aside the needed amount
amountInForMainSwap = amountIn - gasFee;
}

// Transfer gas fee tokens back to the sender
IERC20(gasZRC20).transfer(msg.sender, gasFee);
}

require(amountIn > totalCost, "Amount insufficient to cover costs after tip");

amountOut = amountIn - totalCost;

// Transfer tokens back to the sender
IERC20(tokenOut).transfer(msg.sender, amountOut);

// Only transfer gas fee if gasZRC20 is not zero address
if (gasZRC20 != address(0) && gasFee > 0) {
IERC20(gasZRC20).transfer(msg.sender, gasFee);
// Handle main swap
if (amountInForMainSwap > 0) {
// Check if input and output tokens are the same
if (tokenIn == tokenOut) {
// No swap needed, just return the remaining amount
amountOut = amountInForMainSwap;
} else {
// Calculate amount out based on slippage settings
uint256 slippageCost = (amountInForMainSwap * slippage) / 10000; // slippage in basis points (e.g., 100 = 1%)
amountOut = amountInForMainSwap - slippageCost;
}

// Transfer tokens back to the sender
IERC20(tokenOut).transfer(msg.sender, amountOut);
} else {
amountOut = 0;
}

// Allow to set a custom amount out for testing
Expand All @@ -68,27 +82,41 @@ contract MockSwapModule is ISwap {
// Transfer tokens from sender to this contract
IERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn);

// Calculate amount out based on slippage settings
uint256 slippageCost = (amountIn * slippage) / 10000; // slippage in basis points (e.g., 100 = 1%)
// Handle gas fee first (similar to SwapAlgebra)
uint256 amountInForMainSwap = amountIn;

// Only account for gas fee if gasZRC20 is provided
uint256 totalCost = slippageCost;
if (gasZRC20 != address(0)) {
totalCost += gasFee;
}

require(amountIn > totalCost, "Amount insufficient to cover costs after tip");

amountOut = amountIn - totalCost;
if (gasFee > 0 && gasZRC20 != address(0)) {
// If tokenIn is not the gas token, we need to account for gas fee
if (tokenIn != gasZRC20) {
// In mock, we just reduce the main swap amount by gas fee
amountInForMainSwap = amountIn - gasFee;
} else {
// If tokenIn is already the gas token, just set aside the needed amount
amountInForMainSwap = amountIn - gasFee;
}

// Transfer tokens back to the sender
IERC20(tokenOut).transfer(msg.sender, amountOut);

// Only transfer gas fee if gasZRC20 is not zero address
if (gasZRC20 != address(0) && gasFee > 0) {
// Transfer gas fee tokens back to the sender
IERC20(gasZRC20).transfer(msg.sender, gasFee);
}

// Handle main swap
if (amountInForMainSwap > 0) {
// Check if input and output tokens are the same
if (tokenIn == tokenOut) {
// No swap needed, just return the remaining amount
amountOut = amountInForMainSwap;
} else {
// Calculate amount out based on slippage settings
uint256 slippageCost = (amountInForMainSwap * slippage) / 10000; // slippage in basis points (e.g., 100 = 1%)
amountOut = amountInForMainSwap - slippageCost;
}

// Transfer tokens back to the sender
IERC20(tokenOut).transfer(msg.sender, amountOut);
} else {
amountOut = 0;
}

// Allow to set a custom amount out for testing
if (customAmountOut > 0) {
amountOut = customAmountOut;
Expand Down
Loading
Loading