Summary
The L1ERC20Bridge:: _approveFundsToAssetRouter
function assumes that the approve
function of ERC20 tokens returns a boolean value, as defined by the IERC20
interface. However, certain non-standard ERC20 tokens, such as USDT, do not return any value for their approve
function. This mismatch leads to a revert whenever these tokens are used, preventing such tokens from being used for L1->L2 transactions.
Vulnerability Details
The L1ERC20Bridge:: deposit
function is responsible for initiating a deposit by locking funds on the contract and sending a request to process an L2 transaction where tokens would be minted. During this process, the deposit
function calls L1ERC20Bridge:: _approveFundsToAssetRouter
to transfer ERC20 tokens from the depositor to the contract and approve them for use by the L1_ASSET_ROUTER
. The L1ERC20Bridge:: _approveFundsToAssetRouter
function is as follows:
function _approveFundsToAssetRouter(address _from, IERC20 _token, uint256 _amount) internal returns (uint256) {
uint256 balanceBefore = _token.balanceOf(address(this));
_token.safeTransferFrom(_from, address(this), _amount);
@> bool success = _token.approve(address(L1_ASSET_ROUTER), _amount);
if (!success) {
revert ApprovalFailed();
}
uint256 balanceAfter = _token.balanceOf(address(this));
return balanceAfter - balanceBefore;
}
The contract uses the standard ERC20 interface for interacting with tokens, which requires the approve
method to return a boolean value. However, for non-standard tokens like USDT, the approve
method does not return a boolean value, this causes the approve
function to revert during execution. That means users are unable to use non-standard ERC20 tokens for L1->L2 transactions via the L1ERC20Bridge
contract.
Impact
This issue creates a denial of service (DoS) scenario for users attempting to perform L1->L2 transactions with non-standard ERC20 tokens that do not return a value for the approve
function.
Tools Used
Manual
Recommendations
Use OpenZeppelin's SafeERC20
library to interact with ERC20 tokens. Replace the current approve
logic with the following:
function deposit(
address _l2Receiver,
address _l1Token,
uint256 _amount,
uint256 _l2TxGasLimit,
uint256 _l2TxGasPerPubdataByte,
address _refundRecipient
) public payable nonReentrant returns (bytes32 l2TxHash) {
if (_amount == 0) {
// empty deposit amount
revert EmptyDeposit();
}
if (_l1Token == ETH_TOKEN_ADDRESS) {
revert ETHDepositNotSupported();
}
uint256 amount = _approveFundsToAssetRouter(msg.sender, IERC20(_l1Token), _amount);
if (amount != _amount) {
// The token has non-standard transfer logic
revert TokensWithFeesNotSupported();
}
l2TxHash = L1_ASSET_ROUTER.depositLegacyErc20Bridge{value: msg.value}({
_originalCaller: msg.sender,
_l2Receiver: _l2Receiver,
_l1Token: _l1Token,
_amount: _amount,
_l2TxGasLimit: _l2TxGasLimit,
_l2TxGasPerPubdataByte: _l2TxGasPerPubdataByte,
_refundRecipient: _refundRecipient
});
// clearing approval
- bool success = IERC20(_l1Token).approve(address(L1_ASSET_ROUTER), 0);
+ bool success = IERC20(_l1Token).safeApprove(address(L1_ASSET_ROUTER), 0);
if (!success) {
revert ApprovalFailed();
}
depositAmount[msg.sender][_l1Token][l2TxHash] = _amount;
emit DepositInitiated({
l2DepositTxHash: l2TxHash,
from: msg.sender,
to: _l2Receiver,
l1Token: _l1Token,
amount: _amount
});
}
/// @dev Transfers tokens from the depositor address to the native token vault address.
/// @return The difference between the contract balance before and after the transferring of funds.
function _approveFundsToAssetRouter(address _from, IERC20 _token, uint256 _amount) internal returns (uint256) {
uint256 balanceBefore = _token.balanceOf(address(this));
_token.safeTransferFrom(_from, address(this), _amount);
+ bool success = _token.safeApprove(address(L1_ASSET_ROUTER), _amount);
- bool success = _token.approve(address(L1_ASSET_ROUTER), _amount);
if (!success) {
revert ApprovalFailed();
}
uint256 balanceAfter = _token.balanceOf(address(this));
return balanceAfter - balanceBefore;
}