Issue | Instances | |
---|---|---|
L-1 | Use of tx.origin is unsafe in almost every context |
4 |
L-2 | Some tokens may revert when zero value transfers are made | 2 |
L-3 | Missing checks for address(0) when assigning values to address state variables |
1 |
L-4 | Use of ecrecover is susceptible to signature malleability |
1 |
L-5 | abi.encodePacked() should not be used with dynamic types when passing the result to a hash function such as keccak256() |
10 |
L-6 | Use of tx.origin is unsafe in almost every context |
4 |
L-7 | decimals() is not a part of the ERC-20 standard |
2 |
L-8 | domainSeparator() isn't protected against replay attacks in case of a future chain split |
2 |
L-9 | External call recipient may consume all transaction gas | 4 |
L-10 | Initializers could be front-run | 5 |
L-11 | Prevent accidentally burning tokens | 32 |
L-12 | Owner can renounce while system is paused | 1 |
L-13 | Loss of precision | 4 |
L-14 | Solidity version 0.8.20+ may not work on other chains due to PUSH0 |
17 |
L-15 | File allows a version of solidity that is susceptible to an assembly optimizer bug | 17 |
L-16 | symbol() is not a part of the ERC-20 standard |
4 |
L-17 | Upgradeable contract not initialized | 8 |
L-18 | Use of ecrecover is susceptible to signature malleability | 1 |
According to Vitalik Buterin, contracts should not assume that tx.origin will continue to be usable or meaningful
. An example of this is EIP-3074 which explicitly mentions the intention to change its semantics when it's used with new op codes. There have also been calls to remove tx.origin
, and there are security issues associated with using it for authorization. For these reasons, it's best to completely avoid the feature.
Instances (4):
File: ./contracts/utils/GatewayCaller.sol
51: tx.origin
61: tx.origin
101: tx.origin
113: tx.origin
Example: https://github.com/d-xo/weird-erc20#revert-on-zero-value-transfers.
In spite of the fact that EIP-20 states that zero-valued transfers must be accepted, some tokens, such as LEND will revert if this is attempted, which may cause transactions that involve other tokens (such as batch operations) to fully revert. Consider skipping the transfer if the amount is zero, which will also save gas.
Instances (2):
File: ./contracts/TokenHandler.sol
179: IERC20(tokenAddress).safeTransferFrom(from, to, amount);
184: IERC20(tokenAddress).safeTransfer(to, amount);
Instances (1):
File: ./contracts/executable/InterchainTokenExecutable.sol
27: interchainTokenService = interchainTokenService_;
The built-in EVM precompile ecrecover
is susceptible to signature malleability, which could lead to replay attacks.
References: https://swcregistry.io/docs/SWC-117, https://swcregistry.io/docs/SWC-121, and https://medium.com/cryptronics/signature-replay-vulnerabilities-in-smart-contracts-3b6f7596df57.
While this is not immediately exploitable, this may become a vulnerability if used elsewhere.
Instances (1):
File: ./contracts/interchain-token/ERC20Permit.sol
86: address recoveredAddress = ecrecover(digest, v, r, s);
[L-5] abi.encodePacked()
should not be used with dynamic types when passing the result to a hash function such as keccak256()
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
). "Unless there is a compelling reason, abi.encode
should be preferred". If there is only one argument to abi.encodePacked()
it can often be cast to bytes()
or bytes32()
instead.
If all arguments are strings and or bytes, bytes.concat()
should be used instead
Instances (10):
File: ./contracts/InterchainTokenService.sol
95: bytes32 internal constant ITS_HUB_CHAIN_NAME_HASH = keccak256(abi.encodePacked(ITS_HUB_CHAIN_NAME));
102: bytes32 internal constant ITS_HUB_ROUTING_IDENTIFIER_HASH = keccak256(abi.encodePacked(ITS_HUB_ROUTING_IDENTIFIER));
884: if (keccak256(abi.encodePacked(destinationChain)) == ITS_HUB_CHAIN_NAME_HASH) revert UntrustedChain();
887: if (keccak256(abi.encodePacked(destinationAddress)) == ITS_HUB_ROUTING_IDENTIFIER_HASH) {
976: if (keccak256(abi.encodePacked(sourceChain)) != ITS_HUB_CHAIN_NAME_HASH) revert UntrustedChain();
987: if (keccak256(abi.encodePacked(sourceChain)) == ITS_HUB_CHAIN_NAME_HASH) revert UntrustedChain();
File: ./contracts/interchain-token/ERC20Permit.sol
80: EIP191_PREFIX_FOR_EIP712_STRUCTURED_DATA,
File: ./contracts/utils/Create3AddressFixed.sol
24: uint160(uint256(keccak256(abi.encodePacked(hex'ff', address(this), deploySalt, CREATE_DEPLOY_BYTECODE_HASH))))
27: deployed = address(uint160(uint256(keccak256(abi.encodePacked(hex'd6_94', deployer, hex'01')))));
File: ./contracts/utils/TokenManagerDeployer.sol
31: bytes memory bytecode = abi.encodePacked(type(TokenManagerProxy).creationCode, args);
According to Vitalik Buterin, contracts should not assume that tx.origin will continue to be usable or meaningful
. An example of this is EIP-3074 which explicitly mentions the intention to change its semantics when it's used with new op codes. There have also been calls to remove tx.origin
, and there are security issues associated with using it for authorization. For these reasons, it's best to completely avoid the feature.
Instances (4):
File: ./contracts/utils/GatewayCaller.sol
51: tx.origin
61: tx.origin
101: tx.origin
113: tx.origin
The decimals()
function is not a part of the ERC-20 standard, and was added later as an optional extension. As such, some valid ERC20 tokens do not support this interface, so it is unsafe to blindly cast all tokens to this interface, and then call this function.
Instances (2):
File: ./contracts/InterchainTokenFactory.sol
200: tokenDecimals = token.decimals();
291: uint8 tokenDecimals = token.decimals();
Severity: Low.
Description: See https://eips.ethereum.org/EIPS/eip-2612#security-considerations.
Remediation: Consider using the implementation from OpenZeppelin, which recalculates the domain separator if the current block.chainid
is not the cached chain ID.
Past occurrences of this issue:
Instances (2):
File: ./contracts/interchain-token/ERC20Permit.sol
53: function DOMAIN_SEPARATOR() public view returns (bytes32) {
81: DOMAIN_SEPARATOR(),
There is no limit specified on the amount of gas used, so the recipient can use up all of the transaction's gas, causing it to revert. Use addr.call{gas: <amount>}("")
or this library instead.
Instances (4):
File: ./contracts/InterchainTokenService.sol
828: IGatewayCaller.callContract.selector,
860: IGatewayCaller.callContractWithToken.selector,
File: ./contracts/utils/GatewayCaller.sol
68: gateway.callContract(destinationChain, destinationAddress, payload);
120: gateway.callContractWithToken(destinationChain, destinationAddress, payload, symbol, amount);
Initializers could be front-run, allowing an attacker to either set their own values, take ownership of the contract, and in the best case forcing a re-deployment
Instances (5):
File: ./contracts/interchain-token/InterchainToken.sol
36: _initialize();
56: function _initialize() internal {
86: function init(bytes32 tokenId_, address minter, string calldata tokenName, string calldata tokenSymbol, uint8 tokenDecimals) external {
89: _initialize();
File: ./contracts/utils/InterchainTokenDeployer.sol
66: IInterchainToken(tokenAddress).init(tokenId, minter, name, symbol, decimals);
Minting and burning tokens to address(0) prevention
Instances (32):
File: ./contracts/InterchainTokenFactory.sol
142: minterBytes = minter.toBytes();
145: tokenId = _deployInterchainToken(salt, '', name, symbol, decimals, minterBytes, 0);
153: token.transferMintership(minter);
157: tokenManager.addFlowLimiter(minter);
159: tokenManager.transferOperatorship(minter);
203: if (!token.isMinter(minter)) revert NotMinter(minter);
205: minter_ = minter.toBytes();
209: tokenId = _deployInterchainToken(salt, destinationChain, tokenName, tokenSymbol, tokenDecimals, minter_, gasValue);
233: tokenId = interchainTokenService.deployInterchainToken{ value: gasValue }(
File: ./contracts/InterchainTokenService.sol
343: address tokenAddress = _deployInterchainToken(tokenId, minter, name, symbol, decimals);
345: _deployTokenManager(tokenId, TokenManagerType.NATIVE_INTERCHAIN_TOKEN, abi.encode(minter, tokenAddress));
345: _deployTokenManager(tokenId, TokenManagerType.NATIVE_INTERCHAIN_TOKEN, abi.encode(minter, tokenAddress));
347: _deployRemoteInterchainToken(tokenId, name, symbol, decimals, minter, destinationChain, gasValue);
803: tokenAddress = _deployInterchainToken(tokenId, minterBytes, name, symbol, decimals);
805: _deployTokenManager(tokenId, TokenManagerType.NATIVE_INTERCHAIN_TOKEN, abi.encode(minterBytes, tokenAddress));
805: _deployTokenManager(tokenId, TokenManagerType.NATIVE_INTERCHAIN_TOKEN, abi.encode(minterBytes, tokenAddress));
1041: emit InterchainTokenDeploymentStarted(tokenId, name, symbol, decimals, minter, destinationChain);
1043: bytes memory payload = abi.encode(MESSAGE_TYPE_DEPLOY_INTERCHAIN_TOKEN, tokenId, name, symbol, decimals, minter);
1101: if (bytes(minterBytes).length != 0) minter = minterBytes.toAddress();
1103: (bool success, bytes memory returnData) = interchainTokenDeployer.delegatecall(
1104: abi.encodeWithSelector(IInterchainTokenDeployer.deployInterchainToken.selector, salt, tokenId, minter, name, symbol, decimals)
1115: emit InterchainTokenDeployed(tokenId, tokenAddress, minter, name, symbol, decimals);
File: ./contracts/TokenHandler.sol
59: _mintToken(tokenManager, tokenAddress, to, amount);
105: _burnToken(tokenManager, tokenAddress, from, amount);
107: _burnTokenFrom(tokenAddress, from, amount);
File: ./contracts/interchain-token/InterchainToken.sol
106: _addMinter(minter);
118: _mint(account, amount);
128: _burn(account, amount);
File: ./contracts/utils/InterchainTokenDeployer.sol
66: IInterchainToken(tokenAddress).init(tokenId, minter, name, symbol, decimals);
File: ./contracts/utils/Minter.sol
23: _addRole(minter_, uint8(Roles.MINTER));
32: _transferRole(msg.sender, minter_, uint8(Roles.MINTER));
41: _proposeRole(msg.sender, minter_, uint8(Roles.MINTER));
The contract owner or single user with a role is not prevented from renouncing the role/ownership while the contract is paused, which would cause any user assets stored in the protocol, to be locked indefinitely.
Instances (1):
File: ./contracts/InterchainTokenService.sol
594: function setPauseStatus(bool paused) external onlyOwner {
Division by large numbers may result in the result being zero, due to solidity not supporting fractions. Consider requiring a minimum amount for the numerator to ensure that it is always larger than the denominator
Instances (4):
File: ./contracts/utils/FlowLimit.sol
66: uint256 epoch = block.timestamp / EPOCH_TIME;
79: uint256 epoch = block.timestamp / EPOCH_TIME;
120: uint256 epoch = block.timestamp / EPOCH_TIME;
135: uint256 epoch = block.timestamp / EPOCH_TIME;
The compiler for Solidity 0.8.20 switches the default target EVM version to Shanghai, which includes the new PUSH0
op code. This op code may not yet be implemented on all L2s, so deployment on these chains will fail. To work around this issue, use an earlier EVM version. While the project itself may or may not compile with 0.8.20, other projects with which it integrates, or which extend this project may, and those projects will have problems deploying these contracts/libraries.
Instances (17):
File: ./contracts/InterchainTokenFactory.sol
3: pragma solidity ^0.8.0;
File: ./contracts/InterchainTokenService.sol
3: pragma solidity ^0.8.0;
File: ./contracts/TokenHandler.sol
3: pragma solidity ^0.8.0;
File: ./contracts/interchain-token/ERC20.sol
3: pragma solidity ^0.8.0;
File: ./contracts/interchain-token/InterchainToken.sol
3: pragma solidity ^0.8.0;
File: ./contracts/proxies/InterchainProxy.sol
3: pragma solidity ^0.8.0;
File: ./contracts/proxies/TokenManagerProxy.sol
3: pragma solidity ^0.8.0;
File: ./contracts/token-manager/TokenManager.sol
3: pragma solidity ^0.8.0;
File: ./contracts/utils/Create3AddressFixed.sol
3: pragma solidity ^0.8.0;
File: ./contracts/utils/Create3Fixed.sol
3: pragma solidity ^0.8.0;
File: ./contracts/utils/FlowLimit.sol
3: pragma solidity ^0.8.0;
File: ./contracts/utils/GatewayCaller.sol
3: pragma solidity ^0.8.0;
File: ./contracts/utils/InterchainTokenDeployer.sol
3: pragma solidity ^0.8.0;
File: ./contracts/utils/Minter.sol
3: pragma solidity ^0.8.0;
File: ./contracts/utils/Operator.sol
3: pragma solidity ^0.8.0;
File: ./contracts/utils/RolesConstants.sol
3: pragma solidity ^0.8.0;
File: ./contracts/utils/TokenManagerDeployer.sol
3: pragma solidity ^0.8.0;
In solidity versions 0.8.13 and 0.8.14, there is an optimizer bug where, if the use of a variable is in a separate assembly
block from the block in which it was stored, the mstore
operation is optimized out, leading to uninitialized memory. The code currently does not have such a pattern of execution, but it does use mstore
s in assembly
blocks, so it is a risk for future changes. The affected solidity versions should be avoided if at all possible.
Instances (17):
File: ./contracts/InterchainTokenFactory.sol
3: pragma solidity ^0.8.0;
File: ./contracts/InterchainTokenService.sol
3: pragma solidity ^0.8.0;
File: ./contracts/TokenHandler.sol
3: pragma solidity ^0.8.0;
File: ./contracts/interchain-token/ERC20.sol
3: pragma solidity ^0.8.0;
File: ./contracts/interchain-token/InterchainToken.sol
3: pragma solidity ^0.8.0;
File: ./contracts/proxies/InterchainProxy.sol
3: pragma solidity ^0.8.0;
File: ./contracts/proxies/TokenManagerProxy.sol
3: pragma solidity ^0.8.0;
File: ./contracts/token-manager/TokenManager.sol
3: pragma solidity ^0.8.0;
File: ./contracts/utils/Create3AddressFixed.sol
3: pragma solidity ^0.8.0;
File: ./contracts/utils/Create3Fixed.sol
3: pragma solidity ^0.8.0;
File: ./contracts/utils/FlowLimit.sol
3: pragma solidity ^0.8.0;
File: ./contracts/utils/GatewayCaller.sol
3: pragma solidity ^0.8.0;
File: ./contracts/utils/InterchainTokenDeployer.sol
3: pragma solidity ^0.8.0;
File: ./contracts/utils/Minter.sol
3: pragma solidity ^0.8.0;
File: ./contracts/utils/Operator.sol
3: pragma solidity ^0.8.0;
File: ./contracts/utils/RolesConstants.sol
3: pragma solidity ^0.8.0;
File: ./contracts/utils/TokenManagerDeployer.sol
3: pragma solidity ^0.8.0;
The symbol()
function is not a part of the ERC-20 standard, and was added later as an optional extension. As such, some valid ERC20 tokens do not support this interface, so it is unsafe to blindly cast all tokens to this interface, and then call this function.
Instances (4):
File: ./contracts/InterchainTokenFactory.sol
199: tokenSymbol = token.symbol();
290: string memory tokenSymbol = token.symbol();
317: string memory symbol = IInterchainToken(token).symbol();
File: ./contracts/TokenHandler.sol
113: symbol = IERC20Named(tokenAddress).symbol();
Upgradeable contracts are initialized via an initializer function rather than by a constructor. Leaving such a contract uninitialized may lead to it being taken over by a malicious user
Instances (8):
File: ./contracts/interchain-token/InterchainToken.sol
29: bytes32 internal constant INITIALIZED_SLOT = 0xc778385ecb3e8cecb82223fa1f343ec6865b2d64c65b0c15c7e8aef225d9e214;
36: _initialize();
47: function _isInitialized() internal view returns (bool initialized) {
49: initialized := sload(INITIALIZED_SLOT)
56: function _initialize() internal {
58: sstore(INITIALIZED_SLOT, true)
87: if (_isInitialized()) revert AlreadyInitialized();
89: _initialize();
The built-in EVM precompile ecrecover is susceptible to signature malleability, which could lead to replay attacks.Consider using OpenZeppelin’s ECDSA library instead of the built-in function.
Instances (1):
File: ./contracts/interchain-token/ERC20Permit.sol
86: address recoveredAddress = ecrecover(digest, v, r, s);
Issue | Instances | |
---|---|---|
M-1 | Centralization Risk for trusted owners | 15 |
M-2 | increaseAllowance/decreaseAllowance won't work on mainnet for USDT |
2 |
Contracts have owners with privileged rights to perform admin tasks and need to be trusted to not perform malicious updates or drain funds.
Instances (15):
File: ./contracts/InterchainTokenFactory.sol
301: function registerGatewayToken(bytes32 tokenIdentifier, string calldata symbol) external onlyOwner returns (bytes32 tokenId) {
File: ./contracts/InterchainTokenService.sol
562: function setFlowLimits(bytes32[] calldata tokenIds, uint256[] calldata flowLimits) external onlyRole(uint8(Roles.OPERATOR)) {
578: function setTrustedAddress(string memory chain, string memory address_) external onlyOwner {
586: function removeTrustedAddress(string memory chain) external onlyOwner {
594: function setPauseStatus(bool paused) external onlyOwner {
File: ./contracts/interchain-token/InterchainToken.sol
117: function mint(address account, uint256 amount) external onlyRole(uint8(Roles.MINTER)) {
127: function burn(address account, uint256 amount) external onlyRole(uint8(Roles.MINTER)) {
File: ./contracts/token-manager/TokenManager.sol
131: function transferFlowLimiter(address from, address to) external onlyRole(uint8(Roles.OPERATOR)) {
140: function addFlowLimiter(address flowLimiter) external onlyRole(uint8(Roles.OPERATOR)) {
149: function removeFlowLimiter(address flowLimiter) external onlyRole(uint8(Roles.OPERATOR)) {
167: function setFlowLimit(uint256 flowLimit_) external onlyRole(uint8(Roles.FLOW_LIMITER)) {
File: ./contracts/utils/Minter.sol
31: function transferMintership(address minter_) external onlyRole(uint8(Roles.MINTER)) {
40: function proposeMintership(address minter_) external onlyRole(uint8(Roles.MINTER)) {
File: ./contracts/utils/Operator.sol
32: function transferOperatorship(address operator) external onlyRole(uint8(Roles.OPERATOR)) {
41: function proposeOperatorship(address operator) external onlyRole(uint8(Roles.OPERATOR)) {
On mainnet, the mitigation to be compatible with increaseAllowance/decreaseAllowance
isn't applied: https://etherscan.io/token/0xdac17f958d2ee523a2206206994597c13d831ec7#code, meaning it reverts on setting a non-zero & non-max allowance, unless the allowance is already zero.
Instances (2):
File: ./contracts/interchain-token/ERC20.sol
104: function increaseAllowance(address spender, uint256 addedValue) external virtual returns (bool) {
123: function decreaseAllowance(address spender, uint256 subtractedValue) external virtual returns (bool) {
Issue | Instances | |
---|---|---|
L-1 | domainSeparator() isn't protected against replay attacks in case of a future chain split |
6 |
L-2 | Solidity version 0.8.20+ may not work on other chains due to PUSH0 |
2 |
L-3 | File allows a version of solidity that is susceptible to an assembly optimizer bug | 2 |
Severity: Low.
Description: See https://eips.ethereum.org/EIPS/eip-2612#security-considerations.
Remediation: Consider using the implementation from OpenZeppelin, which recalculates the domain separator if the current block.chainid
is not the cached chain ID.
Past occurrences of this issue:
Instances (6):
File: ./contracts/gateway/AxelarAmplifierGateway.sol
32: bytes32 domainSeparator_,
34: ) BaseWeightedMultisig(previousSignersRetention_, domainSeparator_, minimumRotationDelay_) {}
File: ./contracts/governance/BaseWeightedMultisig.sol
31: bytes32 public immutable domainSeparator;
46: bytes32 domainSeparator_,
50: domainSeparator = domainSeparator_;
251: return keccak256(bytes.concat('\x19Ethereum Signed Message:\n96', domainSeparator, signersHash, dataHash));
The compiler for Solidity 0.8.20 switches the default target EVM version to Shanghai, which includes the new PUSH0
op code. This op code may not yet be implemented on all L2s, so deployment on these chains will fail. To work around this issue, use an earlier EVM version. While the project itself may or may not compile with 0.8.20, other projects with which it integrates, or which extend this project may, and those projects will have problems deploying these contracts/libraries.
Instances (2):
File: ./contracts/gateway/AxelarAmplifierGateway.sol
3: pragma solidity ^0.8.0;
File: ./contracts/gateway/AxelarAmplifierGatewayProxy.sol
3: pragma solidity ^0.8.0;
In solidity versions 0.8.13 and 0.8.14, there is an optimizer bug where, if the use of a variable is in a separate assembly
block from the block in which it was stored, the mstore
operation is optimized out, leading to uninitialized memory. The code currently does not have such a pattern of execution, but it does use mstore
s in assembly
blocks, so it is a risk for future changes. The affected solidity versions should be avoided if at all possible.
Instances (2):
File: ./contracts/gateway/AxelarAmplifierGateway.sol
3: pragma solidity ^0.8.0;
File: ./contracts/gateway/AxelarAmplifierGatewayProxy.sol
3: pragma solidity ^0.8.0;
Issue | Instances | |
---|---|---|
M-1 | Lack of EIP-712 compliance: using keccak256() directly on an array or struct variable |
3 |
Directly using the actual variable instead of encoding the array values goes against the EIP-712 specification https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md#definition-of-encodedata. Note: OpenSea's Seaport's example with offerHashes and considerationHashes can be used as a reference to understand how array of structs should be encoded.
Instances (3):
File: ./contracts/gateway/AxelarAmplifierGateway.sol
81: bytes32 dataHash = keccak256(abi.encode(CommandType.ApproveMessages, messages));
97: bytes32 dataHash = keccak256(abi.encode(CommandType.RotateSigners, newSigners));
File: ./contracts/governance/BaseWeightedMultisig.sol
119: bytes32 signersHash = keccak256(abi.encode(signers));