From f13b2d14ae4b1bf36d70665d6c9fd92c84662a17 Mon Sep 17 00:00:00 2001 From: MrShiroLu Date: Wed, 20 May 2026 11:37:10 +0300 Subject: [PATCH] fix: enforce balance-delta accounting in StandardBridge (#289) Fee-on-transfer and rebasing tokens are documented as unsupported, but `_initiateBridgeERC20` credited `deposits[_localToken][_remoteToken]` and forwarded `_amount` to the remote bridge without checking that the escrow balance actually increased by `_amount`. With a fee-on-transfer token this leaves the local escrow short while the full `_amount` is minted/released on the remote chain, eventually breaking withdrawals. Take the balance before and after `safeTransferFrom` and require the delta to equal `_amount`. Unsupported tokens now revert at deposit time (fail closed) rather than silently underfunding the bridge. Bump L1StandardBridge to 2.9.0 and L2StandardBridge to 1.14.0 and regenerate snapshots/semver-lock.json. --- snapshots/semver-lock.json | 8 ++++---- src/L1/L1StandardBridge.sol | 4 ++-- src/L2/L2StandardBridge.sol | 4 ++-- src/universal/StandardBridge.sol | 7 +++++++ 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/snapshots/semver-lock.json b/snapshots/semver-lock.json index 8b92ddd8b..4fde47d32 100644 --- a/snapshots/semver-lock.json +++ b/snapshots/semver-lock.json @@ -12,8 +12,8 @@ "sourceCodeHash": "0xb20a9176e07c550466ccea8f131d74e98112133e1e833a72cbae5da93039392c" }, "src/L1/L1StandardBridge.sol:L1StandardBridge": { - "initCodeHash": "0xadd7863f0d14360be0f0c575d07aa304457b190b64a91a8976770fb7c34b28a3", - "sourceCodeHash": "0xd9f576a79e97bc541b3d7a2ee928f34223edaaecb074eeb9e6e2ecee857ce6a0" + "initCodeHash": "0xaa5a38f6abb8102055e2289c39ee8d345251b9bafebcad47b7e569dc4a297d33", + "sourceCodeHash": "0xf79f507285f836bd5871669eac2eebad5bd18c90dbfc734258dfdace6872edd3" }, "src/L1/OptimismPortal2.sol:OptimismPortal2": { "initCodeHash": "0x2c01bc6c0a55a1a27263224e05c1b28703ff85c61075bae7ab384b3043820ed2", @@ -88,8 +88,8 @@ "sourceCodeHash": "0x032bda19a2c0a05e8abfdac182982ad14256cbcebc1896ba503bb1a2a681e81f" }, "src/L2/L2StandardBridge.sol:L2StandardBridge": { - "initCodeHash": "0xba5b288a396b34488ba7be68473305529c7da7c43e5f1cfc48d6a4aecd014103", - "sourceCodeHash": "0xc53a2f1fc472b15aa2891d48845b078ef12fdae869a4b74bef19b19d9d20ed5f" + "initCodeHash": "0x71061060e1cf2d0768e0500b0d8300980fbe13b02cde8c09e812bf7f63bfac09", + "sourceCodeHash": "0xe21cf335fb23c8781bd9f4c53fe42b1c03a3e555465bfa0ddc2b611886b0ac2b" }, "src/L2/L2ToL1MessagePasser.sol:L2ToL1MessagePasser": { "initCodeHash": "0xe30675ea6623cd7390dd2cd1e9a523c92c66956dfab86d06e318eb410cd1989b", diff --git a/src/L1/L1StandardBridge.sol b/src/L1/L1StandardBridge.sol index 9dc0a4e0f..6c3c3a144 100644 --- a/src/L1/L1StandardBridge.sol +++ b/src/L1/L1StandardBridge.sol @@ -77,8 +77,8 @@ contract L1StandardBridge is StandardBridge, ProxyAdminOwnedBase, Reinitializabl ); /// @notice Semantic version. - /// @custom:semver 2.8.0 - string public constant version = "2.8.0"; + /// @custom:semver 2.9.0 + string public constant version = "2.9.0"; /// @custom:legacy /// @custom:spacer superchainConfig diff --git a/src/L2/L2StandardBridge.sol b/src/L2/L2StandardBridge.sol index 9567e13b8..b09b20127 100644 --- a/src/L2/L2StandardBridge.sol +++ b/src/L2/L2StandardBridge.sol @@ -57,9 +57,9 @@ contract L2StandardBridge is StandardBridge, ISemver { ); /// @notice Semantic version. - /// @custom:semver 1.13.0 + /// @custom:semver 1.14.0 function version() public pure virtual returns (string memory) { - return "1.13.0"; + return "1.14.0"; } /// @notice Constructs the L2StandardBridge contract. diff --git a/src/universal/StandardBridge.sol b/src/universal/StandardBridge.sol index fb7912551..bfa5c5b00 100644 --- a/src/universal/StandardBridge.sol +++ b/src/universal/StandardBridge.sol @@ -358,7 +358,14 @@ abstract contract StandardBridge is Initializable { IOptimismMintableERC20(_localToken).burn(_from, _amount); } else { + // Reject fee-on-transfer and rebasing tokens by requiring the bridge's balance to + // increase by exactly `_amount`. Without this check, the bridge would credit and + // forward `_amount` to the remote chain while holding less than `_amount` in escrow, + // leading to under-collateralized representations and eventual withdrawal failures. + uint256 balanceBefore = IERC20(_localToken).balanceOf(address(this)); IERC20(_localToken).safeTransferFrom(_from, address(this), _amount); + uint256 received = IERC20(_localToken).balanceOf(address(this)) - balanceBefore; + require(received == _amount, "StandardBridge: amount received does not match amount transferred"); deposits[_localToken][_remoteToken] = deposits[_localToken][_remoteToken] + _amount; }