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: 24/75
Findings: 1
Award: $91.30
π Selected for report: 0
π Solo Findings: 0
π 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
91.2958 USDC - $91.30
setup
may fail silentlySince low level calls do not check for contract existence, and return true
if a contract does not exist, it's possible for Axelar proxy contracts that delegatecall
to a setup
function to fail silently if their implementationAddress
does not exist.
// solhint-disable-next-line avoid-low-level-calls (bool success, ) = implementationAddress.delegatecall( //0x9ded06df is the setup selector. abi.encodeWithSelector(0x9ded06df, params) ); if (!success) revert SetupFailed();
xc20 Proxy#constructor
:
// solhint-disable-next-line avoid-low-level-calls (bool success, ) = implementationAddress.delegatecall( //0x9ded06df is the setup selector. abi.encodeWithSelector(0x9ded06df, params) ); if (!success) revert SetupFailed();
Suggestion: Check address.code.length
for implementationAddress
. For example:
if (implementationAddress.code.length == 0) revert ImplementationDoesNotExist(); // solhint-disable-next-line avoid-low-level-calls (bool success, ) = implementationAddress.delegatecall( //0x9ded06df is the setup selector. abi.encodeWithSelector(0x9ded06df, params) ); if (!success) revert SetupFailed();
In addition to fee-on-transfer tokens (see this finding from the previous C4 contest), the Axelar gateway is not fully compatible with ERC20 tokens that increase an account's underlying balance, like Lido stETH and Aave aTokens. Due to rebases, the gateway contract may accrue a balance greater than the token amount minted on a destination chain, and these excess amounts could remain locked in the gateway. There is not really a reasonable technical solution here: instead it's probably best to document this incompatibility for end users of the protocol if token creation becomes permissionless, or avoid these tokens if it is permissioned.
The ERC20Permit
contract generates and permanently stores an EIP-712 domain separator using block.chainid
at construction time:
constructor(string memory name) { DOMAIN_SEPARATOR = keccak256( abi.encode(DOMAIN_TYPE_SIGNATURE_HASH, keccak256(bytes(name)), keccak256(bytes('1')), block.chainid, address(this)) ); }
constructor(string memory name) { DOMAIN_SEPARATOR = keccak256( abi.encode(DOMAIN_TYPE_SIGNATURE_HASH, keccak256(bytes(name)), keccak256(bytes('1')), block.chainid, address(this)) ); }
Since chainid
is set permanently, there is a risk of replay attacks in the event of a hard fork: since contracts on both forks would use the same stored chainid
, signatures on one chain would be replayable on the fork. This is a pretty low probablity scenario, but as a cross-chain protocol supporting many EVM chains, Axelar is probably at higher risk of exposure to a hard fork than a typical single chain application.
Suggestion: cache chainId
and DOMAIN_SEPARATOR
at construction time. Use the cached DOMAIN_SEPARATOR
unless chainID
has changed. If so, regenerate DOMAIN_SEPARATOR
.
SafeTransfer
libraryA similar ERC20 _safeTransfer
function is defined twice, in both DepositBase
and AxelarGasService
. Consider extracting this function to a shared library. (Note that the version in AxelarGasService
includes a zero amount check that should be moved outside the function if you do implement this refactor).
function _safeTransfer( address tokenAddress, address receiver, uint256 amount ) internal { // solhint-disable-next-line avoid-low-level-calls (bool success, bytes memory returnData) = tokenAddress.call(abi.encodeWithSelector(IERC20.transfer.selector, receiver, amount)); bool transferred = success && (returnData.length == uint256(0) || abi.decode(returnData, (bool))); if (!transferred || tokenAddress.code.length == 0) revert TokenTransferFailed(); }
AxelarGasService#_safeTransfer
function _safeTransfer( address tokenAddress, address receiver, uint256 amount ) internal { if (amount == 0) revert NothingReceived(); // solhint-disable-next-line avoid-low-level-calls (bool success, bytes memory returnData) = tokenAddress.call(abi.encodeWithSelector(IERC20.transfer.selector, receiver, amount)); bool transferred = success && (returnData.length == uint256(0) || abi.decode(returnData, (bool))); if (!transferred || tokenAddress.code.length == 0) revert TransferFailed(); }
Note that a similar function exists in the XC20 contract as well, although it may make sense to keep it isolated:
function _safeTransfer( address tokenAddress, address receiver, uint256 amount ) internal { (bool success, bytes memory returnData) = tokenAddress.call(abi.encodeWithSelector(IERC20.transfer.selector, receiver, amount)); bool transferred = success && (returnData.length == uint256(0) || abi.decode(returnData, (bool))); if (!transferred || tokenAddress.code.length == 0) revert('TransferFailed()'); }
#0 - GalloDaSballo
2022-08-31T23:39:15Z
##Β Proxy setup may fail silently I think this is the one time where instead of just address(0) check we'd also want a contract.size check, valid L
L
L
R
Refreshing to see concise and accurate advice
3L 1R