Axelar Network - Jeiwan's results

Decentralized interoperability network.

General Information

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

Axelar Network

Findings Distribution

Researcher Performance

Rank: 3/47

Findings: 6

Award: $9,509.32

QA:
grade-b
Analysis:
grade-b

🌟 Selected for report: 4

πŸš€ Solo Findings: 0

Findings Information

🌟 Selected for report: Jeiwan

Also found by: nobody2018

Labels

bug
3 (High Risk)
primary issue
selected for report
sponsor confirmed
H-02

Awards

7066.8012 USDC - $7,066.80

External Links

Lines of code

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

Vulnerability details

Impact

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.

Proof of Concept

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:

  1. A malicious contract initiates transferring of 100 ERC777 tokens by calling TokenManager.sendToken.
  2. The _takeToken function calls transferFrom on the ERC777 token contract, which calls the tokensToSend hook on the malicious contract (the sender).
  3. In the hook, the malicious contract makes another call to TokenManager.sendToken and sends 100 more tokens.
  4. In the nested _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.
  5. The re-entered call to TokenManager.sendToken will result in 100 tokens transferred cross-chain.
  6. In the first _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.
  7. As a result, the malicious contract will transfer 100+100 = 200 tokens, but the 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.

Tools Used

Manual review

Consider adding a re-entrancy protection to the TokenManagerLiquidityPool._takeToken and TokenManagerLockUnlock._takeToken functions, for example by using the ReentrancyGuard from OpenZeppelin.

Assessed type

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

Findings Information

🌟 Selected for report: Jeiwan

Also found by: Chom, Shubham

Labels

bug
2 (Med Risk)
disagree with severity
primary issue
selected for report
M-03

Awards

1272.0242 USDC - $1,272.02

External Links

Lines of code

https://github.com/code-423n4/2023-07-axelar/blob/2f9b234bb8222d5fbe934beafede56bfb4522641/contracts/its/remote-address-validator/RemoteAddressValidator.sol#L58

Vulnerability details

Impact

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.

Proof of Concept

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.

Tools Used

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;
     }

Assessed type

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.

Findings Information

Labels

bug
2 (Med Risk)
primary issue
satisfactory
selected for report
sponsor disputed
M-04

Awards

123.2021 USDC - $123.20

External Links

Lines of code

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

Vulnerability details

Impact

Proposals that require sending native coins in at least one of their calls cannot be executed.

Proof of Concept

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.

Tools Used

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.

Assessed type

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 at InterchainGovernance 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.

Findings Information

🌟 Selected for report: Jeiwan

Also found by: Toshii, immeas, pcarranzav

Labels

bug
2 (Med Risk)
disagree with severity
downgraded by judge
primary issue
selected for report
M-05

Awards

858.6163 USDC - $858.62

External Links

Lines of code

https://github.com/code-423n4/2023-07-axelar/blob/2f9b234bb8222d5fbe934beafede56bfb4522641/contracts/its/interchain-token/InterchainToken.sol#L104

Vulnerability details

Impact

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.

Proof of Concept

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:

  1. the function calls tokenManager.transmitInterchainTransfer;
  2. tokenManager.transmitInterchainTransfer calls interchainTokenService.transmitSendToken;
  3. interchainTokenService.transmitSendToken calls _callContract;
  4. _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.

Tools Used

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.

Assessed type

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

Findings Information

Labels

bug
grade-b
QA (Quality Assurance)
Q-06

Awards

43.3267 USDC - $43.33

External Links

[L-01] FinalProxy.finalUpgrade doesn't verify the contract ID of the implementation

Targets

https://github.com/code-423n4/2023-07-axelar/blob/2f9b234bb8222d5fbe934beafede56bfb4522641/contracts/gmp-sdk/upgradable/FinalProxy.sol#L79

Impact

InterchainTokenService can be upgraded to an incorrect implementation irrevocably, breaking the core contract of the protocol.

Proof of Concept

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.

Tools Used

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

Findings Information

🌟 Selected for report: pcarranzav

Also found by: Jeiwan, K42, MrPotatoMagic, Sathish9098, libratus, niloy

Labels

analysis-advanced
grade-b
A-05

Awards

145.3502 USDC - $145.35

External Links

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:

  1. Contracts in 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.
  2. Contracts in 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.
  3. Contracts in 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.
  4. Contracts in 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:

  1. the owner is able to upgrade the contract to a new implementation;
  2. the operator is able to set the flow limit for the contract: the flow limit allows to limit the amount of tokens that are transferred cross-chain via the contract.

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.

Time spent:

30 hours

#0 - c4-judge

2023-09-04T10:45:56Z

berndartmueller marked the issue as grade-b

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax Β© 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter