Platform: Code4rena
Start Date: 29/07/2022
Pot Size: $50,000 USDC
Total HM: 6
Participants: 75
Period: 5 days
Judge: GalloDaSballo
Total Solo HM: 3
Id: 149
League: ETH
Rank: 15/75
Findings: 2
Award: $194.73
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: oyc_109
Also found by: 0x1f8b, 0x52, 0xNazgul, 0xSmartContract, 0xf15ers, 8olidity, Aymen0909, Bnke0x0, CertoraInc, Chom, CodingNameKiki, Deivitto, Dravee, ElKu, IllIllI, JC, Lambda, Noah3o6, NoamYakov, RedOneN, Respx, ReyAdmirado, Rohan16, Rolezn, Ruhum, Sm4rty, TomJ, Twpony, Waze, Yiko, __141345__, ajtra, apostle0x01, ashiq0x01, asutorufos, bardamu, benbaessler, berndartmueller, bharg4v, bulej93, c3phas, cccz, ch13fd357r0y3r, codexploder, cryptonue, cryptphi, defsec, djxploit, durianSausage, fatherOfBlocks, gogo, hansfriese, horsefacts, ignacio, kyteg, lucacez, mics, rbserver, robee, sashik_eth, simon135, sseefried, tofunmi, xiaoming90
70.0708 USDC - $70.07
Overview
Risk Rating | Number of issues |
---|---|
Low Risk | 5 |
Non-Critical Risk | 3 |
Table of Contents
abi.encodePacked()
should not be used with dynamic types when passing the result to a hash function such as keccak256()
The protocol is using low level calls with solidity version <= 0.8.14 which can result in optimizer bug.
https://medium.com/certora/overly-optimistic-optimizer-certora-bug-disclosure-2101e3f7994d
Consider upgrading to solidity 0.8.15 here:
deposit-service/DepositBase.sol:3:pragma solidity 0.8.9; deposit-service/DepositBase.sol:71: (bool success, bytes memory returnData) = tokenAddress.call(abi.encodeWithSelector(IERC20.transfer.selector, receiver, amount)); gas-service/AxelarGasService.sol:3:pragma solidity 0.8.9; gas-service/AxelarGasService.sol:158: (bool success, bytes memory returnData) = tokenAddress.call(abi.encodeWithSelector(IERC20.transfer.selector, receiver, amount)); gas-service/AxelarGasService.sol:172: (bool success, bytes memory returnData) = tokenAddress.call( xc20/contracts/XC20Wrapper.sol:3:pragma solidity 0.8.9; xc20/contracts/XC20Wrapper.sol:95: (bool success, bytes memory returnData) = tokenAddress.call(abi.encodeWithSelector(IERC20.transfer.selector, receiver, amount)); xc20/contracts/XC20Wrapper.sol:106: (bool success, bytes memory returnData) = tokenAddress.call( AxelarGateway.sol:3:pragma solidity 0.8.9; AxelarGateway.sol:320: (bool success, ) = address(this).call(abi.encodeWithSelector(commandSelector, params[i], commandId)); AxelarGateway.sol:461: (bool success, bytes memory returnData) = tokenAddress.call(callData);
Consider using safeApprove
instead (or better: safeIncreaseAllowance()
/safeDecreaseAllowance()
):
deposit-service/AxelarDepositService.sol:30: IERC20(wrappedTokenAddress).approve(gateway, amount); deposit-service/ReceiverImplementation.sol:38: IERC20(tokenAddress).approve(gateway, amount); deposit-service/ReceiverImplementation.sol:64: IERC20(wrappedTokenAddress).approve(gateway, amount);
Consider adding an address(0)
check for immutable variables:
receiverImplementation
File: AxelarDepositService.sol 16: address public immutable receiverImplementation; 17: 18: constructor(address gateway, string memory wrappedSymbol) DepositBase(gateway, wrappedSymbol) { 19: receiverImplementation = address(new ReceiverImplementation(gateway, wrappedSymbol)); 20: }
gatewayAddress
File: XC20Wrapper.sol 24: address public immutable gatewayAddress; 25: 26: constructor(address gatewayAddress_) { 27: gatewayAddress = gatewayAddress_;
This is already done here:
gateway
File: DepositBase.sol 13: address public immutable gateway; ... 21: constructor(address gateway_, string memory wrappedSymbol_) { 22: if (gateway_ == address(0)) revert InvalidAddress();
and here:
AUTH_MODULE
and TOKEN_DEPLOYER_IMPLEMENTATION
File: AxelarGateway.sol 44: 45: address internal immutable AUTH_MODULE; 46: address internal immutable TOKEN_DEPLOYER_IMPLEMENTATION; 47: 48: constructor(address authModule, address tokenDeployerImplementation) { 49: if (authModule.code.length == 0) revert InvalidAuthModule(); 50: if (tokenDeployerImplementation.code.length == 0) revert InvalidTokenDeployer(); 51: 52: AUTH_MODULE = authModule; 53: TOKEN_DEPLOYER_IMPLEMENTATION = tokenDeployerImplementation; 54: } 55:
abi.encodePacked()
should not be used with dynamic types when passing the result to a hash function such as keccak256()
Similar issue in the past: here Original issue: Hash collisions when using abi.encodePacked() with multiple variable length arguments
Use abi.encode()
instead which will pad items to 32 bytes, which will prevent hash collisions (e.g. abi.encodePacked(0x123,0x456)
=> 0x123456
=> abi.encodePacked(0x1,0x23456)
, but abi.encode(0x123,0x456)
=> 0x0...1230...456
). If there is only one argument to abi.encodePacked()
it can often be cast to bytes()
or bytes32()
instead.
AxelarGateway.sol:298: bytes32 commandHash = keccak256(abi.encodePacked(commands[i]));
Contracts inheriting from OpenZeppelin's libraries have the default transferOwnership()
function (a one-step process). It's possible that the onlyOwner
role mistakenly transfers ownership to a wrong address, resulting in a loss of the onlyOwner
role.
Consider overriding the default transferOwnership()
function to first nominate an address as the pendingOwner
and implementing an acceptOwnership()
function which is called by the pendingOwner
to confirm the transfer.
auth/AxelarAuthWeighted.sol:7:import { Ownable } from '../Ownable.sol'; auth/AxelarAuthWeighted.sol:9:contract AxelarAuthWeighted is Ownable, IAxelarAuthWeighted { auth/AxelarAuthWeighted.sol:47: function transferOperatorship(bytes calldata params) external onlyOwner { gas-service/AxelarGasService.sol:120: function collectFees(address payable receiver, address[] calldata tokens) external onlyOwner { gas-service/AxelarGasService.sol:140: ) external onlyOwner { interfaces/IAxelarAuth.sol:5:import { IOwnable } from './IOwnable.sol'; interfaces/IAxelarAuth.sol:7:interface IAxelarAuth is IOwnable { xc20/contracts/XC20Wrapper.sol:36: _transferOwnership(owner_); xc20/contracts/XC20Wrapper.sol:44: function setXc20Codehash(bytes32 newCodehash) external onlyOwner { xc20/contracts/XC20Wrapper.sol:53: ) external payable onlyOwner { xc20/contracts/XC20Wrapper.sol:66: function removeWrapping(string calldata symbol) external onlyOwner {
The emit
keyword is at line L224, consider moving it after L234:
File: AxelarGateway.sol 217: function upgrade( 218: address newImplementation, 219: bytes32 newImplementationCodeHash, 220: bytes calldata setupParams 221: ) external override onlyAdmin { 222: if (newImplementationCodeHash != newImplementation.codehash) revert InvalidCodeHash(); 223: 224: emit Upgraded(newImplementation); 225: 226: // AUDIT: If `newImplementation.setup` performs `selfdestruct`, it will result in the loss of _this_ implementation (thereby losing the gateway) 227: // if `upgrade` is entered within the context of _this_ implementation itself. 228: if (setupParams.length != 0) { 229: (bool success, ) = newImplementation.delegatecall(abi.encodeWithSelector(IAxelarGateway.setup.selector, setupParams)); 230: 231: if (!success) revert SetupFailed(); 232: } 233: 234: _setImplementation(newImplementation); 235: }
bytes.concat()
Solidity version 0.8.4 introduces bytes.concat()
(vs abi.encodePacked(<bytes>,<bytes>)
), this is relevant.
Solidity version 0.8.12 introduces string.concat()
(vs abi.encodePacked(<str>,<str>)
), but it isn't used in the solution, so the upgrade isn't relevant (0.8.9 is ok)
deposit-service/AxelarDepositService.sol:3:pragma solidity 0.8.9; deposit-service/AxelarDepositService.sol:228: abi.encodePacked( deposit-service/AxelarDepositService.sol:233: keccak256(abi.encodePacked(type(DepositReceiver).creationCode, abi.encode(delegateData))) AxelarGateway.sol:3:pragma solidity 0.8.9; AxelarGateway.sol:540: return keccak256(abi.encodePacked(PREFIX_TOKEN_DAILY_MINT_LIMIT, symbol)); AxelarGateway.sol:544: return keccak256(abi.encodePacked(PREFIX_TOKEN_DAILY_MINT_AMOUNT, symbol, day)); AxelarGateway.sol:548: return keccak256(abi.encodePacked(PREFIX_TOKEN_TYPE, symbol)); AxelarGateway.sol:552: return keccak256(abi.encodePacked(PREFIX_TOKEN_ADDRESS, symbol)); AxelarGateway.sol:556: return keccak256(abi.encodePacked(PREFIX_COMMAND_EXECUTED, commandId));
public
functions not called by the contract should be declared external
insteaddeposit-service/AxelarDepositService.sol:241: function contractId() public pure returns (bytes32) { deposit-service/DepositBase.sol:41: function wrappedToken() public view returns (address) { deposit-service/DepositBase.sol:46: function wrappedSymbol() public view returns (string memory symbol) { xc20/contracts/XC20Wrapper.sol:30: function gateway() public view override returns (IAxelarGateway) { xc20/contracts/XC20Wrapper.sol:40: function contractId() public pure returns (bytes32) { AxelarGateway.sol:152: function tokenDailyMintLimit(string memory symbol) public view override returns (uint256) { AxelarGateway.sol:156: function tokenDailyMintAmount(string memory symbol) public view override returns (uint256) { AxelarGateway.sol:164: function implementation() public view override returns (address) { AxelarGateway.sol:176: function isCommandExecuted(bytes32 commandId) public view override returns (bool) {
#0 - GalloDaSballo
2022-08-31T23:28:38Z
NC <img width="801" alt="Screenshot 2022-09-01 at 01 24 45" src="https://user-images.githubusercontent.com/13383782/187801971-b2661bbb-a5c7-430a-b5d7-51284291064f.png">
Disagree with adding allowance but agree with using safeApprove to avoid nasty compatibility L
L
##Â 1.4. abi.encodePacked() should not be used with dynamic types when passing the result to a hash function such as keccak256() I don't believe this is the case unless you can demonstrate non-idempotency caused by the order of commands, however in that case the finding would be of higher severity
NC
Disagree because it's mostly opinion based and slither will produce false positives
NC
R
Pretty good
2L 1R 3NC
🌟 Selected for report: IllIllI
Also found by: 0x1f8b, 0xNazgul, 0xsam, 8olidity, Aymen0909, Bnke0x0, Chom, CodingNameKiki, Deivitto, Dravee, ElKu, Fitraldys, JC, Lambda, MiloTruck, Noah3o6, NoamYakov, RedOneN, Respx, ReyAdmirado, Rohan16, Rolezn, Ruhum, Sm4rty, TomJ, Tomio, Waze, __141345__, a12jmx, ajtra, ak1, apostle0x01, asutorufos, benbaessler, bharg4v, bulej93, c3phas, defsec, djxploit, durianSausage, erictee, fatherOfBlocks, gerdusx, gogo, kyteg, lucacez, medikko, mics, owenthurm, oyc_109, rbserver, robee, sashik_eth, simon135, tofunmi
124.6631 USDC - $124.66
Overview
Risk Rating | Number of issues |
---|---|
Gas Issues | 9 |
Table of Contents:
<array>.length
should not be looked up in every loop of a for-loop
++i
costs less gas compared to i++
or i += 1
(same for --i
vs i--
or i -= 1
)payable
When new contracts have to be instantiated frequently, it's much cheaper for it to be done via minimal proxies. The only downside is that they rely on delegatecall()
calls for every function, which adds an overhead of ~800 gas, but this is multiple orders of magnitude less than the amount saved during deployment.
Due to the following note, this optimization seem relevant for DepositReceiver
:
File: AxelarDepositService.sol 92: // NOTE: `DepositReceiver` is destroyed in the same runtime context that it is deployed.
Affected code:
deposit-service/AxelarDepositService.sol:93: new DepositReceiver{ salt: salt }( deposit-service/AxelarDepositService.sol:123: new DepositReceiver{ salt: salt }( deposit-service/AxelarDepositService.sol:145: new DepositReceiver{ salt: salt }( deposit-service/AxelarDepositService.sol:171: new DepositReceiver{ salt: salt }( deposit-service/AxelarDepositService.sol:191: new DepositReceiver{ salt: salt }( deposit-service/AxelarDepositService.sol:212: new DepositReceiver{ salt: salt }(
Caching a mapping's value in a local storage
or calldata
variable when the value is accessed multiple times saves ~42 gas per access due to not having to perform the same offset calculation every time.
Affected code:
refundTokens[i]
(calldata)deposit-service/AxelarDepositService.sol:118: if (refundTokens[i] == gatewayToken && msg.sender != refundAddress) continue; deposit-service/AxelarDepositService.sol:121: refundToken = refundTokens[i]; deposit-service/AxelarDepositService.sol:208: if (refundTokens[i] == wrappedTokenAddress && msg.sender != refundAddress) continue; deposit-service/AxelarDepositService.sol:210: refundToken = refundTokens[i];
wrapped[axelarToken]
and unwrapped[xc20Token]
(storage)xc20/contracts/XC20Wrapper.sol:57: if (wrapped[axelarToken] != address(0)) revert('AlreadyWrappingAxelarToken()'); xc20/contracts/XC20Wrapper.sol:58: if (unwrapped[xc20Token] != address(0)) revert('AlreadyWrappingXC20Token()'); xc20/contracts/XC20Wrapper.sol:59: wrapped[axelarToken] = xc20Token; xc20/contracts/XC20Wrapper.sol:60: unwrapped[xc20Token] = axelarToken; xc20/contracts/XC20Wrapper.sol:69: address xc20Token = wrapped[axelarToken]; xc20/contracts/XC20Wrapper.sol:71: wrapped[axelarToken] = address(0); xc20/contracts/XC20Wrapper.sol:72: unwrapped[xc20Token] = address(0); xc20/contracts/XC20Wrapper.sol:77: address wrappedToken = wrapped[axelarToken];
The code can be optimized by minimizing the number of SLOADs (here, notably on the gateway
state variable).
SLOADs are expensive (100 gas after the 1st one) compared to MLOADs/MSTOREs (3 gas each). Storage values read multiple times should instead be cached in memory the first time (costing 1 SLOAD) and then read from this cache to avoid multiple SLOADs.
30: IERC20(wrappedTokenAddress).approve(gateway, amount); //@audit gas SLOAD (gateway) 31: IAxelarGateway(gateway).sendToken(destinationChain, destinationAddress, wrappedSymbol(), amount); //@audit gas SLOAD (gateway)
25: address tokenAddress = IAxelarGateway(gateway).tokenAddresses(symbol); //@audit gas SLOAD (gateway) 38: IERC20(tokenAddress).approve(gateway, amount); //@audit gas SLOAD (gateway) 39: IAxelarGateway(gateway).sendToken(destinationChain, destinationAddress, symbol, amount); //@audit gas SLOAD (gateway)
64: IERC20(wrappedTokenAddress).approve(gateway, amount); //@audit gas SLOAD (gateway) 65: IAxelarGateway(gateway).sendToken(destinationChain, destinationAddress, wrappedSymbol(), amount); //@audit gas SLOAD (gateway)
deposit-service/ReceiverImplementation.sol:35: if (amount == 0) revert NothingDeposited(); deposit-service/ReceiverImplementation.sol:60: if (amount == 0) revert NothingDeposited(); deposit-service/ReceiverImplementation.sol:82: if (amount == 0) revert NothingDeposited();
gas-service/AxelarGasService.sol:69: if (msg.value == 0) revert NothingReceived(); gas-service/AxelarGasService.sol:84: if (msg.value == 0) revert NothingReceived(); gas-service/AxelarGasService.sol:115: if (msg.value == 0) revert NothingReceived();
gas-service/AxelarGasService.sol:121: if (receiver == address(0)) revert InvalidAddress(); gas-service/AxelarGasService.sol:141: if (receiver == address(0)) revert InvalidAddress();
gas-service/AxelarGasService.sol:155: if (amount == 0) revert NothingReceived(); gas-service/AxelarGasService.sol:169: if (amount == 0) revert NothingReceived();
gas-service/AxelarGasService.sol:161: if (!transferred || tokenAddress.code.length == 0) revert TransferFailed(); gas-service/AxelarGasService.sol:177: if (!transferred || tokenAddress.code.length == 0) revert TransferFailed();
xc20/contracts/XC20Wrapper.sol:68: if (axelarToken == address(0)) revert('NotAxelarToken()'); xc20/contracts/XC20Wrapper.sol:78: if (wrappedToken == address(0)) revert('NotAxelarToken()');
xc20/contracts/XC20Wrapper.sol:98: if (!transferred || tokenAddress.code.length == 0) revert('TransferFailed()'); xc20/contracts/XC20Wrapper.sol:111: if (!transferred || tokenAddress.code.length == 0) revert('TransferFailed()');
AxelarGateway.sol:504: if (!burnSuccess) revert BurnFailed(symbol); AxelarGateway.sol:515: if (!burnSuccess) revert BurnFailed(symbol);
<array>.length
should not be looked up in every loop of a for-loop
Reading array length at each iteration of the loop consumes more gas than necessary.
In the best case scenario (length read on a memory variable), caching the array length in the stack saves around 3 gas per iteration. In the worst case scenario (external calls at each iteration), the amount of gas wasted can be massive.
Here, consider storing the array's length in a variable before the for-loop, and use this new variable instead:
auth/AxelarAuthWeighted.sol:17: for (uint256 i; i < recentOperators.length; ++i) { auth/AxelarAuthWeighted.sol:98: for (uint256 i = 0; i < signatures.length; ++i) { auth/AxelarAuthWeighted.sol:116: for (uint256 i; i < accounts.length - 1; ++i) { deposit-service/AxelarDepositService.sol:114: for (uint256 i; i < refundTokens.length; i++) { deposit-service/AxelarDepositService.sol:168: for (uint256 i; i < refundTokens.length; i++) { deposit-service/AxelarDepositService.sol:204: for (uint256 i; i < refundTokens.length; i++) { gas-service/AxelarGasService.sol:123: for (uint256 i; i < tokens.length; i++) { AxelarGateway.sol:207: for (uint256 i = 0; i < symbols.length; i++) {
++i
costs less gas compared to i++
or i += 1
(same for --i
vs i--
or i -= 1
)Pre-increments and pre-decrements are cheaper.
For a uint256 i
variable, the following is true with the Optimizer enabled at 10k:
Increment:
i += 1
is the most expensive formi++
costs 6 gas less than i += 1
++i
costs 5 gas less than i++
(11 gas less than i += 1
)Decrement:
i -= 1
is the most expensive formi--
costs 11 gas less than i -= 1
--i
costs 5 gas less than i--
(16 gas less than i -= 1
)Note that post-increments (or post-decrements) return the old value before incrementing or decrementing, hence the name post-increment:
uint i = 1; uint j = 2; require(j == i++, "This will be false as i is incremented after the comparison");
However, pre-increments (or pre-decrements) return the new value:
uint i = 1; uint j = 2; require(j == ++i, "This will be true as i is incremented before the comparison");
In the pre-increment case, the compiler has to create a temporary variable (when used) for returning 1
instead of 2
.
Affected code:
deposit-service/AxelarDepositService.sol:114: for (uint256 i; i < refundTokens.length; i++) { deposit-service/AxelarDepositService.sol:168: for (uint256 i; i < refundTokens.length; i++) { deposit-service/AxelarDepositService.sol:204: for (uint256 i; i < refundTokens.length; i++) { gas-service/AxelarGasService.sol:123: for (uint256 i; i < tokens.length; i++) { AxelarGateway.sol:207: for (uint256 i = 0; i < symbols.length; i++) {
Consider using pre-increments and pre-decrements where they are relevant (meaning: not where post-increments/decrements logic are relevant).
In Solidity 0.8+, there's a default overflow check on unsigned integers. It's possible to uncheck this in for-loops and save some gas at each iteration, but at the cost of some code readability, as this uncheck cannot be made inline.
Consider wrapping with an unchecked
block here (around 25 gas saved per instance):
auth/AxelarAuthWeighted.sol:17: for (uint256 i; i < recentOperators.length; ++i) { auth/AxelarAuthWeighted.sol:69: for (uint256 i = 0; i < weightsLength; ++i) { auth/AxelarAuthWeighted.sol:98: for (uint256 i = 0; i < signatures.length; ++i) { auth/AxelarAuthWeighted.sol:101: for (; operatorIndex < operatorsLength && signer != operators[operatorIndex]; ++operatorIndex) {} auth/AxelarAuthWeighted.sol:116: for (uint256 i; i < accounts.length - 1; ++i) { deposit-service/AxelarDepositService.sol:114: for (uint256 i; i < refundTokens.length; i++) { deposit-service/AxelarDepositService.sol:168: for (uint256 i; i < refundTokens.length; i++) { deposit-service/AxelarDepositService.sol:204: for (uint256 i; i < refundTokens.length; i++) { gas-service/AxelarGasService.sol:123: for (uint256 i; i < tokens.length; i++) { AxelarGateway.sol:195: for (uint256 i; i < adminCount; ++i) { AxelarGateway.sol:207: for (uint256 i = 0; i < symbols.length; i++) { AxelarGateway.sol:292: for (uint256 i; i < commandsLength; ++i) {
The change would be:
- for (uint256 i; i < numIterations; i++) { + for (uint256 i; i < numIterations;) { // ... + unchecked { ++i; } }
The same can be applied with decrements (which should use break
when i == 0
).
The risk of overflow is non-existant for uint256
here.
Using newer compiler versions and the optimizer give gas optimizations. Also, additional safety checks are available for free.
The advantages here are:
Consider upgrading from 0.8.9
to at least 0.8.10
in the solution.
payable
If a function modifier such as onlyOwner
is used, the function will revert if a normal user tries to pay the function. Marking the function as payable
will lower the gas cost for legitimate callers because the compiler will not include checks for whether a payment was provided.
auth/AxelarAuthWeighted.sol:47: function transferOperatorship(bytes calldata params) external onlyOwner { gas-service/AxelarGasService.sol:120: function collectFees(address payable receiver, address[] calldata tokens) external onlyOwner { xc20/contracts/XC20Wrapper.sol:44: function setXc20Codehash(bytes32 newCodehash) external onlyOwner { xc20/contracts/XC20Wrapper.sol:66: function removeWrapping(string calldata symbol) external onlyOwner { AxelarGateway.sol:204: function setTokenDailyMintLimits(string[] calldata symbols, uint256[] calldata limits) external override onlyAdmin { AxelarGateway.sol:331: function deployToken(bytes calldata params, bytes32) external onlySelf { AxelarGateway.sol:367: function mintToken(bytes calldata params, bytes32) external onlySelf { AxelarGateway.sol:373: function burnToken(bytes calldata params, bytes32) external onlySelf { AxelarGateway.sol:397: function approveContractCall(bytes calldata params, bytes32 commandId) external onlySelf { AxelarGateway.sol:411: function approveContractCallWithMint(bytes calldata params, bytes32 commandId) external onlySelf { AxelarGateway.sol:437: function transferOperatorship(bytes calldata newOperatorsData, bytes32) external onlySelf {
#0 - GalloDaSballo
2022-08-23T00:39:59Z
I seem to be getting between 20 and 40 gas savings in caching, I can't quite explain why this would happen for calldata. For storage the position is recomputed, and that takes a keccak (30 gas) + costs for handling memory, but for calldata it's just an offset (I'd assume 3 gas) + calldata load (up to 12 gas on evm.codes)
Will give it 20 gas per instance per calldata * 2 = 40
40 for Storage * 5 = 200
First time saves 94 gas (100 - 6 - SLOAD + MLOAD), each subsequent is 97 (100 - 3 - SLOAD)
94+94+97+94
Around 300 gas like other submissions
Skipping external calls will save about 100 gas per instance, however no instances were listed, giving it 100 gas
In this case I don't think 1 is applicable, same for the last function as while it does save gas it's a trade-off
All in all way better than average report, highly recommend the sponsor to follow the advice
1019 gas saved