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: 9/47
Findings: 3
Award: $1,410.12
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: Jeiwan
Also found by: 0xkazim, Emmanuel, KrisApostolov, T1MOH, Toshii, UniversalCrypto, Viktor_Cortess, immeas, libratus, nobody2018, qpzm
94.7708 USDC - $94.77
Any payable proposal can't be executed
It passes call.value
into target call. However InterchainProposalExecutor.sol doesn't have payable functions therefore can't receive ether to execute payable proposal.
/** * @dev Executes the proposal. Calls each target with the respective value, signature, and data. * @param calls The calls to execute. */ function _executeProposal(InterchainCalls.Call[] memory calls) internal { for (uint256 i = 0; i < calls.length; i++) { InterchainCalls.Call memory call = calls[i]; (bool success, bytes memory result) = call.target.call{ value: call.value }(call.callData); if (!success) { _onTargetExecutionFailed(call, result); } else { _onTargetExecuted(call, result); } } }
Manual Review
Add receive() payable
function as you did in Multisig.sol
call/delegatecall
#0 - c4-pre-sort
2023-07-29T00:04:24Z
0xSorryNotSorry marked the issue as duplicate of #319
#1 - c4-judge
2023-09-08T10:59:45Z
berndartmueller marked the issue as satisfactory
🌟 Selected for report: T1MOH
Also found by: Chom, UniversalCrypto
1272.0242 USDC - $1,272.02
Axelar is supposed to support different chains, not only EVM. And this chains can have different address standard like Polkadot, Tron. This addresses can't be whitelisted in InterchainProposalExecutor.sol to execute proposal. Thus InterchainProposalSender implementation from non-EMV chain can't interact with InterchainProposalExecutor.sol on EVM chain.
Here you can see that sourceAddress is represented as address
, not string
:
// Whitelisted proposal callers. The proposal caller is the contract that calls the `InterchainProposalSender` at the source chain. mapping(string => mapping(address => bool)) public whitelistedCallers; // Whitelisted proposal senders. The proposal sender is the `InterchainProposalSender` contract address at the source chain. mapping(string => mapping(address => bool)) public whitelistedSenders; ... /** * @dev Set the proposal caller whitelist status * @param sourceChain The source chain * @param sourceCaller The source caller * @param whitelisted The whitelist status */ function setWhitelistedProposalCaller( string calldata sourceChain, address sourceCaller, bool whitelisted ) external override onlyOwner { whitelistedCallers[sourceChain][sourceCaller] = whitelisted; emit WhitelistedProposalCallerSet(sourceChain, sourceCaller, whitelisted); } /** * @dev Set the proposal sender whitelist status * @param sourceChain The source chain * @param sourceSender The source sender * @param whitelisted The whitelist status */ function setWhitelistedProposalSender( string calldata sourceChain, address sourceSender, bool whitelisted ) external override onlyOwner { whitelistedSenders[sourceChain][sourceSender] = whitelisted; emit WhitelistedProposalSenderSet(sourceChain, sourceSender, whitelisted); }
Manual Review
Don't convert sourceAddress to address
, use string
instead
// Whitelisted proposal callers. The proposal caller is the contract that calls the `InterchainProposalSender` at the source chain. - mapping(string => mapping(address => bool)) public whitelistedCallers; + mapping(string => mapping(string => bool)) public whitelistedCallers; // Whitelisted proposal senders. The proposal sender is the `InterchainProposalSender` contract address at the source chain. - mapping(string => mapping(address => bool)) public whitelistedSenders; + mapping(string => mapping(string => bool)) public whitelistedSenders;
Invalid Validation
#0 - c4-pre-sort
2023-07-29T00:47:49Z
0xSorryNotSorry marked the issue as primary issue
#1 - c4-sponsor
2023-08-28T16:01:36Z
deanamiel marked the issue as sponsor confirmed
#2 - deanamiel
2023-08-28T16:02:44Z
Support has been added for non-EVM addresses. Public PR links: https://github.com/axelarnetwork/interchain-governance-executor/pull/21 https://github.com/axelarnetwork/interchain-governance-executor/pull/33
#3 - c4-judge
2023-09-02T10:42:14Z
berndartmueller marked the issue as selected for report
🌟 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
https://github.com/code-423n4/2023-07-axelar/blob/2f9b234bb8222d5fbe934beafede56bfb4522641/contracts/cgp/util/Upgradable.sol#L11 https://github.com/code-423n4/2023-07-axelar/blob/2f9b234bb8222d5fbe934beafede56bfb4522641/contracts/gmp-sdk/upgradable/BaseProxy.sol#L16 This is current value
// keccak256('owner') bytes32 internal constant _OWNER_SLOT = 0x02016836a56b71f0d02689e69e326f4f4c1b9057164ef592671cf0d37c8040c0;
According to EIP1967 it should be
Storage slot 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103 (obtained as bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1)).
Modifier onlySigners()
after voting executes action if there are enough votes. Note that if votes not enough, it just returns without execution:
https://github.com/code-423n4/2023-07-axelar/blob/2f9b234bb8222d5fbe934beafede56bfb4522641/contracts/cgp/auth/MultisigBase.sol#L39-L77
modifier onlySigners() { if (!signers.isSigner[msg.sender]) revert NotSigner(); bytes32 topic = keccak256(msg.data); Voting storage voting = votingPerTopic[signerEpoch][topic]; // Check that signer has not voted, then record that they have voted. if (voting.hasVoted[msg.sender]) revert AlreadyVoted(); voting.hasVoted[msg.sender] = true; // Determine the new vote count. uint256 voteCount = voting.voteCount + 1; // Do not proceed with operation execution if insufficient votes. if (voteCount < signers.threshold) { // Save updated vote count. voting.voteCount = voteCount; return; } // Clear vote count and voted booleans. voting.voteCount = 0; uint256 count = signers.accounts.length; for (uint256 i; i < count; ++i) { voting.hasVoted[signers.accounts[i]] = false; } emit MultisigOperationExecuted(topic); _; }
However some actions can require sending msg.value: https://github.com/code-423n4/2023-07-axelar/blob/2f9b234bb8222d5fbe934beafede56bfb4522641/contracts/cgp/governance/AxelarServiceGovernance.sol#L52
function executeMultisigProposal( address target, bytes calldata callData, uint256 nativeValue ) external payable onlySigners { ... _call(target, callData, nativeValue); emit MultisigExecuted(proposalHash, target, callData, nativeValue); }
Imagine 2 concurrent transactions to execute payable action. First transaction executes and all votes are cleared. Then second transaction is just 1st to vote on a new action, and sent msg.value is stuck in MultisigBase.sol. However it can be resqued via another action
Keep mapping for such accidental ether, and add claim function:
mapping(address => uint256) public pendingAmounts;
modifier onlySigners() { ... // Do not proceed with operation execution if insufficient votes. if (voteCount < signers.threshold) { // Save updated vote count. voting.voteCount = voteCount; pendingAmounts[msg.sender] += msg.value; return; } ... _; }
function claim() external { uint256 amount = pendingAmounts[msg.sender]; require(amount != 0); pendingAmounts[msg.sender] = 0; payable(msg.sender).call{value: amount}(""); }
But want to notice that in this case external call to signer will be performed, potentially it is dangerous
Final implementation address is not stored, but computed via CREATE3. But according to EIP1967 implementation must be stored in slot bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)
function implementation() public view override(BaseProxy, IProxy) returns (address implementation_) { implementation_ = _finalImplementation(); if (implementation_ == address(0)) { implementation_ = super.implementation(); } } function _finalImplementation() internal view virtual returns (address implementation_) { /** * @dev Computing the address is cheaper than using storage */ implementation_ = Create3.deployedAddress(address(this), FINAL_IMPLEMENTATION_SALT); if (implementation_.code.length == 0) implementation_ = address(0); }
Use storage instead of computing address. Store implementation always in slot 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc. And store boolean flag isFinal
for remembering that final upgrade was performed
Function selector TokenManagerProxy.setup.selector
is used for setup of TokenManager. Now they are identical, but it can be different in future. Because setupping contract is TokenManager.sol and therefore selector TokenManager.setup.selector
should be used
constructor( address interchainTokenServiceAddress_, uint256 implementationType_, bytes32 tokenId_, bytes memory params ) { interchainTokenServiceAddress = IInterchainTokenService(interchainTokenServiceAddress_); implementationType = implementationType_; tokenId = tokenId_; address impl = _getImplementation(IInterchainTokenService(interchainTokenServiceAddress_), implementationType_); (bool success, ) = impl.delegatecall(abi.encodeWithSelector(TokenManagerProxy.setup.selector, params)); if (!success) revert SetupFailed(); }
Refactor
- (bool success, ) = impl.delegatecall(abi.encodeWithSelector(TokenManagerProxy.setup.selector, params)); + (bool success, ) = impl.delegatecall(abi.encodeWithSelector(TokenManager.setup.selector, params));
getSignerVotesCount()
logic can be much simplerfunction getSignerVotesCount(bytes32 topic) external view override returns (uint256) { - uint256 length = signers.accounts.length; - uint256 voteCount; - for (uint256 i; i < length; ++i) { - if (votingPerTopic[signerEpoch][topic].hasVoted[signers.accounts[i]]) { - voteCount++; - } - } - return voteCount; + return votingPerTopic[signerEpoch][topic].voteCount; }
#0 - c4-judge
2023-09-08T11:24:14Z
berndartmueller marked the issue as grade-b