Fuel logo

Fuel Labs Predeposit Contract Security Review Report

June 2024

Overview

This audit covered the Predeposit smart contract as developed by Fuel Labs.

Our security assessment was a full review of the smart contract, spanning a total of 3 days.

During our audit, we have identified 2 medium severity vulnerabilities, various minor vulnerabilities and code optimisations.

Finally, all of our reported issues were fixed or acknowledged by the development team and consequently validated by us.

We can confidently say that the overall security and code quality have increased after completion of our audit.

Scope

The analyzed resources are located on:

https://github.com/FuelLabs/predeposit-contracts/tree/6c71bdc76c1b291e2e2565648a333c1603375e81

The issues described in this report were fixed in the following commit:

https://github.com/FuelLabs/predeposit-contracts/tree/6d2b80a4579065d240d920af8f0006d54d620c03

Summary

Total number of findings
5

Weaknesses

This section contains the list of discovered weaknesses.

FUEL4-1 | PREDEPOSITS DOESN'T SUPPORT TOKENS WITH AMOUNT CHANGES IN TRANSFERS

Severity:

Medium

Status:

Acknowledged

Description:

The deposit() and depositWithPermit() functions will put an incorrect amount of tokens into the balances mapping if the transfer amount changes during the token calls (like in fee-on-transfer tokens).

This will lead to other users losing funds on the withdraw() call.

contracts/PreDeposits/PreDeposits.sol:L65

    IERC20(token).safeTransferFrom(_msgSender(), address(this), amount);

contracts/PreDeposits/PreDeposits.sol:L90

    IERC20(token).safeTransferFrom(_msgSender(), address(this), amount);

Remediation:

Use the difference between balances before and after the transfers.

FUEL4-3 | PERMIT DEPOSIT DOESN'T SAVE THE DEPOSITPARAM ARGUMENT TO STORAGE

Severity:

Medium

Status:

Fixed

Path:

contracts/PreDeposits/PreDeposits.sol:L69-L92

Description:

The function accepts and emits the depositParam argument in the Deposit event but doesn't actually update it in the storage deposits mapping.

You can see the update in the twin deposit() function:

contracts/PreDeposits/PreDeposits.sol:L62-L63

            _tokenDeposit.depositParam = depositParam;
            deposits[_msgSender()][token] = _tokenDeposit;
function depositWithPermit(
	address token,
	uint240 amount,
	uint16 depositParam,
	uint256 deadline,
	uint8 v,
	bytes32 r,
	bytes32 s
) external whenNotPaused {
	deposits[_msgSender()][token].balance += amount;
 
	ERC20Permit(token).permit(
		_msgSender(),
		address(this),
		amount,
		deadline,
		v,
		r,
		s
	);
 
	IERC20(token).safeTransferFrom(_msgSender(), address(this), amount);
	emit Deposit(_msgSender(), token, amount, depositParam);
}

Remediation:

Add the update to the function.

FUEL4-2 | PERMIT DEPOSIT CAN BE DOS'ED

Severity:

Low

Status:

Fixed

Path:

contracts/PreDeposits/PreDeposits.sol::depositWithPermit():L69-L92

Description:

The PreDeposits.sol::depositWithPermit() function currently utilizes an inner call to the permit() function of the openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol. However, this flow exposes depositWithPermit() to a griefing attack, where an attacker can forcibly block the victim's transaction.

The attack proceeds as follows: the attacker front-runs the victim's transaction, extracts the parameters from the mempool, and places a transaction that directly calls ERC20Permit(token).permit() with the victim's params. Consequently, the victim's transaction reverts since the signature has already been used for permit() in the attacker's transaction.

    function depositWithPermit(
        address token,
        uint240 amount,
        uint16 depositParam,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external whenNotPaused {
        deposits[_msgSender()][token].balance += amount;
 
        ERC20Permit(token).permit(
            _msgSender(),
            address(this),
            amount,
            deadline,
            v,
            r,
            s
        );
 
        IERC20(token).safeTransferFrom(_msgSender(), address(this), amount);
        emit Deposit(_msgSender(), token, amount, depositParam);
    }

Remediation:

Consider adding try/cath, or if block, so if the permit() is already called, just check the allowance of msg.sender and skip the call to permit().

FUEL4-4 | ENABLE WITHDRAWALS TO OTHER ADDRESSES

Severity:

Informational

Status:

Fixed

Path:

contracts/PreDeposits/PreDeposits.sol::withdraw()#L95-L111

Description:

The current implementation of the PreDeposits.sol contract restricts withdrawals to the sender's address. This limitation can be restrictive for users who might want to withdraw their funds to a different address. Allowing withdrawals to other addresses can offer flexibility and convenience to users, facilitating smoother fund management and better user experience.

    function withdraw(address token, uint240 amount) external whenNotPaused {
        // Underflow checks already in effect with new solidity versions
        deposits[_msgSender()][token].balance =
            deposits[_msgSender()][token].balance -
            amount;
 
        if (token == address(0)) {
            (bool success, ) = _msgSender().call{ value: amount }("");
            if (!success) {
                revert RecipientRevert();
            }
        } else {
            IERC20(token).safeTransfer(_msgSender(), amount);
        }
 
        emit Withdraw(_msgSender(), token, amount);
    }

Remediation:

Add address recipient to the withdraw() function.

FUEL4-5 | THE MIGRATE FUNCTION IS NOT IMPLEMENTED

Severity:

Informational

Status:

Acknowledged

Path:

contracts/PreDeposits/PreDeposits.sol::migrate():L134-L140

Description:

The migrate() function is typically used for migration management. However, in this case, it is not implemented and will always revert.

    function migrate(
        address /*token*/,
        address /*migrationFacilitator*/,
        bytes calldata /*facilitatorData*/
    ) external view whenNotPaused {
        revert("UNIMPLEMENTED");
    }

Remediation:

Consider implementing the migrate() function.

Table of contents