Platform: Code4rena
Start Date: 07/04/2022
Pot Size: $50,000 USDC
Total HM: 5
Participants: 19
Period: 5 days
Judge: 0xean
Total Solo HM: 4
Id: 109
League: COSMOS
Rank: 6/19
Findings: 2
Award: $2,136.00
🌟 Selected for report: 2
🚀 Solo Findings: 0
🌟 Selected for report: IllIllI
Also found by: CertoraInc, Dravee, Funen, cccz, delfin454000, dirk_y, ilan, rayn, rishabh
Storing the block.chainid
is not safe. See this issue from a prior contest for details.
DOMAIN_SEPARATOR = keccak256( abi.encode( DOMAIN_TYPE_SIGNATURE_HASH, keccak256(bytes(name)), keccak256(bytes('1')), block.chainid,
allow
transferFrom
to occur whileexpiry >= block.timestamp
.
https://github.com/ethereum/EIPs/blob/1473025f064929bfab405eb00b8cd16dd741f269/EIPS/eip-2612.md?plain=1#L172
The current code should change to use <=
require(block.timestamp < deadline, 'EXPIRED');
revert()
ing rather than allowing operations to go throughfunction _execute( string memory sourceChain, string memory sourceAddress, bytes calldata payload ) internal virtual {} function _executeWithToken( string memory sourceChain, string memory sourceAddress, bytes calldata payload, string memory tokenSymbol, uint256 amount ) internal virtual {}
address(0x0)
when assigning values to address
state variablesTOKEN_DEPLOYER_IMPLEMENTATION = tokenDeployerImplementation;
Code architecture, incentives, and error handling/reporting questions/issues should be resolved before deployment
// TODO move burnFrom into a separate BurnableERC20 contract
Low-level calls return success if there is no code present at the specified address. In addition to the zero-address checks, add a check to verify that <address>.code.length > 0
if (tokenAddress == address(0)) revert TokenDoesNotExist(symbol); if (_getTokenType(symbol) == TokenType.External) { _checkTokenStatus(symbol); DepositHandler depositHandler = new DepositHandler{ salt: salt }(); (bool success, bytes memory returnData) = depositHandler.execute( tokenAddress, abi.encodeWithSelector( IERC20.transfer.selector, address(this), IERC20(tokenAddress).balanceOf(address(depositHandler)) ) ); if (!success || (returnData.length != uint256(0) && !abi.decode(returnData, (bool)))) revert BurnFailed(symbol);
(bool success, ) = gatewayImplementation.delegatecall(
let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)
(bool success, bytes memory data) = TOKEN_DEPLOYER_IMPLEMENTATION.delegatecall(
require()
/revert()
statements should have descriptive reason stringsrequire(_lockedStatus == IS_NOT_LOCKED);
public
functions not called by the contract should be declared external
insteadContracts are allowed to override their parents' functions and change the visibility from external
to public
.
function burn(bytes32 salt) public onlyOwner {
function mint(address account, uint256 amount) public onlyOwner {
constant
s should be defined rather than using magic numbersbytes1(0xff),
if (signature.length != 65) revert InvalidSignatureLength();
if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) revert InvalidS();
if (v != 27 && v != 28) revert InvalidV();
if (v != 27 && v != 28) revert InvalidV();
require(uint256(s) <= 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0, 'INV_S');
require(v == 27 || v == 28, 'INV_V');
require(v == 27 || v == 28, 'INV_V');
Use a solidity version of at least 0.8.12 to get string.concat()
to be used instead of abi.encodePacked(<str>,<str>)
pragma solidity 0.8.9;
pragma solidity 0.8.9;
pragma solidity 0.8.9;
pragma solidity 0.8.9;
pragma solidity 0.8.9;
pragma solidity 0.8.9;
address
mappings can be combined into a single mapping
of an address
to a struct
, where appropriatemapping(address => uint256) public override balanceOf; mapping(address => mapping(address => uint256)) public override allowance;
const
/immutable
variablesIf the variable needs to be different based on which class it comes from, a view
/pure
function should be used instead (e.g. like this).
bytes32 public DOMAIN_SEPARATOR;
// SPDX-License-Identifier: MIT
// SPDX-License-Identifier: MIT
// SPDX-License-Identifier: MIT
// SPDX-License-Identifier: MIT
// SPDX-License-Identifier: MIT
// SPDX-License-Identifier: MIT
// SPDX-License-Identifier: MIT
// SPDX-License-Identifier: MIT
// SPDX-License-Identifier: MIT
// SPDX-License-Identifier: MIT
// SPDX-License-Identifier: MIT
indexed
fieldsEach event
should use three indexed
fields if there are three or more fields
event TokenSent( address indexed sender, string destinationChain, string destinationAddress, string symbol, uint256 amount );
event ContractCall( address indexed sender, string destinationChain, string destinationContractAddress, bytes32 indexed payloadHash, bytes payload );
event ContractCallWithToken( address indexed sender, string destinationChain, string destinationContractAddress, bytes32 indexed payloadHash, bytes payload, string symbol, uint256 amount );
event TokenDeployed(string symbol, address tokenAddresses);
event TokenFrozen(string symbol);
event TokenUnfrozen(string symbol);
event OwnershipTransferred(address[] preOwners, uint256 prevThreshold, address[] newOwners, uint256 newThreshold);
event OperatorshipTransferred( address[] preOperators, uint256 prevThreshold, address[] newOperators, uint256 newThreshold );
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
If the modifier is used with a function that has a named return, the default value will be returned which may lead to confusing behavior. Consider using a function instead of a modifier
if (adminVoteCount < _getAdminThreshold(adminEpoch)) return; _;
Doing what OpenZeppelin does and considering type(uint256).max
to be infinite approval may help users to create smaller transactions.
_approve(sender, _msgSender(), allowance[sender][_msgSender()] - amount);
The current owner would nominate a new owner, and to become the new owner, the nominated account would have to approve the change, so that the address is proven to be valid
function transferOwnership(address newOwner) public virtual onlyOwner {
if (signature.length != 65) revert InvalidSignatureLength();
#0 - deluca-mike
2022-04-20T09:38:18Z
Acknowledged, since just as that linked finding suggested, if Ethereum forks we will not use any minority fork, since we are dealing with (bridging) assets that cannot be split.
Confirmed.
Disputed. They are no unimplemented, they are virtual and meant to be overridden, similar to _beforeTokenTransfer
in ERC20.
Confirmed. Will correct by checking code length of the address, which will also help with a delegatcall elsewhere in the contract.
Confirmed, will remove TODO and move the fucntion as noted.
File: src/AxelarGateway.sol (lines 398-415) Disputed since that is not a low-level call, despite looking similar to one.
File: src/AxelarGatewayProxy.sol (line 19) Acknowledged, but given that it is a highly-used delegatecall as part of the proxy pattern, there is no point checking if the contract exists. If it does not, we have much much larger problems.
File: src/AxelarGatewayProxy.sol (line 34) As above, acknowledged, but given that it is a highly-used delegatecall as part of the proxy pattern, there is no point checking if the contract exists. If it does not, we have much much larger problems.
File: src/AxelarGateway.sol (line 350)
Confirmed, but we will solve this but doing a if (tokenDeployerImplementation.code.length == 0) revert InvalidTokenDeployer();
in the gateway's constructor, to prevent it form ever being initialized with an invalid TokenDeployer.
Confirmed, and will be fixed with adopting if-revert with custom error messages in place of all requires.
Confirmed.
Acknowledged, but these were intentional so they could be compared to (Solidity Docs)[https://docs.soliditylang.org/en/v0.8.9/control-structures.html?highlight=bytes1(0xff)#salted-contract-creations-create2] and standard ECDSA implementations.
Acknowledged, but these were all tested on 0.8.9, and this is locked in for release. The next release will take latest Solidity available at the time testing is started.
Disputed. While true, and would save gas, this would e a deviation from the ERC20 standard, and add complexity (in terms of standards and readability) for minimal gain.
Confirmed. That variable should have been, and will be, made immutable.
Acknowledged, but more documentation will come in subsequent releases.
Acknowledged. Indexed fields actually cost a bit more gas, and should only be reserved for data that can be known ahead of time. Further, it is not yet clear which fields should be indexed, so they will only be indexed at a later release when we gather requirements. However, one should never index fields whose possible values are not part of some reasonable set (i.e. amount).
Confirmed, however since all functions that currently use this modifier do not return data, we will just leave a comment above the modifier noting that it should be used with care.
Confirmed.
Acknowledged, but such a comment would be misplaced here, and would be better in normal documentation.
#1 - 0xean
2022-05-12T19:17:08Z
Severities proposed by warden seem correct with the exception of
Unumplemented functions should have the default behavior of revert()ing rather than allowing operations to go through
which is invalid as stated by the sponsor.
#2 - liveactionllama
2022-06-13T06:19:59Z
Just a note that C4 is excluding the invalid entry (as noted by the judge above) from the official report.
1
and 2
rather than 0
and 1
saves gasSee this issue from a prior contest for details
uint256 internal constant IS_NOT_LOCKED = uint256(0); uint256 internal constant IS_LOCKED = uint256(1);
return payable(msg.sender);
Use a solidity version of at least 0.8.10 to have external calls skip contract existence checks if the external call has a return value
pragma solidity 0.8.9;
pragma solidity 0.8.9;
pragma solidity 0.8.9;
pragma solidity 0.8.9;
pragma solidity 0.8.9;
pragma solidity 0.8.9;
pragma solidity 0.8.9;
pragma solidity 0.8.9;
pragma solidity 0.8.9;
pragma solidity 0.8.9;
pragma solidity 0.8.9;
pragma solidity 0.8.9;
pragma solidity 0.8.9;
pragma solidity 0.8.9;
pragma solidity 0.8.9;
pragma solidity 0.8.9;
pragma solidity 0.8.9;
pragma solidity 0.8.9;
pragma solidity 0.8.9;
bool
s for storage incurs overhead// Booleans are more expensive than uint256 or any type that takes up a full // word because each write operation emits an extra SLOAD to first read the // slot's contents, replace the bits taken up by the boolean, and then write // back. This is the compiler's defense against contract upgrades and // pointer aliasing, and it cannot be disabled.
mapping(bytes32 => bool) private _boolStorage;
<array>.length
should not be looked up in every loop of a for
-loopEven memory arrays incur the overhead of bit tests and bit shifts to calculate the array length
for (uint256 i; i < accounts.length; i++) {
for (uint256 i; i < accounts.length; i++) {
calldata
instead of memory
for read-only arguments in external
functions saves gasstring memory destinationChain,
string memory destinationAddress,
string memory symbol,
string memory destinationChain,
string memory destinationContractAddress,
bytes memory payload
string memory destinationChain,
string memory destinationContractAddress,
bytes memory payload,
string memory symbol,
string memory sourceChain,
string memory sourceAddress,
string memory sourceChain,
string memory sourceAddress,
string memory symbol,
string memory sourceChain,
string memory sourceAddress,
string memory sourceChain,
string memory sourceAddress,
string memory symbol,
function freezeToken(string memory symbol) external override onlyAdmin {
function unfreezeToken(string memory symbol) external override onlyAdmin {
string memory name,
string memory symbol,
string memory destinationChain,
string memory destinationAddress,
string memory symbol,
string memory destinationChain,
string memory contractAddress,
bytes memory payload
string memory destinationChain,
string memory contractAddress,
bytes memory payload,
string memory symbol,
string memory sourceChain,
string memory sourceAddress,
string memory sourceChain,
string memory sourceAddress,
string memory symbol,
string memory sourceChain,
string memory sourceAddress,
string memory sourceChain,
string memory sourceAddress,
string memory symbol,
function tokenAddresses(string memory symbol) external view returns (address);
function tokenFrozen(string memory symbol) external view returns (bool);
function freezeToken(string memory symbol) external;
function unfreezeToken(string memory symbol) external;
string memory sourceChain,
string memory sourceAddress,
string memory sourceChain,
string memory sourceAddress,
string memory tokenSymbol,
++i
/i++
should be unchecked{++i}
/unchecked{++i}
when it is not possible for them to overflow, as is the case when used in for
- and while
-loopsfor (uint256 i; i < adminCount; i++) {
for (uint256 i; i < adminCount; i++) {
for (uint256 i; i < adminLength; i++) {
for (uint256 i; i < accounts.length - 1; ++i) {
for (uint256 i; i < accounts.length; i++) {
for (uint256 i; i < ownerCount; i++) {
for (uint256 i; i < accountLength; i++) {
for (uint256 i; i < accounts.length; i++) {
for (uint256 i; i < operatorCount; i++) {
for (uint256 i; i < accountLength; i++) {
for (uint256 i; i < signatureCount; i++) {
for (uint256 i; i < commandsLength; i++) {
++i
costs less gas than ++i
, especially when it's used in for
-loops (--i
/i--
too)for (uint256 i; i < adminCount; i++) {
for (uint256 i; i < adminCount; i++) {
for (uint256 i; i < adminLength; i++) {
for (uint256 i; i < accounts.length; i++) {
for (uint256 i; i < ownerCount; i++) {
for (uint256 i; i < accountLength; i++) {
for (uint256 i; i < accounts.length; i++) {
for (uint256 i; i < operatorCount; i++) {
for (uint256 i; i < accountLength; i++) {
for (uint256 i; i < signatureCount; i++) {
for (uint256 i; i < commandsLength; i++) {
uints
/ints
smaller than 32 bytes (256 bits) incurs overheadWhen using elements that are smaller than 32 bytes, your contract’s gas usage may be higher. This is because the EVM operates on 32 bytes at a time. Therefore, if the element is smaller than that, the EVM must use more operations in order to reduce the size of the element from 32 bytes to the desired size.
https://docs.soliditylang.org/en/v0.8.11/internals/layout_in_storage.html Use a larger size then downcast where needed
uint8 internal constant OLD_KEY_RETENTION = 16;
uint8 decimals,
uint8 public immutable decimals;
uint8 decimals_
(string memory name, string memory symbol, uint8 decimals, uint256 cap, address tokenAddr) = abi.decode(
uint8 decimals,
uint8 decimals,
uint8 v;
uint8 decimals,
uint8 v,
keccak256()
, should use immutable
rather than constant
See this issue for a detail description of the issue
bytes32 internal constant KEY_ALL_TOKENS_FROZEN = keccak256('all-tokens-frozen');
bytes32 internal constant PREFIX_COMMAND_EXECUTED = keccak256('command-executed');
bytes32 internal constant PREFIX_TOKEN_ADDRESS = keccak256('token-address');
bytes32 internal constant PREFIX_TOKEN_TYPE = keccak256('token-type');
bytes32 internal constant PREFIX_TOKEN_FROZEN = keccak256('token-frozen');
bytes32 internal constant PREFIX_CONTRACT_CALL_APPROVED = keccak256('contract-call-approved');
bytes32 internal constant PREFIX_CONTRACT_CALL_APPROVED_WITH_MINT = keccak256('contract-call-approved-with-mint');
bytes32 internal constant SELECTOR_BURN_TOKEN = keccak256('burnToken');
bytes32 internal constant SELECTOR_DEPLOY_TOKEN = keccak256('deployToken');
bytes32 internal constant SELECTOR_MINT_TOKEN = keccak256('mintToken');
bytes32 internal constant SELECTOR_APPROVE_CONTRACT_CALL = keccak256('approveContractCall');
bytes32 internal constant SELECTOR_APPROVE_CONTRACT_CALL_WITH_MINT = keccak256('approveContractCallWithMint');
bytes32 internal constant SELECTOR_TRANSFER_OPERATORSHIP = keccak256('transferOperatorship');
bytes32 internal constant SELECTOR_TRANSFER_OWNERSHIP = keccak256('transferOwnership');
bytes32 internal constant KEY_ADMIN_EPOCH = keccak256('admin-epoch');
bytes32 internal constant PREFIX_ADMIN = keccak256('admin');
bytes32 internal constant PREFIX_ADMIN_COUNT = keccak256('admin-count');
bytes32 internal constant PREFIX_ADMIN_THRESHOLD = keccak256('admin-threshold');
bytes32 internal constant PREFIX_ADMIN_VOTE_COUNTS = keccak256('admin-vote-counts');
bytes32 internal constant PREFIX_ADMIN_VOTED = keccak256('admin-voted');
bytes32 internal constant PREFIX_IS_ADMIN = keccak256('is-admin');
bytes32 internal constant KEY_OWNER_EPOCH = keccak256('owner-epoch');
bytes32 internal constant PREFIX_OWNER = keccak256('owner');
bytes32 internal constant PREFIX_OWNER_COUNT = keccak256('owner-count');
bytes32 internal constant PREFIX_OWNER_THRESHOLD = keccak256('owner-threshold');
bytes32 internal constant PREFIX_IS_OWNER = keccak256('is-owner');
bytes32 internal constant KEY_OPERATOR_EPOCH = keccak256('operator-epoch');
bytes32 internal constant PREFIX_OPERATOR = keccak256('operator');
bytes32 internal constant PREFIX_OPERATOR_COUNT = keccak256('operator-count');
bytes32 internal constant PREFIX_OPERATOR_THRESHOLD = keccak256('operator-threshold');
bytes32 internal constant PREFIX_IS_OPERATOR = keccak256('is-operator');
require()
/revert()
checks should be refactored to a modifier or functionrequire(account != address(0), 'ZERO_ADDR');
immutable
string public name;
string public symbol;
uint256 public cap;
revert()
/require()
strings to save deployment gaspayable
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.
function freezeToken(string memory symbol) external override onlyAdmin {
function unfreezeToken(string memory symbol) external override onlyAdmin {
function freezeAllTokens() external override onlyAdmin {
function unfreezeAllTokens() external override onlyAdmin {
function upgrade( address newImplementation, bytes32 newImplementationCodeHash, bytes calldata setupParams ) external override onlyAdmin {
function deployToken(bytes calldata params, bytes32) external onlySelf {
function mintToken(bytes calldata params, bytes32) external onlySelf {
function burnToken(bytes calldata params, bytes32) external onlySelf {
function approveContractCall(bytes calldata params, bytes32 commandId) external onlySelf {
function approveContractCallWithMint(bytes calldata params, bytes32 commandId) external onlySelf {
function transferOwnership(bytes calldata params, bytes32) external onlySelf {
function transferOperatorship(bytes calldata params, bytes32) external onlySelf {
function burn(bytes32 salt) public onlyOwner {
function transferOwnership(address newOwner) public virtual onlyOwner {
function mint(address account, uint256 amount) public onlyOwner {
function burnFrom(address account, uint256 amount) external onlyOwner {
#0 - deluca-mike
2022-04-20T09:50:56Z
Confirmed.
Disputed, since in most cases it does not actually cost more gas, however, we are removing the Context contract anyway.
Acknowledged, but these were all tested on 0.8.9, and this is locked in for release. The next release will take latest Solidity available at the time testing is started.
Acknowledged, but it's better to use bools for readability in many cases.
Disputed, since recent versions of solidity optimize for this if the array has a fixed length.
Confirmed.
Confirmed for using ++i instead of i++, but acknowledged for the unchecked suggestion, which we will forgo for readability, for now.
Acknowledged, but will not change since the overhead is at worst minimal, and much of this functionality/code is shared in the space. Further, it helps with readability.
Disputed, since the issue being referred to is 2 years old, and since then, Solidity computes the literals at compile-time.
Acknowledged, but will not change as this is a design preference and the compiler can optimize out duplicate code.
While true, and thus confirmed, for cap, this is not true, and thus disputed for strings, since non-value types cannot be immutable in Solidity (yet).
Confirmed.
Acknowledged, but will not do since it is gas savings at the expense of confusion, readability issues, and the possibilities for ETH to be received.