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
Rank: 4/19
Findings: 3
Award: $7,056.43
π Selected for report: 1
π Solo Findings: 1
π Selected for report: rayn
6071.4286 USDC - $6,071.43
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
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.
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.
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:
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.
π Selected for report: IllIllI
Also found by: CertoraInc, Dravee, Funen, cccz, delfin454000, dirk_y, ilan, rayn, rishabh
We list 2 low-critical findings and 4 non-critical findings here:
upgrade()
function in proxy rather than implementationIn conclusion, it's better to check the token symbol, use original implementation of ECDSA, and follow EIP712.
_deployToken
function in AxelarGateway.sol doesn't check against the provided symbol and actual symbol of external tokens.
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");
upgrade()
function in proxy rather than implementationThe upgrade()
function is implemented in AxelarGateway
. If the new AxelarGateway
implementation doesn't have an upgrade()
function, the proxy will not be upgradable.
https://github.com/code-423n4/2022-04-axelar/blob/main/src/AxelarGateway.sol#L258
Move upgrade()
to the proxy contract.
In ECDSA.sol,
recover()
only handles specific types of signatures. But it should be able to take care of more types.
https://github.com/code-423n4/2022-04-axelar/blob/main/src/ECDSA.sol#L31
Follow the original implementation of ECDSA.
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.
https://github.com/code-423n4/2022-04-axelar/blob/main/src/ERC20Permit.sol#L43
Use require(block.timestamp <= deadline, 'EXPIRED');
instead
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.
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
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.
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
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
.
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.
Unclear in both recommendation and in specifying what the problem is.
Confirmed.
Acknowledged, and absolutely agree, but will not change since this was grandfathered in from a release already on mainnet.
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.
The for loop has no overflow risk of i
. Use an unchecked block to save gas.
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
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; } }
toEthSignedMessageHash
once and cacheIn the _execute
function, it calculates toEthSignedMessageHash
many times in loop
https://github.com/code-423n4/2022-04-axelar/blob/main/src/AxelarGatewayMultisig.sol#L496
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
Acknowledged, but will not do unchecked in this release, as it is a design choice for slight increase in readability.
Great find! Confirmed, we'll do this.