Axelar Network contest - rayn's results

Decentralized interoperability network.

General Information

Platform: Code4rena

Start Date: 07/04/2022

Pot Size: $50,000 USDC

Total HM: 5

Participants: 19

Period: 5 days

Judge: 0xean

Total Solo HM: 4

Id: 109

League: COSMOS

Axelar Network

Findings Distribution

Researcher Performance

Rank: 4/19

Findings: 3

Award: $7,056.43

🌟 Selected for report: 1

πŸš€ Solo Findings: 1

Findings Information

🌟 Selected for report: rayn

Labels

bug
2 (Med Risk)
disagree with severity
sponsor acknowledged

Awards

6071.4286 USDC - $6,071.43

External Links

Lines of code

https://github.com/code-423n4/2022-04-axelar/blob/main/src/AxelarGatewayMultisig.sol#L484 https://github.com/code-423n4/2022-04-axelar/blob/main/src/AxelarGatewayMultisig.sol#L490 https://github.com/code-423n4/2022-04-axelar/blob/main/src/AxelarGatewayMultisig.sol#L529

Vulnerability details

Impact

Since this is important, we quote it again instead of referring to our other bug report on a different, yet related bug. The context within which a command is executed is extremely important.

AxelarGatewayMultisig.execute() takes a signed batch of commands. Each command has a corresponding commandID. This is guaranteed to be unique from the Axelar network. execute intentionally allows retrying a commandID if the command failed to be processed; this is because commands are state dependent, and someone might submit command 2 before command 1 causing it to fail.

Thus if an attacker manages to rearrange execution order of commands within a batch, it should probably be treated seriously. This is exactly what might happen here due to reentrancy. A malicious player that managed to gain reentrancy over execute can easily execute later commands in a batch before earlier commands are fully executed, effectively breaking all assumptions on command executed context.

Proof of Concept

The _execute function and its wrapper execute are both reentrant.

function execute(bytes calldata input) external override; function _execute(bytes memory data, bytes[] memory signatures) internal;

Thus if an attacker manages to reenter the _execute function with the same batch of commands and signatures, previously successfully executed and ongoing commands will be skipped due to premature marking of the success flag.

if (isCommandExecuted(commandId)) continue; /* Ignore if duplicate commandId received */

This allows later commands to be executed before the current ongoing command is finished. The reentrant attack can be nested to perform further reordering of commands.

Generally speaking, other unrelated batches of signed commands can only be executed, but since the assumption of ordering is most likely stronger within a single batch, we focus on illustrating the single batch scenario above.

Tools Used

vim, ganache-cli

Make execute nonReentrant

Add an ever increasing nonce to signatures to prevent replay

function execute(bytes calldata input) nonReentrant external override { ... }

#0 - deluca-mike

2022-04-20T10:37:30Z

Axelar and the gateways make no concrete guarantees about command execution order, so the idea of "reordering" is moot. Currently, the only commands that call out are:

  • deploy a token (so its order is irrelevant)
  • minting a token (and thus a call to the token contract will not re-enter, since it is either one the gateway deployed, or the gateway onboarded a malicious external ERC2, which still has no effect on the other tokens)
  • burn a token (same as above)

As with the other issue, while we do acknowledge that the contestant has correctly pointed out how the gateway handles (and can re-handle) commands, this is a low-risk (or no-risk) issue, since no actual risk has been demonstrated apart from "maybe, somehow, possibly, it could be abused". We'd need at least some feasible and concrete hypothetical or example.

#1 - 0xean

2022-04-23T18:56:13Z

I am going to side with the warden on this but downgrade to medium severity. The transaction ordering is not the high risk issue, but the ability for re-entrancy and thus replay presents a risk that should be mitigated.

