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: 1/75
Findings: 4
Award: $11,259.63
🌟 Selected for report: 1
🚀 Solo Findings: 1
🌟 Selected for report: Chom
7699.2754 USDC - $7,699.28
XC20Wrapper may lost received token forever if LocalAsset(xc20).mint is reverted indefinitely.
Similar to ERC20, the spec said that if mitn returns false it means minting is failed. But it is commonly revert instead of returning false which is also a minting failure. XC20 may revert on minting as well and common sense also guiding programmers to use the revert pattern instead of returning false.
This case is not handled if SC20 minting is reverted indefinitely. No matter how hard you retry the GMP message execution, it always fail thus the token get locked forever.
function _executeWithToken( string calldata, string calldata, bytes calldata payload, string calldata tokenSymbol, uint256 amount ) internal override { address receiver = abi.decode(payload, (address)); address tokenAddress = gateway().tokenAddresses(tokenSymbol); address xc20 = wrapped[tokenAddress]; if (xc20 == address(0) || !LocalAsset(xc20).mint(receiver, amount)) { _safeTransfer(tokenAddress, receiver, amount); } }
Manual review
Use try catch
function _executeWithToken( string calldata, string calldata, bytes calldata payload, string calldata tokenSymbol, uint256 amount ) internal override { address receiver = abi.decode(payload, (address)); address tokenAddress = gateway().tokenAddresses(tokenSymbol); address xc20 = wrapped[tokenAddress]; if (xc20 == address(0)) { _safeTransfer(tokenAddress, receiver, amount); } try LocalAsset(xc20).mint(receiver, amount) returns (bool success) { if (!success) _safeTransfer(tokenAddress, receiver, amount); } catch { _safeTransfer(tokenAddress, receiver, amount); } }
#0 - re1ro
2022-08-05T09:54:25Z
We addressed the issue with introducing _safeMint
function
https://github.com/axelarnetwork/axelar-xc20-wrapper/pull/4
#1 - GalloDaSballo
2022-09-04T19:01:31Z
The warden states that mint()
may fail and cause a revert instead of returning false.
With the code in scope we can check the used ERC20 implementation and we find:
if (account == address(0)) revert InvalidAccount();
Because a revert can happen, the scenario, which hypothetically would brick the functionality can actually happen.
We may also have reverts due to overflow and underflow.
Because the code is built to assume that no revert can happen, but the warden demonstrated how a revert could factually happen, I do agree with Medium Severity.
The sponsor has mitigated by using _safeMint
3464.6739 USDC - $3,464.67
https://github.com/code-423n4/2022-07-axelar/blob/9c4c44b94cddbd48b9baae30051a4e13cbe39539/contracts/deposit-service/DepositReceiver.sol#L8-L26 https://github.com/code-423n4/2022-07-axelar/blob/9c4c44b94cddbd48b9baae30051a4e13cbe39539/contracts/deposit-service/AxelarDepositService.sol#L85-L102 https://github.com/code-423n4/2022-07-axelar/blob/9c4c44b94cddbd48b9baae30051a4e13cbe39539/contracts/deposit-service/AxelarDepositService.sol#L106-L135 https://github.com/code-423n4/2022-07-axelar/blob/9c4c44b94cddbd48b9baae30051a4e13cbe39539/contracts/deposit-service/AxelarDepositService.sol#L138-L153 https://github.com/code-423n4/2022-07-axelar/blob/9c4c44b94cddbd48b9baae30051a4e13cbe39539/contracts/deposit-service/AxelarDepositService.sol#L157-L182 https://github.com/code-423n4/2022-07-axelar/blob/9c4c44b94cddbd48b9baae30051a4e13cbe39539/contracts/deposit-service/AxelarDepositService.sol#L185-L194 https://github.com/code-423n4/2022-07-axelar/blob/9c4c44b94cddbd48b9baae30051a4e13cbe39539/contracts/deposit-service/AxelarDepositService.sol#L198-L218
selfdestruct will be banned after "The Purge" phase of ethereum merge. DepositReceiver may be broken. Any interaction between AxelarDepositService may result in the loss of fund. Since AxelarDepositService that deal with native token is highly associated with DepositReceiver contract which use to be banned selfdestruct command.
Look at this image in "The Purge" section in "EVM simplification track" that said "Ban SELFDESTRUCT".
This means any contract that use selfdestruct will break after "The Purge"
constructor(bytes memory delegateData) { // Reading the implementation of the AxelarDepositService // and delegating the call back to it // solhint-disable-next-line avoid-low-level-calls (bool success, ) = IAxelarDepositService(msg.sender).receiverImplementation().delegatecall(delegateData); // if not success revert with the original revert data if (!success) { // solhint-disable-next-line no-inline-assembly assembly { let ptr := mload(0x40) let size := returndatasize() returndatacopy(ptr, 0, size) revert(ptr, size) } } selfdestruct(payable(msg.sender)); }
DepositReceiver contract use to be banned selfdestruct as shown above.
Once the purge is implemented, new DepositReceiver
instruction will be reverted.
Since the fund is sent to DepositReceiver before any functions in AxelarDepositService is called.
Any function in AxelarDepositService that leverage DepositReceiver including sendTokenDeposit, refundTokenDeposit, sendNativeDeposit, refundNativeDeposit, nativeUnwrap, refundNativeUnwrap will always revert. As a result, the fund in DepositReceiver will be lost forever since nobody can deploy a working contract to that address.
Manual review, listen to news and use google to find reference
Reimplement DepositReceiver such that it can do the job without using selfdestruct.
#0 - re1ro
2022-08-05T05:14:56Z
Good spot. We will address that
#1 - re1ro
2022-08-22T03:34:32Z
Duplicate of #20
#2 - GalloDaSballo
2022-09-04T19:21:05Z
Valid find, downgrading to Med per #20
🌟 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
64.4609 USDC - $64.46
https://github.com/code-423n4/2022-07-axelar/blob/main/xc20/contracts/XC20Wrapper.sol#L63
function addWrapping( string calldata symbol, address xc20Token, string memory newName, string memory newSymbol ) external payable onlyOwner { address axelarToken = gateway().tokenAddresses(symbol); if (axelarToken == address(0)) revert('NotAxelarToken()'); if (xc20Token.codehash != xc20Codehash) revert('NotXc20Token()'); if (wrapped[axelarToken] != address(0)) revert('AlreadyWrappingAxelarToken()'); if (unwrapped[xc20Token] != address(0)) revert('AlreadyWrappingXC20Token()'); wrapped[axelarToken] = xc20Token; unwrapped[xc20Token] = axelarToken; if (!LocalAsset(xc20Token).set_team(address(this), address(this), address(this))) revert('NotOwner()'); if (!LocalAsset(xc20Token).set_metadata(newName, newSymbol, IERC20(axelarToken).decimals())) revert('CannotSetMetadata()'); payable(msg.sender).transfer(address(this).balance); }
Why transfer dust fund using addWrapper? Consider adding a separate function to do this.
Using deprecated transfer()
on address payable may revert in these cases:
Additionally, using higher than 2300 gas might be mandatory for some multisig wallets.
An example on Axelar codebase that using .transfer is in https://github.com/code-423n4/2022-07-axelar/blob/9c4c44b94cddbd48b9baae30051a4e13cbe39539/contracts/gas-service/AxelarGasService.sol#L144
receiver.transfer(amount);
Should be
(bool success, ) = payable(receiver).call{value: amount}(""); require(success, "ETH transfer failed");
XC20Wrapper should emit events on wrap / unwrap / receive cross chain fund (_executeWithToken) to allow indexers to track the transactions.
It is using accounts.length - 1
which will be underflow and revert when accounts.length == 0
Consider adding if to skip this case
function _isSortedAscAndContainsNoDuplicate(address[] memory accounts) internal pure returns (bool) { if (accounts.length == 0) return false; for (uint256 i; i < accounts.length - 1; ++i) { if (accounts[i] >= accounts[i + 1]) { return false; } } return accounts[0] != address(0); }
if (newThreshold == 0 || totalWeight < newThreshold) revert InvalidThreshold();
New threshold shouldn't be too low such that just one low weight operator can execute malicious transaction.
It would be great to have some more shield on this important function by checking new threshold to be more than xx% of totalWeight
pragma solidity ^0.8.9;
instead of pragma solidity 0.8.9;
pragma solidity ^0.8.9;
allow application that import this library to use more recent version of solidity 0.8.x such as 0.8.13 to compile the contracts while pragma solidity 0.8.9;
doesn't allow it.
If that application use some library that require ^0.8.13. That application may not be able to used with Axelar.
We cannot see anything related to contract calling such as target contract address or payload in the https://github.com/code-423n4/2022-07-axelar/blob/main/contracts/deposit-service/AxelarDepositService.sol.
Which means it doesn't support general message passing
function _executeWithToken( string calldata, string calldata, bytes calldata payload, string calldata tokenSymbol, uint256 amount ) internal override { address receiver = abi.decode(payload, (address)); address tokenAddress = gateway().tokenAddresses(tokenSymbol); address xc20 = wrapped[tokenAddress]; if (xc20 == address(0) || !LocalAsset(xc20).mint(receiver, amount)) { _safeTransfer(tokenAddress, receiver, amount); } }
payload only hold receiver which is not sufficient for general message passing (require target contract and payload). Moreover, there aren't any logic involve contract calling on _executeWithToken.
#0 - GalloDaSballo
2022-08-31T20:56:39Z
Valid R
L
NC
##Â _isSortedAscAndContainsNoDuplicate failed if accounts.length == 0 Don't think you need this, NC
Valid L
Disagree, your colleagues would be sending "floating pragma" reports.
Disputing in lack of detail, please add more information next time (what would the fixed system look like?)
Also not convinced by this as the codebase doesn't seem to use it
Overall a genuine report, thank you!
2L 1R 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
31.222 USDC - $31.22
This reduce gas cost as show here https://forum.openzeppelin.com/t/a-collection-of-gas-optimisation-tricks/19966/5
Caching the length in for loops:
for loop postcondition can be made unchecked Gas savings: roughly speaking this can save 30-40 gas per loop iteration. For lengthy loops, this can be significant!
for (uint256 i; i < refundTokens.length; i++) {
Can be optimized to
uint256 refundTokensLength = refundTokens.length; for (uint256 i; i < refundTokensLength;) { ... unchecked { ++i; } }
Currently every loops such as
are using i++ which is more expensive than ++i
for (uint256 i; i < refundTokens.length; ++i) {
is better than
for (uint256 i; i < refundTokens.length; i++) {
Change
wrapped[axelarToken] = address(0); unwrapped[xc20Token] = address(0);
to
delete wrapped[axelarToken]; delete unwrapped[xc20Token];
to save gas since delete is using negative gas (Get gas refund)
#0 - GalloDaSballo
2022-08-20T22:36:43Z
Less than 100 gas saved (delete doesn't save gas btw, delete is a SSTORE for 0)