Overview
This audit covered an update to the pre-deposit contracts of Fuel Network. The update added a points migration function.
Our security assessment was a full review of the updates made, spanning a total of 1 week.
During our audit, we have only identified minor severity 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/1dc275e58f1b8dade136b02798ffae6b5eb2c9ba
The issues described in this report were fixed. Corresponding commits are mentioned in the description.
Summary
Weaknesses
This section contains the list of discovered weaknesses.
FUEL7-13 USE FORCEAPPROVE() INSTEAD OF SAFEINCREASEALLOWANCE() IN PREDEPOSITS
Severity:
Status:
Fixed
Path:
contracts/PreDeposits/PreDeposits.sol#L173-L176
Description:
The allowance increase is not necessary in this case because the old allowance will never be used by FuelERC20Gateway.
The stricter forceApprove() has fewer safety assumptions (no risk of leftover approvals usage) and uses less gas.
IERC20(token).safeIncreaseAllowance(
fuelERC20Gateway,
balanceAdjusted
);
Remediation:
Replace the safeIncreaseAllowance call with forceApprove.
FUEL7-20 DEPOSITWITHPERMIT DOESN'T WORK AS EXPECTED WITH WETH TOKEN
Severity:
Status:
Acknowledged
Path:
contracts/PreDeposits/PreDeposits.sol#L61-L94
Description:
The depositWithPermit function is designed to approve a spender by verifying a valid signature using the permit function. Most ERC20 tokens implement this functionality, allowing users to approve transfers without needing to send a separate transaction.
However, WETH (Wrapped Ether) does not implement the permit function. Despite this, when permit is called on the WETH token, the transaction executes without any errors because WETH's fallback function is triggered instead. This silent execution gives the appearance that the permit function succeeded, but in reality, the approval does not occur. As a result, the subsequent transfer in the depositWithPermit function will fail, causing the transaction to revert when the approval limit is insufficient.
function depositWithPermit(
address token,
uint240 amount,
uint16 depositParam, // deprecated param
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external whenNotPaused {
DepositRecord memory _tokenDeposit = deposits[_msgSender()][token];
_tokenDeposit.balance += amount;
_tokenDeposit.depositParam = depositParam;
deposits[_msgSender()][token] = _tokenDeposit;
// (hexens fuel4-2 fix: Permit can be ddos'ed)
// Adding a try-catch clause allows to skip ddos transactions here
// We are not interested in either catching the error or implementing
// a success flow, we just continue and let safeTransferFrom revert
try
ERC20Permit(token).permit(
_msgSender(),
address(this),
amount,
deadline,
v,
r,
s
)
{} catch {}
IERC20(token).safeTransferFrom(_msgSender(), address(this), amount);
emit Deposit(_msgSender(), token, amount, depositParam);
}
Remediation:
Add a check in the depositWithPermit function to detect if the token being deposited is WETH. If WETH is detected, skip the permit function call entirely since it is not supported by WETH. Instead, require manual approval for WETH transfers.
FUEL7-15 CHECK IF BALANCEADJUSTED IS MORE THAN 0 IN PREDEPOSITS._MIGRATE()
Severity:
Status:
Fixed
Path:
contracts/PreDeposits/PreDeposits.sol#L166-L166
Description:
The PreDeposits._migrate() will create an empty cross-chain transfer in case _adjustDepositDecimals returns zero for ETH or WETH, which is possible if the amount to send is less than 1e9.
Zero transfer in the case of other ERC20 tokens will result in a revert in FuelERC20GatewayV4, but FuelMessagePortal doesn't incorporate such checks.
uint256 balanceAdjusted = _adjustDepositDecimals(balance, token);
Remediation:
Add a check for balanceAdjusted > 0.
FUEL7-14 THE FUNCTION DECIMALS() IS NOT PART OF ERC-20
Severity:
Status:
Fixed
Path:
PreDeposits.sol#L255
Description:
The decimals() function is not a part of the ERC-20 standard.
It is just an optional extension - see here: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/extensions/IERC20Metadata.sol
Thus some ERC20 tokens may not support this interface, which potentially makes it unsafe to blindly cast all tokens to this interface.
Note that the PreDeposits.deposit() function will not revert for ERC20 tokens without the decimals() function, but when trying to migrate these tokens, it may revert.
function _adjustDepositDecimals(
uint256 amount,
address token
) internal returns (uint256) {
uint256 tokenDecimals;
if (token == address(0)) {
tokenDecimals = 18;
} else {
tokenDecimals = IERC20Metadata(token).decimals();
}
Remediation:
Consider handling the tokens without decimals() function.
FUEL7-16 INCONSISTENT USAGE OF MSG.SENDER IN PREDEPOSITS.SOL
Severity:
Status:
Fixed
Path:
PreDeposits.sol#L265-L267
Description:
In the PreDeposits contract the usage of msg.sender is inconsistent.
IERC20(token).safeTransfer(msg.sender, dust);
} else {
(bool sent, ) = address(msg.sender).call{ value: dust }(
Remediation:
Consider adjusting PreDeposits.sol by using _msgSender() instead of msg.sender for better consistency in the codebase.
FUEL7-17 PREDEPOSITS.SOL MAY EMIT A MIGRATION EVENT THAT ISN'T DISTINGUISHING WETH MIGRATIONS
Severity:
Status:
Fixed
Path:
PreDeposits.sol#L161-L184
Description:
When a user migrates their WETH from PreDeposits.sol, a Migration event may be emitted suggesting that ETH tokens address(0) were migrated, since token is set to address(0) in this case inside PreDeposits::_migrate() on line 163 in PreDeposits.sol.
if (token == address(weth)) {
weth.withdraw(balance);
token = address(0);
}
...
emit Migration(_msgSender(), token, balanceAdjusted);
Remediation:
Consider adjusting PreDeposits::_migrate() to emit the WETH token address when migrating WETH tokens.
FUEL7-19 INCONSISTENCY WITH DEPRECATED DEPOSITPARAM HANDLING
Severity:
Status:
Fixed
Path:
PreDeposits::depositWithPermit()#L73, PreDeposits::_depositWithoutCommitment()#L210
Description:
PreDeposits::depositWithPermit() still sets the depositParam whereas PreDeposits::deposit() is setting depositParam to 0.
_tokenDeposit.depositParam = depositParam;
_tokenDeposit.depositParam = 0;
Remediation:
Inside PreDeposits::depositWithPermit() set depositParam to 0.
FUEL7-21 WETH DEPOSITS FROM CONTRACTS WITHOUT RECEIVE() MAY NOT BE ABLE TO USE PREDEPOSITS::MIGRATE()
Severity:
Status:
Fixed
Path:
PreDeposits.sol#L267-L272
Description:
If a contract without receive() deposits WETH with some possible dust amount, it cannot use PreDeposits::migrate() since it will revert.
(bool sent, ) = address(msg.sender).call{ value: dust }(
""
);
if (!sent) {
revert TransferReverted();
}
Remediation:
Consider mentioning the following for example in Fuel docs so that other developers are aware of this:
To avoid funds getting stuck, the contract which uses PreDeposits.sol with WETH should implement either:
- receive function
- a function to withdraw from PreDeposits
- deposit without dust amount
FUEL7-22 MISSING RECOVERY SYSTEM FOR ACCIDENTALLY SENT ETH
Severity:
Status:
Acknowledged
Path:
contracts/PreDeposits/PreDeposits.sol#L44
Description:
In the PreDeposits contract, the receive() external payable {} function is used to allow the contract to accept ETH. However, if a user accidentally sends ETH to this contract, there is currently no mechanism to recover the funds, causing the ETH to become stuck in the contract.
receive() external payable {}
Remediation:
It is recommended to implement a recovery mechanism that allows for the retrieval of these funds.