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: 49/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
The owner can call AllowList.setDepositLimit() to set the user's deposit cap for tokens.
function setDepositLimit(address _l1Token, bool _depositLimitation, uint256 _depositCap) external onlyOwner { tokenDeposit[_l1Token].depositLimitation = _depositLimitation; tokenDeposit[_l1Token].depositCap = _depositCap; }
In L1ERC20Bridge.deposit(), each deposit increases totalDepositedAmountPerUser and then checks if it reaches the deposit cap.
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); ... 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; } }
Then if the deposit fails, totalDepositedAmountPerUser is decreased in 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);
The problem here is that if setDepositLimit() is called to set the deposit cap after the user has made a deposit, and then if the deposit fails, the user's claimFailedDeposit() call will be reverted due to overflow. Consider the following scenario.
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; // @auditor: overflow here
To run the POC, we need to change _verifyDepositLimit() to public first.
diff --git a/code/contracts/ethereum/contracts/bridge/L1ERC20Bridge.sol b/code/contracts/ethereum/contracts/bridge/L1ERC20Bridge.sol index 9d134f8..827781c 100644 --- a/code/contracts/ethereum/contracts/bridge/L1ERC20Bridge.sol +++ b/code/contracts/ethereum/contracts/bridge/L1ERC20Bridge.sol @@ -337,7 +337,7 @@ contract L1ERC20Bridge is IL1Bridge, IL1BridgeLegacy, AllowListed, ReentrancyGua } /// @dev Verify the deposit limit is reached to its cap or not - function _verifyDepositLimit(address _l1Token, address _depositor, uint256 _amount, bool _claiming) internal { + function _verifyDepositLimit(address _l1Token, address _depositor, uint256 _amount, bool _claiming) public { IAllowList.Deposit memory limitData = IAllowList(allowList).getTokenDepositLimitData(_l1Token); if (!limitData.depositLimitation) return; // no deposit limitation is placed for this token
Then write the following POC that shows that calling setDepositLimit() after the user deposit, _verifyDepositLimit() in claimFailedDeposit() will be reverted due to overflow
diff --git a/code/contracts/ethereum/test/unit_tests/l1_erc20_bridge_test.spec.ts b/code/contracts/ethereum/test/unit_tests/l1_erc20_bridge_test.spec.ts index ce6a634..7aab014 100644 --- a/code/contracts/ethereum/test/unit_tests/l1_erc20_bridge_test.spec.ts +++ b/code/contracts/ethereum/test/unit_tests/l1_erc20_bridge_test.spec.ts @@ -152,6 +152,20 @@ describe(`L1ERC20Bridge tests`, function () { ); }); + it(`POC`, async () => { + const depositorAddress = await randomSigner.getAddress(); + await depositERC20( + l1ERC20Bridge.connect(randomSigner), + zksyncContract, + depositorAddress, + testnetERC20TokenContract.address, + ethers.utils.parseUnits('800', 18), + 10000000 + ); + allowList.setDepositLimit(testnetERC20TokenContract.address,true,ethers.utils.parseUnits('1000000', 18)); + l1ERC20Bridge._verifyDepositLimit(testnetERC20TokenContract.address,depositorAddress,ethers.utils.parseUnits('10000', 18),true); + }); + it(`Should revert on finalizing a withdrawal with wrong message length`, async () => { const revertReason = await getCallRevertReason( l1ERC20Bridge.connect(randomSigner).finalizeWithdrawal(0, 0, 0, '0x', [])
https://github.com/code-423n4/2023-10-zksync/blob/72f5f16ed4ba94c7689fe38fcb0b7d27d2a3f135/code/contracts/ethereum/contracts/bridge/L1ERC20Bridge.sol#L340-L350 https://github.com/code-423n4/2023-10-zksync/blob/72f5f16ed4ba94c7689fe38fcb0b7d27d2a3f135/code/contracts/ethereum/contracts/common/AllowList.sol#L129-L132
None
It is recommended that the subtraction operation in claimFailedDeposit() be performed only when totalDepositedAmountPerUser is greater than _amount.
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) { + if(totalDepositedAmountPerUser[_l1Token][_depositor] >= _amount) totalDepositedAmountPerUser[_l1Token][_depositor] -= _amount;
Context
#0 - c4-pre-sort
2023-10-31T14:30:47Z
bytes032 marked the issue as duplicate of #246
#1 - c4-pre-sort
2023-10-31T14:30:52Z
bytes032 marked the issue as sufficient quality report
#2 - c4-judge
2023-11-24T20:01:14Z
GalloDaSballo marked the issue as satisfactory
#3 - thereksfour
2023-12-02T07:48:59Z
This issue is not a duplicate of #246, which describes the possibility that users may not be able to withdraw failed deposits when the owner sets deposit limits. Instead, #246 describes that WETH's deposit limits will be applied to L1WethBridge rather than per user. And, for the severity of this issue, I'd say low likelihood + high impact = medium severity.
#4 - GalloDaSballo
2023-12-04T11:00:24Z
Disagree with the distinction, same underlying issue of the underflow when limit is set
#5 - thereksfour
2023-12-04T11:34:16Z
First of all, thank you for reviewing it.@GalloDaSballo. But I don't see any description of underflow from #246. #246 says
The flow bridgeProxy.deposit() -> L1WethBridge.deposit() -> Mailbox.requestL2Transaction() allows one to deposit WETH from L1 to L2. However, when checking the deposit limit for a particular depositor, Mailbox.requestL2Transaction() checks the limit of msg.sender, which is the address of L1WethBridge.
#246 describes an issue where users are unable to deposit WETH via L1WethBridge when the limit is reached because the deposit limit is on L1WethBridge and not the user, and is not an underflow issue.
However, this issue is for L1ERC20Bridge, and describes the issue of users not being able to withdraw failed deposits due to underflow when the owner sets a deposit limit. I don't see any similarities between the two issues other than they are both about deposit limits.
#6 - c4-judge
2023-12-12T17:56:50Z
GalloDaSballo marked the issue as not a duplicate
#7 - c4-judge
2023-12-12T17:57:03Z
GalloDaSballo marked the issue as duplicate of #425