Contract fails to deliver promised returns, but doesn't lose value
Griefing (e.g. no profit motive for an attacker, but damage to the users or the protocol)
Block stuffing
Description
Brief/Intro
the fuelERC20gateway.sol allow users to deposit tokens with/without data to L2 by calling the functions deposit and depositWithData, however we recognized that the assetIssuerID play a crusial rule in encoding the data that later used to generate message on L2, the problem arises when users call deposit or depositWthData before the admin make call to the setAssetIssuerId, this lead to encode incorrect data and pass it to L2 sequencer which may lead to return incorrect message data input.
we set this report to high because on the sway L2 part the inputs cames from the bytes that we encoded and incorrect bytes mean incorrect value on L2 sides and its possible to happen only in time gap between deployed gateway and calling the setAssetIssuerId.
Vulnerability Details
the function deposit and DepositWithData and even sendMetadata use assetIssuerID as one of the params to encode valid message data:
functiondeposit(bytes32 to,address tokenAddress,uint256 amount) externalpayablevirtualwhenNotPaused {uint8 decimals =_getTokenDecimals(tokenAddress);uint256 l2MintedAmount =_adjustDepositDecimals(decimals, amount);bytesmemory depositMessage = abi.encodePacked( assetIssuerId,uint256(MessageType.DEPOSIT_TO_ADDRESS),bytes32(uint256(uint160(tokenAddress))),uint256(0),// token_id = 0 for all erc20 depositsbytes32(uint256(uint160(msg.sender))), to, l2MintedAmount,uint256(decimals) );_deposit(tokenAddress, amount, l2MintedAmount, depositMessage); }/// @notice Deposits the given tokens to a contract on Fuel with optional data/// @param to Fuel account or contract to deposit tokens to/// @param tokenAddress Address of the token being transferred to Fuel/// @param amount Amount of tokens to deposit/// @param data Optional data to send with the deposit/// @dev Made payable to reduce gas costsfunctiondepositWithData(bytes32 to,address tokenAddress,uint256 amount,bytescalldata data ) externalpayablevirtualwhenNotPaused {uint8 decimals =_getTokenDecimals(tokenAddress);uint256 l2MintedAmount =_adjustDepositDecimals(decimals, amount);bytesmemory depositMessage = abi.encodePacked( assetIssuerId,uint256(data.length ==0? MessageType.DEPOSIT_TO_CONTRACT : MessageType.DEPOSIT_WITH_DATA),bytes32(uint256(uint160(tokenAddress))),uint256(0),// token_id = 0 for all erc20 depositsbytes32(uint256(uint160(msg.sender))), to, l2MintedAmount,uint256(decimals), data );_deposit(tokenAddress, amount, l2MintedAmount, depositMessage); }functionsendMetadata(address tokenAddress) externalpayablevirtualwhenNotPaused {bytesmemory metadataMessage = abi.encodePacked( assetIssuerId,uint256(MessageType.METADATA), abi.encode( tokenAddress,uint256(0),// token_id = 0 for all erc20 depositsIERC20MetadataUpgradeable(tokenAddress).symbol(),IERC20MetadataUpgradeable(tokenAddress).name() ) );sendMessage(CommonPredicates.CONTRACT_MESSAGE_PREDICATE, metadataMessage); }
the problem is that the assetIssuerId is not set when the implementation initialize function get called which allow the scenario below to be possible:
the fuelERC20Gateway.sol deployed on ethereum and since its impl, the proxy made call to the initialize function which shown as below:
between the deploy time and calling setAssetIssuerId, any user can make deposit call or send metadata call with the assetIssuerID equal to zero bytes which this lead to create incorrect messageData which in its turn lead to two cases:
case A : the sequencers on L2 fuel not create the messageID or set it to invalid transaction since the assetIssuer not equal to the bridge address and this lead to loss of users funds.
case B: creating incorrect messageData which later used in the l2 bridge contract main.sw to get the message data value and return incorrect data.
in both cases user face lose of funds and creating incorrect message data.
Impact Details
not setting the assetIssuerID inside the initialize function might lead to lose of funds or return incorrect message data.
References
its highly recommended to set the assetIssuerID inside the Initialize function when the fuelERC20Gateway get deployed.
Proof of concept
Proof of Concept
// SPDX-License-Identifier: BSD 3-Clause Licensepragmasolidity ^0.8.15;import"forge-std/Test.sol";import"./interfaces/IPortalMessage.sol";import"./interfaces/IERC20.sol";// run this test in this repo : https://github.com/SunWeb3Sec/defilabs run forge test --contracts src/test/test.sol -vvvv
contractContractTestisTest {eventLogBytes(bytes data);addresspublic alice;addresspublic bob;bytes32public assetIssuerID; FuelMessagePortalV3 fuelPortal =FuelMessagePortalV3(0x01855B78C1f8868DE70e84507ec735983bf262dA);address usdt =0xdAC17F958D2ee523a2206206994597C13D831ec7;enumMessageType { DEPOSIT_TO_ADDRESS, DEPOSIT_TO_CONTRACT, DEPOSIT_WITH_DATA, METADATA }functionsetUp() public { vm.createSelectFork("sepolia"); alice = vm.addr(1); bob = vm.addr(2);deal(address(alice),1ether); }functiontestExploitHere() public {uint256 l2MintedAmount =100*10**9;bytesmemory depositMessage1 = abi.encodePacked( assetIssuerID,// this will be 0000uint256(MessageType.DEPOSIT_TO_ADDRESS),bytes32(uint256(uint160(usdt))),uint256(0),// token_id = 0 for all erc20 depositsbytes32(uint256(uint160(msg.sender))), alice, l2MintedAmount,uint256(18) );emitLogBytes(depositMessage1); bytes32 id = bytes32(uint256(uint160(alice))); // any address because the issuerID address is unknown currently which we believe its l2 bridge address
setAssetIssuerID(id);bytesmemory depositMessage2 = abi.encodePacked( assetIssuerID,// this will be 0000uint256(MessageType.DEPOSIT_TO_ADDRESS),bytes32(uint256(uint160(usdt))),uint256(0),// token_id = 0 for all erc20 depositsbytes32(uint256(uint160(msg.sender))), alice, l2MintedAmount,uint256(18) );emitLogBytes(depositMessage2); }functionsetAssetIssuerID(bytes32 id) public { assetIssuerID = id; }}