Platform: Code4rena
Start Date: 02/10/2023
Pot Size: $1,100,000 USDC
Total HM: 28
Participants: 64
Period: 21 days
Judge: GalloDaSballo
Total Solo HM: 13
Id: 292
League: ETH
Rank: 54/64
Findings: 1
Award: $273.57
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: HE1M
Also found by: 0xsomeone, AkshaySrivastav, Aymen0909, J4de, Koolex, Mohandes, bin2chen, brgltd, cccz, hals, ladboy233, max10afternoon, peanuts, rvierdiiev, shealtielanz, tsvetanovv, zzzitron
273.5673 USDC - $273.57
In L1ERC20Bridge.sol
it is possible that the totalDepositedAmountPerUser
mapping is not correct and therefore a user cannot use the deposit()
function for a specific token.
This is because the deposit()
function checks the deposit limit by calling _verifyDepositLimit()
. And because of an incorrect totalDepositedAmountPerUser
statement, a user will not be able to use the deposit()
function when they should be able to make that transaction.
In L1ERC20Bridge.sol
we have deposit()
function:
function deposit(     address _l2Receiver,     address _l1Token,     uint256 _amount,     uint256 _l2TxGasLimit,     uint256 _l2TxGasPerPubdataByte,     address _refundRecipient   ) public payable nonReentrant senderCanCallFunction(allowList) returns (bytes32 l2TxHash) {     require(_amount != 0, "2T"); // empty deposit amount     uint256 amount = _depositFunds(msg.sender, IERC20(_l1Token), _amount);     require(amount == _amount, "1T"); // The token has non-standard transfer logic     // verify the deposit amount is allowed     _verifyDepositLimit(_l1Token, msg.sender, _amount, false);     bytes memory l2TxCalldata = _getDepositL2Calldata(msg.sender, _l2Receiver, _l1Token, amount);     // If the refund recipient is not specified, the refund will be sent to the sender of the transaction.     // Otherwise, the refund will be sent to the specified address.     // If the recipient is a contract on L1, the address alias will be applied.     address refundRecipient = _refundRecipient;     if (_refundRecipient == address(0)) {       refundRecipient = msg.sender != tx.origin ? AddressAliasHelper.applyL1ToL2Alias(msg.sender) : msg.sender;     }     l2TxHash = zkSync.requestL2Transaction{value: msg.value}(       l2Bridge,       0, // L2 msg.value       l2TxCalldata,       _l2TxGasLimit,       _l2TxGasPerPubdataByte,       new bytes[](0),       refundRecipient     );     // Save the deposited amount to claim funds on L1 if the deposit failed on L2     depositAmount[msg.sender][_l1Token][l2TxHash] = amount;     emit DepositInitiated(l2TxHash, msg.sender, _l2Receiver, _l1Token, amount);   }
This function initiates a deposit by locking funds on the contract and sending the request of processing an L2 transaction where tokens would be minted.
If the transaction fails we can call claimFailedDeposit()
:
function claimFailedDeposit(     address _depositSender,     address _l1Token,     bytes32 _l2TxHash,     uint256 _l2BatchNumber,     uint256 _l2MessageIndex,     uint16 _l2TxNumberInBatch,     bytes32[] calldata _merkleProof   ) external nonReentrant senderCanCallFunction(allowList) {     bool proofValid = zkSync.proveL1ToL2TransactionStatus(       _l2TxHash,       _l2BatchNumber,       _l2MessageIndex,       _l2TxNumberInBatch,       _merkleProof,       TxStatus.Failure     );     require(proofValid, "yn");     uint256 amount = depositAmount[_depositSender][_l1Token][_l2TxHash];     require(amount > 0, "y1");     // Change the total deposited amount by the user     _verifyDepositLimit(_l1Token, _depositSender, amount, true);     delete depositAmount[_depositSender][_l1Token][_l2TxHash];     // Withdraw funds     IERC20(_l1Token).safeTransfer(_depositSender, amount);     emit ClaimedFailedDeposit(_depositSender, _l1Token, amount);   }
claimFailedDeposit()
withdraw funds from the initiated deposit, that failed when finalizing on L2.
Both functions call _verifyDepositLimit()
:
function _verifyDepositLimit(address _l1Token, address _depositor, uint256 _amount, bool _claiming) internal {     IAllowList.Deposit memory limitData = IAllowList(allowList).getTokenDepositLimitData(_l1Token);     if (!limitData.depositLimitation) return; // no deposit limitation is placed for this token     if (_claiming) {       totalDepositedAmountPerUser[_l1Token][_depositor] -= _amount;     } else {       require(totalDepositedAmountPerUser[_l1Token][_depositor] + _amount <= limitData.depositCap, "d1");       totalDepositedAmountPerUser[_l1Token][_depositor] += _amount;     }   }
The deposit()
function uses this functor to check the deposit limit is reached to its cap or not. This is done by the following mapping:
totalDepositedAmountPerUser[_l1Token][_depositor]
claimFailedDeposit()
calls the _verifyDepositLimit()
function with the last parameter bool _claiming = true
to be able to reduce the totalDepositedAmountPerUser
mapping because the transaction did not actually occur:
    if (_claiming) {       totalDepositedAmountPerUser[_l1Token][_depositor] -= _amount;
Now imagine the following situation: I use sample numbers and a random token for easy understanding
depositCap
of 100 000 tokenstotalDepositedAmountPerUser[_l1Token][_depositor]
becomes the maximum allowed.claimFailedDeposit()
which calls _verifyDepositLimit()
and everything goes right.totalDepositedAmountPerUser[_l1Token][_depositor]
is not reset because there is no longer a deposit limit and the function cannot get to totalDepositedAmountPerUser[_l1Token][_depositor] -= _amount;
totalDepositedAmountPerUser
will remain forever at 100 000 tokens, with no way to reduce it.totalDepositedAmountPerUser[_l1Token][_depositor]
is 100 000 tokens but actually, it should be 0.deposit()
function at all because he will always be stopped at the following code:require(totalDepositedAmountPerUser[_l1Token][_depositor] + _amount <= limitData.depositCap, "d1");
Visual Studio Code
When the _verifyDepositLimit()
function is called with the _claiming = true
parameter, the mapping reduction must be before the deposit limitation check:
function _verifyDepositLimit(address _l1Token, address _depositor, uint256 _amount, bool _claiming) internal {     IAllowList.Deposit memory limitData = IAllowList(allowList).getTokenDepositLimitData(_l1Token);     if (_claiming) {       totalDepositedAmountPerUser[_l1Token][_depositor] -= _amount;     }     if (!limitData.depositLimitation) return; // no deposit limitation is placed for this token     if (!_claiming) {       require(totalDepositedAmountPerUser[_l1Token][_depositor] + _amount <= limitData.depositCap, "d1");       totalDepositedAmountPerUser[_l1Token][_depositor] += _amount;     };   }
Other
#0 - c4-pre-sort
2023-11-02T15:24:22Z
141345 marked the issue as duplicate of #425
#1 - c4-judge
2023-11-24T20:00:53Z
GalloDaSballo marked the issue as satisfactory