Skip to content

Commit 9da7ddc

Browse files
authored
fix: implement audit reviews (#36)
* add more checks in calculateExpectedAmount * use approval reset * add zero address check * fmt * add gas limit range * add slippage protection * comment
1 parent 5a8ade7 commit 9da7ddc

File tree

7 files changed

+349
-15
lines changed

7 files changed

+349
-15
lines changed

src/Intent.sol

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,10 @@ contract Intent is
158158
__Pausable_init_unchained();
159159
__ReentrancyGuard_init_unchained();
160160

161+
// Validate addresses
162+
require(_gateway != address(0), "Gateway cannot be zero address");
163+
require(_router != address(0), "Router cannot be zero address");
164+
161165
// Set up admin role
162166
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
163167
_grantRole(PAUSER_ROLE, msg.sender);

src/Router.sol

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ contract Router is
6060
// Default gas limit for withdraw operations
6161
uint256 private constant DEFAULT_WITHDRAW_GAS_LIMIT = 400000;
6262

63+
// Min and max gas limits for withdraw operations
64+
uint256 private constant MIN_WITHDRAW_GAS_LIMIT = 100000;
65+
uint256 private constant MAX_WITHDRAW_GAS_LIMIT = 10000000;
66+
6367
// Current gas limit for withdraw operations (can be modified by admin)
6468
uint256 public withdrawGasLimit;
6569

@@ -198,6 +202,10 @@ contract Router is
198202
pure
199203
returns (uint256)
200204
{
205+
// Input validation
206+
require(decimalsIn <= 30, "Source decimals too high");
207+
require(decimalsOut <= 30, "Destination decimals too high");
208+
201209
// If decimals are the same, no conversion needed
202210
if (decimalsIn == decimalsOut) {
203211
return amountIn;
@@ -206,6 +214,10 @@ contract Router is
206214
// If destination has more decimals, multiply
207215
if (decimalsOut > decimalsIn) {
208216
uint256 scalingFactor = 10 ** (decimalsOut - decimalsIn);
217+
218+
// Check for potential overflow before multiplication
219+
require(amountIn == 0 || (type(uint256).max / amountIn) >= scalingFactor, "Decimal conversion overflow");
220+
209221
return amountIn * scalingFactor;
210222
}
211223

@@ -364,6 +376,7 @@ contract Router is
364376
settlementInfo.tipAfterSwap = wantedTip;
365377
} else {
366378
// Approve swap module to spend tokens
379+
IERC20(intentInfo.zrc20).approve(swapModule, 0);
367380
IERC20(intentInfo.zrc20).approve(swapModule, intentInfo.amountWithTip);
368381

369382
// Perform swap through swap module
@@ -415,6 +428,7 @@ contract Router is
415428
bytes memory settlementPayload
416429
) internal {
417430
// Transfer tokens to the target Intent contract
431+
IERC20(zrc20).approve(intentContract, 0);
418432
IERC20(zrc20).approve(intentContract, amount);
419433

420434
// Create a MessageContext
@@ -458,7 +472,9 @@ contract Router is
458472
});
459473

460474
// Approve gateway to spend tokens
475+
IERC20(targetZRC20).approve(gateway, 0);
461476
IERC20(targetZRC20).approve(gateway, amount);
477+
IERC20(gasZRC20).approve(gateway, 0);
462478
IERC20(gasZRC20).approve(gateway, gasFee);
463479

464480
// Call gateway to withdraw and call intent contract
@@ -675,7 +691,8 @@ contract Router is
675691
* @param newGasLimit The new gas limit to set
676692
*/
677693
function setWithdrawGasLimit(uint256 newGasLimit) public onlyRole(DEFAULT_ADMIN_ROLE) {
678-
require(newGasLimit > 0, "Gas limit cannot be zero");
694+
require(newGasLimit >= MIN_WITHDRAW_GAS_LIMIT, "Gas limit below minimum");
695+
require(newGasLimit <= MAX_WITHDRAW_GAS_LIMIT, "Gas limit above maximum");
679696
emit WithdrawGasLimitUpdated(withdrawGasLimit, newGasLimit);
680697
withdrawGasLimit = newGasLimit;
681698
}
@@ -686,7 +703,8 @@ contract Router is
686703
* @param gasLimit The gas limit to set
687704
*/
688705
function setChainWithdrawGasLimit(uint256 chainId, uint256 gasLimit) public onlyRole(DEFAULT_ADMIN_ROLE) {
689-
require(gasLimit > 0, "Gas limit cannot be zero");
706+
require(gasLimit >= MIN_WITHDRAW_GAS_LIMIT, "Gas limit below minimum");
707+
require(gasLimit <= MAX_WITHDRAW_GAS_LIMIT, "Gas limit above maximum");
690708
chainWithdrawGasLimits[chainId] = gasLimit;
691709
emit ChainWithdrawGasLimitSet(chainId, gasLimit);
692710
}

src/swapModules/SwapAlgebra.sol

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ contract SwapAlgebra is ISwap, Ownable {
2727
uint160 internal constant MIN_SQRT_RATIO = 4295128739;
2828
uint160 internal constant MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970342;
2929

30+
// Maximum allowed slippage (1%)
31+
uint256 private constant MAX_SLIPPAGE = 100;
32+
uint256 private constant BASIS_POINTS_DENOMINATOR = 10000;
33+
3034
// Algebra Factory address
3135
IAlgebraFactory public immutable algebraFactory;
3236

@@ -111,6 +115,15 @@ contract SwapAlgebra is ISwap, Ownable {
111115
return amounts[0];
112116
}
113117

118+
/**
119+
* @dev Calculates the minimum amount out based on the given slippage tolerance
120+
* @param amountOut The expected amount out
121+
* @return The minimum amount out that satisfies the MAX_SLIPPAGE tolerance
122+
*/
123+
function calculateMinAmountOutWithSlippage(uint256 amountOut) public pure returns (uint256) {
124+
return (amountOut * (BASIS_POINTS_DENOMINATOR - MAX_SLIPPAGE)) / BASIS_POINTS_DENOMINATOR;
125+
}
126+
114127
/// @notice Returns a valid `limitSqrtPrice` just beyond the current price
115128
/// @param zeroForOne If true, token0 → token1; else token1 → token0
116129
function getValidLimitSqrtPrice(bool zeroForOne) internal pure returns (uint160) {
@@ -153,6 +166,12 @@ contract SwapAlgebra is ISwap, Ownable {
153166
uint256 gasFee,
154167
string memory tokenName
155168
) public returns (uint256 amountOut) {
169+
// Record initial balances for slippage calculation
170+
uint256 initialOutBalance = 0;
171+
if (tokenOut != address(0)) {
172+
initialOutBalance = IERC20(tokenOut).balanceOf(address(this));
173+
}
174+
156175
// Transfer tokens from sender to this contract
157176
IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);
158177

@@ -234,8 +253,21 @@ contract SwapAlgebra is ISwap, Ownable {
234253
emit SwapWithIntermediary(tokenIn, intermediaryToken, tokenOut, amountInForMainSwap, amountOut);
235254
}
236255

256+
// Calculate actual balance change to account for any existing balance
257+
uint256 finalOutBalance = IERC20(tokenOut).balanceOf(address(this));
258+
uint256 actualAmountOut = finalOutBalance - initialOutBalance;
259+
260+
// Apply slippage check using constant MAX_SLIPPAGE (1%)
261+
uint256 minRequiredAmount = calculateMinAmountOutWithSlippage(amountOut);
262+
263+
// Verify we received at least the minimum amount expected
264+
require(actualAmountOut >= minRequiredAmount, "Slippage tolerance exceeded");
265+
237266
// Transfer output tokens to sender
238-
IERC20(tokenOut).safeTransfer(msg.sender, amountOut);
267+
IERC20(tokenOut).safeTransfer(msg.sender, actualAmountOut);
268+
269+
// Update the return value to match what was actually received and transferred
270+
amountOut = actualAmountOut;
239271
}
240272

241273
return amountOut;
@@ -260,7 +292,6 @@ contract SwapAlgebra is ISwap, Ownable {
260292

261293
IERC20(tokenIn).approve(pool, amountIn);
262294

263-
// (uint160 currentPrice, , , , , , ) = IAlgebraPool(pool).globalState();
264295
uint160 limitSqrtPrice = getValidLimitSqrtPrice(zeroToOne);
265296

266297
// Perform the swap

test/Intent.t.sol

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,26 @@ contract IntentTest is Test {
132132
assertEq(intent.router(), router);
133133
}
134134

135+
function test_InitializationWithZeroAddresses() public {
136+
// Deploy a new implementation
137+
Intent newImplementation = new Intent(false);
138+
139+
// Prepare initialization data with zero gateway
140+
bytes memory initDataZeroGateway = abi.encodeWithSelector(Intent.initialize.selector, address(0), router);
141+
142+
// Deploy proxy with zero gateway
143+
vm.expectRevert("Gateway cannot be zero address");
144+
new ERC1967Proxy(address(newImplementation), initDataZeroGateway);
145+
146+
// Prepare initialization data with zero router
147+
bytes memory initDataZeroRouter =
148+
abi.encodeWithSelector(Intent.initialize.selector, address(gateway), address(0));
149+
150+
// Deploy proxy with zero router
151+
vm.expectRevert("Router cannot be zero address");
152+
new ERC1967Proxy(address(newImplementation), initDataZeroRouter);
153+
}
154+
135155
function test_ComputeIntentId() public {
136156
owner = owner;
137157

test/Router.t.sol

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -508,10 +508,44 @@ contract RouterTest is Test {
508508

509509
function test_SetWithdrawGasLimit_ZeroValueReverts() public {
510510
uint256 zeroGasLimit = 0;
511-
vm.expectRevert("Gas limit cannot be zero");
511+
vm.expectRevert("Gas limit below minimum");
512512
router.setWithdrawGasLimit(zeroGasLimit);
513513
}
514514

515+
function test_SetWithdrawGasLimit_BelowMinimumReverts() public {
516+
uint256 tooLowGasLimit = 99999; // Just below minimum of 100000
517+
518+
vm.expectRevert("Gas limit below minimum");
519+
router.setWithdrawGasLimit(tooLowGasLimit);
520+
}
521+
522+
function test_SetWithdrawGasLimit_AboveMaximumReverts() public {
523+
uint256 tooHighGasLimit = 10000001; // Just above maximum of 10000000
524+
525+
vm.expectRevert("Gas limit above maximum");
526+
router.setWithdrawGasLimit(tooHighGasLimit);
527+
}
528+
529+
function test_SetWithdrawGasLimit_AtMinimum() public {
530+
uint256 minimumGasLimit = 100000; // Exactly at minimum
531+
532+
// Set at minimum should work
533+
router.setWithdrawGasLimit(minimumGasLimit);
534+
535+
// Verify the gas limit was set
536+
assertEq(router.withdrawGasLimit(), minimumGasLimit);
537+
}
538+
539+
function test_SetWithdrawGasLimit_AtMaximum() public {
540+
uint256 maximumGasLimit = 10000000; // Exactly at maximum
541+
542+
// Set at maximum should work
543+
router.setWithdrawGasLimit(maximumGasLimit);
544+
545+
// Verify the gas limit was set
546+
assertEq(router.withdrawGasLimit(), maximumGasLimit);
547+
}
548+
515549
function test_SetWithdrawGasLimit_NonAdminReverts() public {
516550
uint256 newGasLimit = 200000;
517551
vm.prank(user1);
@@ -1094,10 +1128,50 @@ contract RouterTest is Test {
10941128
uint256 zeroGasLimit = 0;
10951129

10961130
// Attempt to set a zero gas limit
1097-
vm.expectRevert("Gas limit cannot be zero");
1131+
vm.expectRevert("Gas limit below minimum");
10981132
router.setChainWithdrawGasLimit(chainId, zeroGasLimit);
10991133
}
11001134

1135+
function test_SetChainWithdrawGasLimit_BelowMinimumReverts() public {
1136+
uint256 chainId = 42161; // Arbitrum chain ID
1137+
uint256 tooLowGasLimit = 99999; // Just below minimum of 100000
1138+
1139+
// Attempt to set a gas limit below the minimum
1140+
vm.expectRevert("Gas limit below minimum");
1141+
router.setChainWithdrawGasLimit(chainId, tooLowGasLimit);
1142+
}
1143+
1144+
function test_SetChainWithdrawGasLimit_AboveMaximumReverts() public {
1145+
uint256 chainId = 42161; // Arbitrum chain ID
1146+
uint256 tooHighGasLimit = 10000001; // Just above maximum of 10000000
1147+
1148+
// Attempt to set a gas limit above the maximum
1149+
vm.expectRevert("Gas limit above maximum");
1150+
router.setChainWithdrawGasLimit(chainId, tooHighGasLimit);
1151+
}
1152+
1153+
function test_SetChainWithdrawGasLimit_AtMinimum() public {
1154+
uint256 chainId = 42161; // Arbitrum chain ID
1155+
uint256 minimumGasLimit = 100000; // Exactly at minimum
1156+
1157+
// Set at minimum should work
1158+
router.setChainWithdrawGasLimit(chainId, minimumGasLimit);
1159+
1160+
// Verify the custom gas limit was set
1161+
assertEq(router.chainWithdrawGasLimits(chainId), minimumGasLimit);
1162+
}
1163+
1164+
function test_SetChainWithdrawGasLimit_AtMaximum() public {
1165+
uint256 chainId = 42161; // Arbitrum chain ID
1166+
uint256 maximumGasLimit = 10000000; // Exactly at maximum
1167+
1168+
// Set at maximum should work
1169+
router.setChainWithdrawGasLimit(chainId, maximumGasLimit);
1170+
1171+
// Verify the custom gas limit was set
1172+
assertEq(router.chainWithdrawGasLimits(chainId), maximumGasLimit);
1173+
}
1174+
11011175
function test_SetChainWithdrawGasLimit_NonAdminReverts() public {
11021176
uint256 chainId = 42161; // Arbitrum chain ID
11031177
uint256 customGasLimit = 500000;

test/RouterDecimalConversion.t.sol

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ contract MockRouterForTests {
1515
pure
1616
returns (uint256)
1717
{
18+
// Input validation
19+
require(decimalsIn <= 30, "Source decimals too high");
20+
require(decimalsOut <= 30, "Destination decimals too high");
21+
1822
// If decimals are the same, no conversion needed
1923
if (decimalsIn == decimalsOut) {
2024
return amountIn;
@@ -23,12 +27,18 @@ contract MockRouterForTests {
2327
// If destination has more decimals, multiply
2428
if (decimalsOut > decimalsIn) {
2529
uint256 factor = 10 ** (decimalsOut - decimalsIn);
30+
31+
// Check for potential overflow before multiplication
32+
require(amountIn == 0 || (type(uint256).max / amountIn) >= factor, "Decimal conversion overflow");
33+
2634
return amountIn * factor;
2735
}
2836

2937
// If destination has fewer decimals, divide
3038
uint256 divider = 10 ** (decimalsIn - decimalsOut);
3139

40+
// No need to check for division by zero since divider is always > 0
41+
3242
// Round down by default (matches typical token behavior)
3343
return amountIn / divider;
3444
}
@@ -201,7 +211,63 @@ contract RouterDecimalConversionTest is Test {
201211
uint8 decimalsOut = 18;
202212

203213
// This should revert due to overflow when multiplying by 10^18
204-
vm.expectRevert();
214+
vm.expectRevert("Decimal conversion overflow");
205215
callCalculateExpectedAmount(largeAmount, decimalsIn, decimalsOut);
206216
}
217+
218+
/**
219+
* @dev Tests excessive decimal places validation
220+
*/
221+
function testExcessiveDecimals() public {
222+
// Test with source decimals too high
223+
uint256 amount = 1000;
224+
uint8 excessiveDecimals = 31;
225+
uint8 normalDecimals = 18;
226+
227+
vm.expectRevert("Source decimals too high");
228+
callCalculateExpectedAmount(amount, excessiveDecimals, normalDecimals);
229+
230+
// Test with destination decimals too high
231+
vm.expectRevert("Destination decimals too high");
232+
callCalculateExpectedAmount(amount, normalDecimals, excessiveDecimals);
233+
}
234+
235+
/**
236+
* @dev Tests calculation with maximum allowed decimal values
237+
*/
238+
function testMaxAllowedDecimals() public {
239+
// Test with maximum allowed decimals (30)
240+
uint256 amount = 1000;
241+
uint8 maxDecimals = 30;
242+
uint8 normalDecimals = 18;
243+
244+
// Should work without reverting
245+
callCalculateExpectedAmount(amount, normalDecimals, maxDecimals);
246+
247+
// Should work without reverting
248+
callCalculateExpectedAmount(amount, maxDecimals, normalDecimals);
249+
}
250+
251+
/**
252+
* @dev Tests with various edge cases
253+
*/
254+
function testEdgeCases() public {
255+
// Test with zero input amount
256+
uint256 zeroAmount = 0;
257+
uint8 decimalsIn = 18;
258+
uint8 decimalsOut = 6;
259+
260+
uint256 result = callCalculateExpectedAmount(zeroAmount, decimalsIn, decimalsOut);
261+
assertEq(result, 0, "Zero input should result in zero output");
262+
263+
// Test with maximum uint256 input and no decimal change
264+
uint256 maxAmount = type(uint256).max;
265+
266+
uint256 sameDecimalResult = callCalculateExpectedAmount(maxAmount, 18, 18);
267+
assertEq(sameDecimalResult, maxAmount, "Same decimal conversion should not change amount");
268+
269+
// Test with maximum uint256 input and reducing decimals
270+
uint256 reducedDecimalResult = callCalculateExpectedAmount(maxAmount, 18, 17);
271+
assertEq(reducedDecimalResult, maxAmount / 10, "Should reduce by factor of 10");
272+
}
207273
}

0 commit comments

Comments
 (0)