Looking at other recent hacks ( for example - https://rekt.news/agave-hundred-rekt/ ), new environments pose new risks for re-entrancy to appear and with the point of this protocol being to extend across many chains, added additional measures to avoid re-entrancy and the replay that could occur as a result seems like a very logical choice.

#2 - 0xean

2022-04-23T18:56:37Z

2 β€” Med: Assets not at direct risk, but the function of the protocol or its availability could be impacted, or leak value with a hypothetical attack path with stated assumptions, but external requirements.

Findings Information

🌟 Selected for report: IllIllI

Also found by: CertoraInc, Dravee, Funen, cccz, delfin454000, dirk_y, ilan, rayn, rishabh

Labels

bug
QA (Quality Assurance)

Awards

891.4944 USDC - $891.49

External Links

Summary

We list 2 low-critical findings and 4 non-critical findings here:

  • (Low) No checks against provided symbol and actual symbol
  • (Low) Implement upgrade() function in proxy rather than implementation
  • (Non) ECDSA only handles specific types of signatures
  • (Non) ERC20Permit timestamp<deadline instead of timestamp<=deadline (different from original implementation)
  • (Non) It’s better to follow common upgradeable patterns
  • (Non) Use EIP712 instead of encodeObjects

In conclusion, it's better to check the token symbol, use original implementation of ECDSA, and follow EIP712.

(Low) No checks against provided symbol and actual symbol

Impact

_deployToken function in AxelarGateway.sol doesn't check against the provided symbol and actual symbol of external tokens.

Proof of Concept

https://github.com/code-423n4/2022-04-axelar/blob/main/src/AxelarGateway.sol#L336

Check param symbol to equal the actual symbol of external token:

require(symbol == token.symbol(), "Symbol doesn't match");

(Low) Implement upgrade() function in proxy rather than implementation

Impact

The upgrade() function is implemented in AxelarGateway. If the new AxelarGateway implementation doesn't have an upgrade() function, the proxy will not be upgradable.

Proof of Concept

https://github.com/code-423n4/2022-04-axelar/blob/main/src/AxelarGateway.sol#L258

Move upgrade() to the proxy contract.

(Non) ECDSA only handles specific types of signatures

Impact

In ECDSA.sol, recover()only handles specific types of signatures. But it should be able to take care of more types.

Proof of Concept

https://github.com/code-423n4/2022-04-axelar/blob/main/src/ECDSA.sol#L31

Follow the original implementation of ECDSA.

(Non) ERC20Permit timestamp<deadline instead of timestamp<=deadline (different from original implementation)

Impact

In the original openzeppelin implementation, ERC20Permit/permit() uses block.timestamp <= deadline, but in Axelar, it uses block.timestamp < deadline. While this is harmless, we recommend developers to conform to the publicly agreed standards while reimplementing contracts.

Proof of Concept

https://github.com/code-423n4/2022-04-axelar/blob/main/src/ERC20Permit.sol#L43

Use require(block.timestamp <= deadline, 'EXPIRED'); instead

(Non) It’s better to follow common upgradeable patterns

Impact

The implementation of upgradeable contracts seems to be fine, but it is better to follow common upgradeable patterns, like using initialize instead of setup. And guard initialization functions with the initializer modifier instead of adding a dummy function in Proxy to prevent function delegation.

Proof of Concept

https://github.com/code-423n4/2022-04-axelar/blob/main/src/AxelarGateway.sol#L66

Follow common upgradeable patterns

https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable

(Non) Use EIP712 instead of encodeObjects

Impact

Current implementations require extreme careful choice between encode and encodePacked, since using encodePacked on data including multiple dynamic length fields will most likely make injections possible. We have manually reviewed and confirmed all choices present in contract are correct, but still advise developers to adopt EIP712 for robustness.

Proof of Concept

https://github.com/code-423n4/2022-04-axelar/blob/main/src/AxelarGateway.sol#L482

https://github.com/code-423n4/2022-04-axelar/blob/main/src/AxelarGateway.sol#L506

Adopt EIP712

#0 - deluca-mike

2022-04-19T18:35:37Z

No checks against provided symbol and actual symbol

Disputed. symbol in this context is an Axelar Network value, unique to Axelar's network, and need not match the symbol of the token on any specific chain. Consider axUSDConETH as the symbol for USDT on Ethereum's chain, which would have the symbol USDC.

Implement upgrade() function in proxy rather than implementation

Disputed. We want the proxy to be as transparent as possible, and be able to upgrade the upgrade logic itself. "If the new AxelarGateway implementation doesn't have an upgrade() function, the proxy will not be upgradable." This is intended.

ECDSA only handles specific types of signatures

Unclear in both recommendation and in specifying what the problem is.

ERC20Permit timestamp<deadline instead of timestamp<=deadline (different from original implementation)

Confirmed.

It’s better to follow common upgradeable patterns

Acknowledged, and absolutely agree, but will not change since this was grandfathered in from a release already on mainnet.

Use EIP712 instead of encodeObjects

Acknowledged, but as noted, none of the encodePacked usages are vulnerable to external abuse. Also, now it is too late since existing deployed gateways already have and expect these storage slots to be set. Perhaps we will do a full migration later on if and when a much larger upgrade happens.

Findings Information

🌟 Selected for report: IllIllI

Also found by: 0v3rf10w, 0xNazgul, 0xkatana, CertoraInc, Chom, Dravee, Funen, Hawkeye, Tomio, ilan, nahnah, rayn, rfa

Labels

bug
G (Gas Optimization)

Awards

93.5105 USDC - $93.51

External Links

Save gas in for loops by unchecked arithmetic

The for loop has no overflow risk of i. Use an unchecked block to save gas.

Proof of Concept

https://github.com/code-423n4/2022-04-axelar/blob/main/src/AxelarGatewayMultisig.sol#L118 https://github.com/code-423n4/2022-04-axelar/blob/main/src/AxelarGatewayMultisig.sol#L140 https://github.com/code-423n4/2022-04-axelar/blob/main/src/AxelarGatewayMultisig.sol#L181 https://github.com/code-423n4/2022-04-axelar/blob/main/src/AxelarGatewayMultisig.sol#L271 https://github.com/code-423n4/2022-04-axelar/blob/main/src/AxelarGatewayMultisig.sol#L293 https://github.com/code-423n4/2022-04-axelar/blob/main/src/AxelarGatewayMultisig.sol#L332 https://github.com/code-423n4/2022-04-axelar/blob/main/src/AxelarGatewayMultisig.sol#L332 https://github.com/code-423n4/2022-04-axelar/blob/main/src/AxelarGatewayMultisig.sol#L526

Recommendation

Use unchecked blocks to avoid overflow checks, or use ++i rather than i++ if you don't use unchecked blocks.

for (uint256 i = 0; i < length; ) { ... unchecked { ++i; } }

Calculate toEthSignedMessageHash once and cache

In the _execute function, it calculates toEthSignedMessageHash many times in loop

Proof of Concept

https://github.com/code-423n4/2022-04-axelar/blob/main/src/AxelarGatewayMultisig.sol#L496

Recommendation

Calculate toEthSignedMessageHash once and cache to save gas:

bytes32 hash = ECDSA.toEthSignedMessageHash(keccak256(data)); for (uint256 i; i < signatureCount; i++) { signers[i] = ECDSA.recover(hash, signatures[i]); }

#0 - deluca-mike

2022-04-19T18:37:25Z

Save gas in for loops by unchecked arithmetic

Acknowledged, but will not do unchecked in this release, as it is a design choice for slight increase in readability.

Calculate toEthSignedMessageHash once and cache

Great find! Confirmed, we'll do this.

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