Platform: Code4rena
Start Date: 29/07/2022
Pot Size: $50,000 USDC
Total HM: 6
Participants: 75
Period: 5 days
Judge: GalloDaSballo
Total Solo HM: 3
Id: 149
League: ETH
Rank: 5/75
Findings: 3
Award: $2,169.87
🌟 Selected for report: 0
🚀 Solo Findings: 0
2078.8043 USDC - $2,078.80
https://github.com/code-423n4/2022-07-axelar/blob/main/contracts/auth/AxelarAuthWeighted.sol#L55-L84
Following scenario: There are 7 operators and the necessary threshold to transfer operatorship is 3.
3 of the 7 operators turn rogue and want to kick out the rest of them. They write a smart contract that calls transferOperatorship()
16 times (see AxelarAuthWeighted.OLD_KEY_RETENTION
) with the final call being the desired operator state. They run the contract's function and successfully lock out the rest of the operators without them being able to do anything. All the 16 operator states that are usable are from the attackers. Any subsequent call by honest operators to AxelarAuth.validateProof()
will fail.
A more reasonable approach would be to allow the transferOperatorship()
function to be only called once in a given timeframe. That way, no operator can loose their access in a matter of a single transaction. This would give them time to react. Additionally, there should be a mechanism for an older set of operators to gain back control by being allowed to call transferOperatorship()
as well. Currently, the current operators are the only ones who can transfer operatorship. This means, that you can't block a rogue set of operators from taking over control over transfer mechanism.
Currently, there seems to be a trust that operators can always be trusted. You can't be sure that it will be always the case. Equipping your contract to handle such a situation might come handy at some point.
https://github.com/code-423n4/2022-07-axelar/blob/main/contracts/auth/AxelarAuthWeighted.sol#L55-L84
none
Add a state variable that keeps track of the last time operators were changed. Only allow operators to be changed after a day has passed. Also, allow past operators within the retention period to be able to transfer operatorship as well.
#0 - re1ro
2022-08-05T03:34:10Z
Not a real issue. If our operators turn rogue they can completely overtake the gateway. It's an obvious thing. The network designed in a way that is more profitable to be a good operator. That's why we are running 20 operators now and will have 50 operators soon
#1 - GalloDaSballo
2022-09-04T19:39:30Z
Dup of #19
As the statement is that operators can kick old operator in one tx vs 16
🌟 Selected for report: oyc_109
Also found by: 0x1f8b, 0x52, 0xNazgul, 0xSmartContract, 0xf15ers, 8olidity, Aymen0909, Bnke0x0, CertoraInc, Chom, CodingNameKiki, Deivitto, Dravee, ElKu, IllIllI, JC, Lambda, Noah3o6, NoamYakov, RedOneN, Respx, ReyAdmirado, Rohan16, Rolezn, Ruhum, Sm4rty, TomJ, Twpony, Waze, Yiko, __141345__, ajtra, apostle0x01, ashiq0x01, asutorufos, bardamu, benbaessler, berndartmueller, bharg4v, bulej93, c3phas, cccz, ch13fd357r0y3r, codexploder, cryptonue, cryptphi, defsec, djxploit, durianSausage, fatherOfBlocks, gogo, hansfriese, horsefacts, ignacio, kyteg, lucacez, mics, rbserver, robee, sashik_eth, simon135, sseefried, tofunmi, xiaoming90
56.196 USDC - $56.20
Consider using a two-step process for transferring the ownership of a contract. While it costs a little more gas, it's safer than transferring directly.
Here's an example from the Compound Timelock contract: https://github.com/compound-finance/compound-protocol/blob/master/contracts/Timelock.sol#L45-L58
The _transferOperatorship()
function validates certain properties of the new operators. One of them is the check whether the threshold is not 0. I'd argue, that you should increase the minimum limit to a more reasonable number. With the current implementation, the operators can configure the contract so that only 1 operator needs to sign off (threshold == 1
). That could happen by mistake or intentionally. Even then it's most likely not in the best interest of the users that a single entity gains control. The whole idea behind the design is that multiple operators have to sign off.
https://github.com/code-423n4/2022-07-axelar/blob/main/contracts/auth/AxelarAuthWeighted.sol#L55-L84
Change line 72 to something like:
if (newThreshold < 5 || totalWeight < newThreshold) revert InvalidThreshold();
It allows third parties to track those changes efficiently.
#0 - re1ro
2022-08-05T01:58:52Z
Dup #16
This contract is designed to be generic. newThreshold < 5
would still allow one operator with weight 5
Good spot. Will consider that
#1 - GalloDaSballo
2022-09-04T21:11:25Z
NC
L as the finding is valid and while trust assumptions are not broken, risk is increased (this is equivalent to having a Multisig with one Signer)
NC
1L 2NC
🌟 Selected for report: IllIllI
Also found by: 0x1f8b, 0xNazgul, 0xsam, 8olidity, Aymen0909, Bnke0x0, Chom, CodingNameKiki, Deivitto, Dravee, ElKu, Fitraldys, JC, Lambda, MiloTruck, Noah3o6, NoamYakov, RedOneN, Respx, ReyAdmirado, Rohan16, Rolezn, Ruhum, Sm4rty, TomJ, Tomio, Waze, __141345__, a12jmx, ajtra, ak1, apostle0x01, asutorufos, benbaessler, bharg4v, bulej93, c3phas, defsec, djxploit, durianSausage, erictee, fatherOfBlocks, gerdusx, gogo, kyteg, lucacez, medikko, mics, owenthurm, oyc_109, rbserver, robee, sashik_eth, simon135, tofunmi
34.8669 USDC - $34.87
Since the loop's iterator can't realistically overflow because of the loop's condition, you can increment it in an unchecked block to save gas:
./contracts/auth/AxelarAuthWeighted.sol:17: for (uint256 i; i < recentOperators.length; ++i) { ./contracts/auth/AxelarAuthWeighted.sol:69: for (uint256 i = 0; i < weightsLength; ++i) { ./contracts/auth/AxelarAuthWeighted.sol:98: for (uint256 i = 0; i < signatures.length; ++i) { ./contracts/auth/AxelarAuthWeighted.sol:116: for (uint256 i; i < accounts.length - 1; ++i) { ./contracts/gas-service/AxelarGasService.sol:123: for (uint256 i; i < tokens.length; i++) { ./contracts/deposit-service/AxelarDepositService.sol:114: for (uint256 i; i < refundTokens.length; i++) { ./contracts/deposit-service/AxelarDepositService.sol:168: for (uint256 i; i < refundTokens.length; i++) { ./contracts/deposit-service/AxelarDepositService.sol:204: for (uint256 i; i < refundTokens.length; i++) { ./contracts/AdminMultisigBase.sol:51: for (uint256 i; i < adminCount; ++i) { ./contracts/AdminMultisigBase.sol:158: for (uint256 i; i < adminLength; ++i) { ./contracts/AxelarGateway.sol:195: for (uint256 i; i < adminCount; ++i) { ./contracts/AxelarGateway.sol:207: for (uint256 i = 0; i < symbols.length; i++) { ./contracts/AxelarGateway.sol:293: for (uint256 i; i < commandsLength; ++i) { ./contracts/auth/AxelarAuthWeighted.sol:101: for (; operatorIndex < operatorsLength && signer != operators[operatorIndex]; ++operatorIndex) {}
Just search for "for (" in your .sol
files to find all the relevant instances
Using ++i
saves gas because you don't have to cache the original value in memory. You already use it in some places but not consistently.
./contracts/gas-service/AxelarGasService.sol:123: for (uint256 i; i < tokens.length; i++) { ./contracts/deposit-service/AxelarDepositService.sol:114: for (uint256 i; i < refundTokens.length; i++) { ./contracts/deposit-service/AxelarDepositService.sol:168: for (uint256 i; i < refundTokens.length; i++) { ./contracts/deposit-service/AxelarDepositService.sol:204: for (uint256 i; i < refundTokens.length; i++) { ./contracts/AxelarGateway.sol:207: for (uint256 i = 0; i < symbols.length; i++) {
solmate's contracts are optimized for gas usage. They are more efficient than OZ's contracts. Consider switching to them. It will save gas for all the ERC20 transfers, burns, etc.
https://github.com/transmissions11/solmate
In XC20Wrapper.unwrap()
the function checks whether the caller has enough tokens to burn before calling the burn()
function. But, the burn function will simply revert if the user doesn't have enough. Explicitly checking for that is a waste of gas.
https://github.com/code-423n4/2022-07-axelar/blob/main/xc20/contracts/XC20Wrapper.sol#L85
#0 - re1ro
2022-08-05T01:54:24Z
Yup. Dup #2
Yup. Dup #2
Good spot. We might consider that
Good spot.
#1 - GalloDaSballo
2022-08-23T01:09:34Z
Would like to see a benchmark in difference
Will give the report 350 gas