Platform: Code4rena
Start Date: 12/07/2023
Pot Size: $80,000 USDC
Total HM: 11
Participants: 47
Period: 9 days
Judge: berndartmueller
Total Solo HM: 1
Id: 260
League: ETH
Rank: 22/47
Findings: 2
Award: $188.68
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: immeas
Also found by: Bauchibred, DavidGiladi, Emmanuel, Jeiwan, MohammedRizwan, MrPotatoMagic, Rolezn, Sathish9098, T1MOH, Udsen, banpaleo5, hals, matrix_0wl, naman1778
43.3267 USDC - $43.33
The onlySigners modifier in the link above should be made into a function since it makes changes to storage. These are the lines where state changes occur:
File: contracts/cgp/auth/MultisigBase.sol 53: voting.hasVoted[msg.sender] = true; 61: voting.voteCount = voteCount; 66: voting.voteCount = 0; 71: voting.hasVoted[signers.accounts[i]] = false;
implementations is written incorrectly as implementaions on Line 559 (as parameter) and Line 564.
File: contracts/its/interchain-token-service/InterchainTokenService.sol 559: function _sanitizeTokenManagerImplementation(address[] memory implementaions, TokenManagerType tokenManagerType) 560: internal 561: pure 562: returns (address implementation) 563: { 564: implementation = implementaions[uint256(tokenManagerType)]; 565: if (implementation == address(0)) revert ZeroAddress(); 566: if (ITokenManager(implementation).implementationType() != uint256(tokenManagerType)) revert InvalidTokenManagerImplementation(); 567: }
There are 2 instances of this:
Event is emitted in the if block but not else block.
File: contracts/its/interchain-token-service/InterchainTokenService.sol 609: if (expressCaller == address(0)) { 610: amount = tokenManager.giveToken(destinationAddress, amount); 611: emit TokenReceived(tokenId, sourceChain, destinationAddress, amount); 612: } else { 613: amount = tokenManager.giveToken(expressCaller, amount); //@audit missing event emission here 614: }
In this instance, an early return misses event emission on Line 653.
File: contracts/its/interchain-token-service/InterchainTokenService.sol 652: if (expressCaller != address(0)) { 653: amount = tokenManager.giveToken(expressCaller, amount); 654: return; //@audit early return misses event emission 655: } 656: } 657: amount = tokenManager.giveToken(destinationAddress, amount); 658: IInterchainTokenExpressExecutable(destinationAddress).executeWithInterchainToken(sourceChain, sourceAddress, data, tokenId, amount); 659: emit TokenReceivedWithData(tokenId, sourceChain, destinationAddress, amount, sourceAddress, data); 660: }
The _getProposalHash() function is used to get the proposal hash in the _processCommand() function in the InterchainGovernance.sol contract but not in the _processCommand() function in the AxelarServiceCommand.sol contract (which inherits from InterchainGovernance.sol).
We can take a look at the difference in pattern below:
File: contracts/cgp/governance/InterchainGovernance.sol 125: bytes32 proposalHash = _getProposalHash(target, callData, nativeValue);
File: contracts/cgp/governance/AxelarServiceGovernance.sol 84: bytes32 proposalHash = keccak256(abi.encodePacked(target, callData, nativeValue));
Missing EOA check for distributor
File: contracts/its/utils/Distributable.sol 39: function _setDistributor(address distributor_) internal { 40: assembly { 41: sstore(DISTRIBUTOR_SLOT, distributor_) 42: } 43: emit DistributorChanged(distributor_); 44: }
Missing EOA check for Operator:
File: contracts/its/utils/Operatable.sol 39: function _setOperator(address operator_) internal { 40: assembly { 41: sstore(OPERATOR_SLOT, operator_) 42: } 43: emit OperatorChanged(operator_); 44: }
Solution for both would be to check the extcodesize(distributor/operator) > 0. If this is true, we revert with an error mentioning that the distributor/operator is not an EOA.
There are 3 instances of this issue occurring in the following 3 functions:
deployRemoteCanonicalToken - https://github.com/code-423n4/2023-07-axelar/blob/2f9b234bb8222d5fbe934beafede56bfb4522641/contracts/its/interchain-token-service/InterchainTokenService.sol#L322
deployRemoteCustomTokenManager - https://github.com/code-423n4/2023-07-axelar/blob/2f9b234bb8222d5fbe934beafede56bfb4522641/contracts/its/interchain-token-service/InterchainTokenService.sol#L360
deployAndRegisterRemoteStandardizedToken - https://github.com/code-423n4/2023-07-axelar/blob/2f9b234bb8222d5fbe934beafede56bfb4522641/contracts/its/interchain-token-service/InterchainTokenService.sol#L413
When sending proposal for execution at single destination chain using sendProposal(), we do not check if the msg.value provided is equal to the gas required. This check has been added to the sendProposals() function but not in sendProposal().
#0 - c4-judge
2023-09-05T10:17:15Z
berndartmueller marked the issue as grade-c
#1 - mcgrathcoutinho
2023-09-08T19:37:54Z
Hi @berndartmueller , could you please provide some feedback on why this was marked grade C.
I've seen other reports point out the issues I have as well (example: #490) and I believe deserves a higher grade than C.
Thank you.
#2 - berndartmueller
2023-09-09T19:55:06Z
Hey @mcgrathcoutinho,
I've decided to change the grade to "B". Thanks for pointing it out. It seems I was too hasty grading your submission.
#3 - c4-judge
2023-09-09T19:55:23Z
berndartmueller marked the issue as grade-b
#4 - mcgrathcoutinho
2023-09-09T20:21:38Z
Thank you!
🌟 Selected for report: pcarranzav
Also found by: Jeiwan, K42, MrPotatoMagic, Sathish9098, libratus, niloy
To start evaluating this project, I identified the high level functionalities and its corresponding contracts involved in the scope (more on this in the Mechanism Review section below). The evaluation of the contracts (for each of a,b,c) was done in a base-to-derived contract manner. The functionalities are as follows: a. Interchain Governance b. Interchain Token/Message transfer c. Helper utility contracts involved in facilitating a and b as well as to upgrade certain contracts (I will not be diving deeper into this separately but will refer to the contracts wherever necessary).
Additionally, I approached this codebase from an attacker's mindset. Through this type of mentality, I could recognise three points where failure/potential bugs could exist:
This diagram serves as a great reference to visualize these endpoints: https://cdn.discordapp.com/attachments/1126884239330263072/1130039751173484604/gmp-diagram.webp
The architecture of the protocol is quite simple and easy to understand. The developer team has done an amazing job in modularising the code into smaller chunks and storing it in separate files. The only recommendation I would have is to divide the InterchainTokenService.sol contract into two files. The pattern of this contract is as follows:
=> represents "calling" Public/External functions => Internal functions (within the same contract)
Having the public/external functions and the internal functions in two separate files would greatly reduce the size of the file from 533 nSLOC to half of it, making the code more readable, modular and less concentrated (just like the rest of the contracts).
Overall, the codebase is structured in an easy to understand manner. The documentation and Natspec comments provided helps to recognize the endpoint (both customer and validator side) and intermediary contracts involved in facilitating interchain transfers. There are minor edge case issues that can lead to big exploits. Some of these issues (that I have included in my findings) are invalid implicit type conversion, improper validation of create2 return value, strict equality check, freezing governance functions when malicious signer votes when vote count is one less than threshold etc. Additionally, there are numerous instances across the codebase where a caller's ETH can get locked. Other than this, the developer team has done a good job with handling storage variables (For example, having storage structs, enums and mappings in their own separate files, updating boolean and uint256 Voting struct members in the onlySigners() modifier correctly etc).
There seem to be several centralization points in the protocol, which I believe decrease the decentralisation in an interoperable network. They are here as follows:
If any of these centralized entities have leaked private keys, it can destabilize and halt the protocol since they have control over important settings of the protocol like flow in and flow out limits, the address of the liquidity pool, minting and burning tokens etc. A multi-sig should be used for configuration operations like these in order to stay one step away from losing control over the protocol.
To understand the mechanism of this protocol, we need to look at the functionalities in scope: a. Interchain Governance b. Interchain Token Service c. Helper utility contracts involved in facilitating a and b as well as to upgrade certain contracts (I will not be diving deeper into this separately but will refer to the contracts wherever necessary).
Here is a mindmap to get an extremely high level overview of this protocol with the above mentioned functionalities in scope: https://drive.google.com/file/d/1YEMRzE9MChjO1_6l-ru1RUhUzhuJqxyn/view?usp=sharing
Here is a mindmap that explains the inner workings and important function calls of the Interchain Governance mechanism. I'll be referring to this while explaining: https://drive.google.com/file/d/1ocbESssCLWZi4Xtll-GrM41xWzdrCng_/view?usp=sharing
The diagram provides a high-level overview of how the main contracts are connected. The heaviest contracts (functionality-wise) involved are InterchainGovernance.sol, MultisigBase.sol, InterchainProposalSender.sol and InterchainProposalExecutor.sol. The rest of the blue box contracts (refer to the legend in diagram for more info) are either helper contracts or contracts inheriting from these heavier contracts to extend functionality. To simply explain the whole process:
InterchainGovernance.sol contract can create, cancel and execute proposals. a. Creating and cancelling of proposals happens through specific commandIds, which are basically their index in the enum they are stored in. To create and cancel, you can pass in the respective commandId (0 for create, 1 for cancel) as parameter value to the _execute() function. b. Execution of the proposal happens through the executeProposal() function, which finalizes the timelock (i.e. sets the hash of the proposal to 0) and executes the proposal using the low-level call in the Caller.sol contract. Note, the proposals have a minimum delay before they are ready to execute. This minimum delay is added by the Timelock.sol contract while scheduling the proposal.
AxelarServiceGovernance.sol - Does everything InterchainGovernance.sol can do, the only addition being execution of multisig proposals, which are handled by the signers (rotated every epoch) set in the the MultisigBase.sol contract.
Multisig.sol - Inherits from MultisigBase.sol and allows signers to execute any function in any target contract.
InterchainProposalSender.sol and InterchainProposalExecutor.sol play hand-in-hand in the sending and execution of proposals. The proposal sender is a contract on the source chain, which communicates with the proposal executor on the destination chain through the AxelarGateway.sol contract. Note in order for execution to occur on the destination chain, the source caller and source address need to be whitelisted by the owner.
InterchainCalls.sol - This is a contract that stores the struct definition for interchain calls, which includes destination chain specific info and calls to be executed. It is used in the proposal sender and executor contracts.
Here is a mindmap that explains the inner workings and important function calls of the Interchain Token Service. I'll be referring to this while explaining: https://drive.google.com/file/d/1AQI1QwuYzXASMtsqL6VfIGoQza0Fygv_/view?usp=sharing
The diagram provides a high-level overview of how the main functions communicate. Refer to the legend to understand what each color represents. The way to read this mindmap would be to start from the top left corner.
The first contract we come across is the InterchainTokenService.sol contract. This is by far the heaviest contract (both functionality-wise and nSLOC-wise). The first level in the hierarchy (side-ways) are the callable functions in the contract. Each function either directly or indirectly (through internal functions - level 2 in side-ways hierarchy) deploys contracts on both source and destination chains. What differentiates the contracts deployed on the source and destination chains is the "remote" keyword used in the function names. The function names and comments provided by the team are self-explanatory so I will not be delving much deeper into explaining what each function does.
InterchainToken.sol and TokenManager.sol are both the originators of interchain transfer calls (since the functions in both contracts are external, thus call can originate from either). The InterchainToken.sol calls the TokenManager.sol, which further calls the InterchainTokenService.sol contract. Here is how the flow looks:
InterchainToken.sol => TokenManager.sol => InterchainTokenService.sol (function transmitSendToken()) => AxelarGateway.sol (function callContract())
They both call the transmitSendToken() function to directly/indirectly (check disgram) to emit an event in the callContract function in the AxelarGateway.sol contract. The execution on the destination chain follows as mentioned by this example provided in the third paragraph of the Overview section in the README.md file: https://github.com/code-423n4/2023-07-axelar#overview
RemoteAddressValidator.sol - The main function of this contract is to add/remove trusted interchain token service addresses and add/remove Axelar supported gateway chains. This contract also provides a function, which can be used to validate if the sender is a valid interchain token service address.
StnadardizedToken.sol - Inherits from InterchainToken.sol to allow for interchain transfer (although this can be considered as originator of interchain transfer as well, I have not marked it in the diagram). This contract provides a setup function (Callable by onlyProxy) that sets up certain parameters like the distributor, tokenManager etc. Additionally, the distributor can use the token minting and burning functions.
TokenAddressStorage.sol - This can be considered as originator of interchain transfer since it inherits from the TokenManager.sol contract (not marked in diagram though). It has a setter function to set the token address as well. Although this contract is not important by itself, it is used by its child contracts - TokenManagerLockUnlock.sol, TokenManagerMintBurn.sol and TokenManagerLiqudiityPool.sol.
TokenManagerLockUnlock.sol, TokenManagerMintBurn.sol and TokenManagerLiqudiityPool.sol - These are different implementation types of the token manager and are differentiated by their implementation number (i.e. their index in the TokenManagerType enum located in the ITokenManagerTypes interface). Additionally, each contract has their own giveToken() and takeToken() functions that interact with the FlowLimit.sol contract.
FlowLimit.sol - This contract makes sure that while giving and taking tokens, the flow limit set by the operator is adhered to in order to prevent exploit attacks.
90 hours
#0 - c4-judge
2023-09-04T10:54:00Z
berndartmueller marked the issue as grade-b