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: 3/47
Findings: 6
Award: $9,509.32
π Selected for report: 4
π Solo Findings: 0
π Selected for report: Jeiwan
Also found by: nobody2018
7066.8012 USDC - $7,066.80
https://github.com/code-423n4/2023-07-axelar/blob/2f9b234bb8222d5fbe934beafede56bfb4522641/contracts/its/token-manager/implementations/TokenManagerLiquidityPool.sol#L82-L85 https://github.com/code-423n4/2023-07-axelar/blob/2f9b234bb8222d5fbe934beafede56bfb4522641/contracts/its/token-manager/implementations/TokenManagerLockUnlock.sol#L48-L51
A malicious actor can trick a TokenManager
into thinking that a bigger amount of tokens was transferred. On the destination chain, the malicious actor will be able to receive more tokens than they sent on the source chain.
TokenManagerLockUnlock and TokenManagerLiquidityPool are TokenManager
implementations that transfer tokens from/to users when sending tokens cross-chain. The low-level _takeToken
function (TokenManagerLiquidityPool._takeToken, TokenManagerLockUnlock._takeToken) is used to take tokens from a user on the source chain before emitting a cross-chain message, e.g. via the TokenManager.sendToken function. The function computes the difference in the balance of the liquidity pool or the token manager before and after the transfer, to track the actual amount of tokens transferred. The amount is then passed in the cross-chain message to tell the InterchainTokenService
contract on the destination chain how much tokens to give to the recipient.
The _takeToken
function, however, is not protected from re-entrancies, which opens up the following attack scenario:
_takeToken
function calls transferFrom
on the ERC777 token contract, which calls the tokensToSend hook on the malicious contract (the sender).TokenManager.sendToken
and sends 100 more tokens._takeToken
call, the balance change will equal 100 since, in ERC777, the balance state is updated only after the tokensToSend
hook, so only the re-entered token transfer will be counted.TokenManager.sendToken
will result in 100 tokens transferred cross-chain._takeToken
call, the balance change will equal 200 because the balance of the receiver will increase twice during the transferFrom
call: once for the first call and once for the re-entered call.TokenManager
contract will emit two cross-chain messages: one will transfer 100 tokens (the re-entered call) and the other will transfer 200 tokens (the first call). This will let the malicious actor to receive 300 tokens on the destination chain, while spending only 200 tokens on the source chain.Since the protocol is expected to support different implementations of ERC20 tokens, including custom ones, the attack scenario is valid for any token implementation that uses hooks during transfers.
Manual review
Consider adding a re-entrancy protection to the TokenManagerLiquidityPool._takeToken
and TokenManagerLockUnlock._takeToken
functions, for example by using the ReentrancyGuard from OpenZeppelin.
Reentrancy
#0 - c4-pre-sort
2023-07-29T00:35:42Z
0xSorryNotSorry marked the issue as primary issue
#1 - c4-sponsor
2023-08-25T17:57:05Z
deanamiel marked the issue as sponsor confirmed
#2 - deanamiel
2023-08-25T17:58:22Z
We have added a separate token manager for fee on transfer tokens, which is protected from reentrancy. Here is a link to the public PR: https://github.com/axelarnetwork/interchain-token-service/pull/96
#3 - c4-judge
2023-09-01T10:30:30Z
berndartmueller marked the issue as selected for report
1272.0242 USDC - $1,272.02
The validateSender
and addTrustedAddress
functions of RemoteAddressValidator
can incorrectly handle the passed address arguments, which will result in false negatives. E.g. a valid sender address may be invalidated.
The RemoteAddressValidator._lowerCase function is used to convert an address to lower case. Since the protocol is expected to support different EVM and non-EVM chains, account addresses may have different format, thus the necessity to convert them to strings and to convert the strings to lower case when comparing them. However, the function only converts the hexadecimal letters, i.e. the characters in ranges A-F:
if ((b >= 65) && (b <= 70)) bytes(s)[i] = bytes1(b + uint8(32));
Here, 65
corresponds to A
, and 70
corresponds to F
. But, since different EVM and non-EVM chains are supported, addresses can contain other characters. For example, Cosmos uses bech32 addresses and Evmos supports both hexadecimal and bech32 addresses.
If not all alphabetical characters of an address are converted to lower case, then the address comparison in the validateSender can fail and result in a false revert.
Manual review
In the _lowerCase
function, consider converting all alphabetical characters to lower case, e.g.:
diff --git a/contracts/its/remote-address-validator/RemoteAddressValidator.sol b/contracts/its/remote-address-validator/RemoteAddressValidator.sol index bb101e5..e83431b 100644 --- a/contracts/its/remote-address-validator/RemoteAddressValidator.sol +++ b/contracts/its/remote-address-validator/RemoteAddressValidator.sol @@ -55,7 +55,7 @@ contract RemoteAddressValidator is IRemoteAddressValidator, Upgradable { uint256 length = bytes(s).length; for (uint256 i; i < length; i++) { uint8 b = uint8(bytes(s)[i]); - if ((b >= 65) && (b <= 70)) bytes(s)[i] = bytes1(b + uint8(32)); + if ((b >= 65) && (b <= 90)) bytes(s)[i] = bytes1(b + uint8(32)); } return s; }
Other
#0 - c4-pre-sort
2023-07-29T00:12:04Z
0xSorryNotSorry marked the issue as primary issue
#1 - c4-sponsor
2023-08-28T15:54:13Z
deanamiel marked the issue as disagree with severity
#2 - deanamiel
2023-08-28T15:56:12Z
Corrected Severity: QA This was originally meant to cover the EVM addresses, but we implemented a fix to account for non-EVM addresses as well. Public PR link: https://github.com/axelarnetwork/interchain-token-service/pull/96
#3 - berndartmueller
2023-09-01T12:05:05Z
I'm maintaining the medium severity for this issue as it prevents using any non-EVM addresses.
#4 - c4-judge
2023-09-01T12:07:10Z
berndartmueller marked the issue as selected for report
#5 - milapsheth
2023-11-08T12:07:45Z
We consider this finding QA or Low severity since the scope of the implementation is for EVM chains (even though Axelar's cross-chain messaging API is generic). Non EVM chains require further consideration that wasn't the focus for this version.
π Selected for report: Jeiwan
Also found by: 0xkazim, Emmanuel, KrisApostolov, T1MOH, Toshii, UniversalCrypto, Viktor_Cortess, immeas, libratus, nobody2018, qpzm
123.2021 USDC - $123.20
https://github.com/code-423n4/2023-07-axelar/blob/2f9b234bb8222d5fbe934beafede56bfb4522641/contracts/interchain-governance-executor/InterchainProposalExecutor.sol#L41 https://github.com/code-423n4/2023-07-axelar/blob/2f9b234bb8222d5fbe934beafede56bfb4522641/contracts/interchain-governance-executor/InterchainProposalExecutor.sol#L76 https://github.com/code-423n4/2023-07-axelar/blob/2f9b234bb8222d5fbe934beafede56bfb4522641/contracts/gmp-sdk/executable/AxelarExecutable.sol#L17
Proposals that require sending native coins in at least one of their calls cannot be executed.
The InterchainProposalExecutor._execute executes cross-chain governance proposals. The function extracts the list of calls to make under the proposal and calls _executeProposal
. _executeProposal
, in its turn, makes distinct calls and sends native coins along with each call as specified by the call.value
argument:
(bool success, bytes memory result) = call.target.call{ value: call.value }(call.callData);
However, InterchainProposalExecutor._execute
is called from a non-payable function, AxelarExecutable.execute, and thus native coins cannot be passed in the call. As a result, proposal calls that have the value
argument greater than 0 cannot be executed.
Sending native funds to the contract in advance cannot be a solution because such funds can be stolen by back running and executing a proposal that would consume them.
Manual review
Consider implementing an alternative AxelarExecutable
contract (i.e. AxelarExecutablePayable
) that has the execute
function payable and consider inheriting in InterchainProposalExecutor
from this alternative implementation, not from AxelarExecutable
.
Payable
#0 - c4-pre-sort
2023-07-29T00:01:56Z
0xSorryNotSorry marked the issue as primary issue
#1 - c4-sponsor
2023-08-15T07:26:55Z
deanamiel marked the issue as sponsor disputed
#2 - deanamiel
2023-08-15T07:28:20Z
The intention is for the contract that executes the proposal to have been already funded with native value. Native value is not meant to be sent with the call to AxelarExecutable.execute().
#3 - berndartmueller
2023-09-01T12:15:42Z
The InterchainProposalExecutor
contract, in line 20, states that this contract is abstract. The only derived contract in the repository is the TestProposalExecutor
intended for testing purposes. Can you show the concrete implementation for such a derived contract that is going to be deployed? @deanamiel
Given the code in scope, there would not be a receive
function to be able to fund the contract with native tokens. This would render this submission valid.
#4 - deanamiel
2023-09-06T22:46:52Z
So it would be the contract that inherits AxelarExecutable
that would need to be funded with native value for proposal execution. If we look at InterchainGovernance
as an example, this contract does contain a receive function here. Does this answer your question?
#5 - berndartmueller
2023-09-08T10:55:28Z
So it would be the contract that inherits
AxelarExecutable
that would need to be funded with native value for proposal execution. If we look atInterchainGovernance
as an example, this contract does contain a receive function here. Does this answer your question?
The issue is that the InterchainProposalExecutor
contract can not be funded with native funds, but attempts to execute proposals (target contracts) that potentially require native funds (see L76). @deanamiel
I'm inclined to leave this submission as medium severity, as it does not allow using the InterchainProposalExecutor
contract in conjunction with proposals that require native funds.
#6 - c4-judge
2023-09-08T10:58:54Z
berndartmueller marked the issue as satisfactory
#7 - c4-judge
2023-09-08T10:58:58Z
berndartmueller marked the issue as selected for report
#8 - milapsheth
2023-11-08T12:16:54Z
We acknowledge the severity. The solution is to add a receive function as mentioned in #344, since the design expects to fund tokens to the contract and then execute. This funding + execution can be done within a contract atomically if there's a concern of another proposal stealing tokens.
π Selected for report: Jeiwan
Also found by: Toshii, immeas, pcarranzav
858.6163 USDC - $858.62
In a case when gas fees are refunded for a token transfer made via the InterchainToken.interchainTransferFrom
function, the fees will be refunded to the owner of the tokens, not the address that actually paid the fees. As a result, the sender will lose the fees paid for the cross-chain transaction and will not receive tokens on the other chain; the owner of the token will have their tokens and will receive the fees.
The InterchainToken.interchainTransferFrom function is used to transfer tokens cross-chain. The function is identical to the ERC20.transferFrom
function: an approved address can send someone else's tokens to another chain. Since this is a cross-chain transaction, the sender also has to pay the additional gas fee for executing the transaction:
tokenManager.transmitInterchainTransfer
calls interchainTokenService.transmitSendToken;interchainTokenService.transmitSendToken
calls _callContract;_callContract
uses the msg.value
to pay the extra gas fees.Notice that the gasService.payNativeGasForContractCall
call in _callContract
takes the refundTo
address, i.e. the address that will receive refunded gas fee. If we return up on the call stack, we'll see that the refund address is the sender address that's passed to the tokenManager.transmitInterchainTransfer
call. Thus, gas fees will be refunded to the token owner, not the caller, however it's the caller who pays them.
Manual review
The TokenManager.transmitInterchainTransfer
and the InterchainTokenService.transmitSendToken
functions, besides taking the sender
/sourceAddress
argument, need also take the "refund to" address. In the InterchainToken.interchainTransferFrom
function, the two argument will be set to different addresses: the sender
/sourceAddress
argument will be set to the token owner address; the new "refund to" argument will be set to msg.sender
. Thus, while tokens will be taken from their owner, the cross-chain gas fees will be refunded to the actual transaction sender.
ETH-Transfer
#0 - c4-pre-sort
2023-07-29T00:21:02Z
0xSorryNotSorry marked the issue as primary issue
#1 - c4-sponsor
2023-08-25T18:00:50Z
deanamiel marked the issue as disagree with severity
#2 - deanamiel
2023-08-25T18:09:02Z
Corrected Severity: QA In most cases it will have been called by the sender anyway, so having the sender be refunded is the desired effect. Sometimes this will not be the case though, depending on the use case. Therefore, we have added a parameter to keep track of where the funds need to be refunded. Link to public PR: https://github.com/axelarnetwork/interchain-token-service/pull/96
#3 - berndartmueller
2023-09-01T10:35:47Z
Considering this medium severity as per "...In most cases it will have been called by the sender anyway" (sponsors statement above). Nevertheless, in the cases where an approved address transfers the tokens, the gas is incorrectly refunded.
#4 - c4-judge
2023-09-01T10:35:57Z
berndartmueller changed the severity to 2 (Med Risk)
#5 - c4-judge
2023-09-01T10:36:08Z
berndartmueller marked the issue as selected for report
#6 - milapsheth
2023-11-08T12:20:48Z
We acknowledge the severity
π 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
[L-01] FinalProxy.finalUpgrade
doesn't verify the contract ID of the implementation
InterchainTokenService
can be upgraded to an incorrect implementation irrevocably, breaking the core contract of the protocol.
The FinalProxy.finalUpgrade sets the final implementation of the proxy, without allowing to change it later. The proxy contract is meant to work with the contracts of the protocol, and each such contract implements the contractId
function (e.g. InterchainTokenService.contractId) that returns the unique ID of the contract. The Upgradable contract (that's extensively used by the proxy contracts of the protocol) checks the contractId
of a new implementation, however FinalProxy.finalUpgrade
doesn't do that.
In the protocol, InterchainTokenServiceProxy is the only proxy that inherits from the FinalProxy
contract. This means that if InterchainTokenService
is upgraded to an incorrect implementation, it'll break the entire cross-chain token management functionality of the protocol.
Manual review
Consider checking the contract ID of the final implementation in the FinalProxy.finalUpgrade
function, as it's done in Upgradable.upgrade
.
#0 - deanamiel
2023-08-31T00:05:29Z
We added a contract ID check for final proxy and fixed proxy. See PR here
#1 - c4-judge
2023-09-08T11:33:20Z
berndartmueller marked the issue as grade-b
π Selected for report: pcarranzav
Also found by: Jeiwan, K42, MrPotatoMagic, Sathish9098, libratus, niloy
The audited protocol represents a cross-chain bridge that facilitates the interaction between different EVM and non-EVM chains. While the core functionality of the bridge, namely the AxelarGateway
contract, was out of scope of the audit, the in-scope contracts can be grouped as follows:
cgp/governance/
implement a cross-chain governance solution that allows to create, deliver, and execute proposals on different chains. The core contract AxelarServiceGovernance
allows a group of signers to create and manager proposals in a coordinated way using a multisig mechanism.gmp-sdk/deploy
and gmp-sdk/upgradable
are auxiliary contracts that are extensively used by the core contracts. The Create3Deployer
contract implements the CREATE3 solution that's used to deploy all the core contracts of the protocol. The set of proxy contracts is used by the core contracts of the protocol to cut the gas cost of deploying contract for the users of the protocol.interchain-governance-executor
are meant to send (the InterchainProposalSender
contract) and execute (the InterchainProposalExecutor
contract) cross-chain governance proposals. The set of contracts is tightly integrated with the governance module.its
("Interchain Token Service") form a big mechanism of deploying, using, and transferring cross-chain tokens. The contracts allow to deploy a token manager for any existing or a new token, which facilitates cross-chain transferring of tokens. At the core of the module is the InterchainTokenService
contract that's responsible for deploying standardized tokens and token manager on the current and remote chains.The four describe modules are tightly integrated with the AxelarGateway
βthey call the contract to facilitate all cross-chain operations. For a bridge protocol, it's crucial to guarantee that a cross-chain message can only created and sent by an authentic sender. The protocol achieves this by letting recipients check the source chain and the source address of all messages they receive.
Since the protocol implements a mechanism of deploying and transferring of ERC20 tokens and custom token implementations, it's also critical to guarantee the connection between one token deployed to multiple chains. The protocol achieves this by creating a unique token ID for each token: the mechanism of deploying tokens to other chains guarantees that the token on the other chains will have the same token ID.
The codebase follows the best practices, while implementing such complex techniques as CREATE3 deployments and advanced usage of proxy contracts. Specifically, the protocol ensures unique and deterministic addresses for all contracts deployed by users.
The protocol has low centralization risks. All user-deployed contracts are either managed by the user who deploys them (e.g. the operator role in a token manager and the distributor role in a standardized token are set by the deployer) or have no manager at all (e.g. token managers deployed for canonical tokens). The InterchainTokenService
(the core contract of the Interchain Token Service) has two trusted roles:
The audit of the protocol was performed using the line-by-line method, followed by a higher-level checking of the codebase against the documentation.
30 hours
#0 - c4-judge
2023-09-04T10:45:56Z
berndartmueller marked the issue as grade-b