diff --git a/packages/contracts-bedrock/interfaces/L2/ISuperchainWETH.sol b/packages/contracts-bedrock/interfaces/L2/ISuperchainWETH.sol index e646807b8d1..883252c69ae 100644 --- a/packages/contracts-bedrock/interfaces/L2/ISuperchainWETH.sol +++ b/packages/contracts-bedrock/interfaces/L2/ISuperchainWETH.sol @@ -9,6 +9,7 @@ interface ISuperchainWETH is IWETH98, IERC7802, ISemver { error Unauthorized(); error InvalidCrossDomainSender(); error ZeroAddress(); + error Permit2AllowanceIsFixedAtInfinity(); event SendETH(address indexed from, address indexed to, uint256 amount, uint256 destination); diff --git a/packages/contracts-bedrock/snapshots/abi/SuperchainWETH.json b/packages/contracts-bedrock/snapshots/abi/SuperchainWETH.json index 24b27063f34..cdc8ab052f2 100644 --- a/packages/contracts-bedrock/snapshots/abi/SuperchainWETH.json +++ b/packages/contracts-bedrock/snapshots/abi/SuperchainWETH.json @@ -519,6 +519,11 @@ "name": "InvalidCrossDomainSender", "type": "error" }, + { + "inputs": [], + "name": "Permit2AllowanceIsFixedAtInfinity", + "type": "error" + }, { "inputs": [], "name": "Unauthorized", diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 76079207832..a1c2f21311c 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -121,15 +121,15 @@ }, "src/L2/SuperchainERC20.sol": { "initCodeHash": "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470", - "sourceCodeHash": "0x981dca5b09da9038a9dff071b40a880e1b52b20268c6780ef54be3bc98a4f629" + "sourceCodeHash": "0x8beaea08eef20031b8b77226144479eac9e2f14bbd620ab35f0863c5ce48a8c0" }, "src/L2/SuperchainTokenBridge.sol": { "initCodeHash": "0x6b568ed564aede82a3a4cbcdb51282cad0e588a3fe6d91cf76616d3113df3901", "sourceCodeHash": "0xcd2b49cb7cf6d18616ee8bec9183fe5b5b460941875bc0b4158c4d5390ec3b0c" }, "src/L2/SuperchainWETH.sol": { - "initCodeHash": "0x545686820e440d72529c815b7406844272d5ec33b741b2be6ebbe3a3db1ca8ad", - "sourceCodeHash": "0x6145e61cc0a0c95db882a76ecffea15c358c2b574d5157e53b85a69908701613" + "initCodeHash": "0xf75310a99b112a6d8f639c4981105b746dbfa80e13c5846bc28cdb5732bebf61", + "sourceCodeHash": "0xc8d46e4a22a1c13b44a71f33e1a56cdb1831887de1570f6bed4b8176d0fe1cad" }, "src/L2/WETH.sol": { "initCodeHash": "0x38b396fc35d72e8013bad2fe8d7dea5285499406d4c4b62e27c54252e1e0f00a", diff --git a/packages/contracts-bedrock/src/L2/SuperchainERC20.sol b/packages/contracts-bedrock/src/L2/SuperchainERC20.sol index 061c2d867b6..f27f215cbd9 100644 --- a/packages/contracts-bedrock/src/L2/SuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/SuperchainERC20.sol @@ -14,8 +14,11 @@ import { ISemver } from "interfaces/universal/ISemver.sol"; import { IERC7802, IERC165 } from "interfaces/L2/IERC7802.sol"; /// @title SuperchainERC20 -/// @notice A standard ERC20 extension implementing IERC7802 for unified cross-chain fungibility across -/// the Superchain. Allows the SuperchainTokenBridge to mint and burn tokens as needed. +/// @notice A standard ERC20 extension implementing IERC7802 for unified cross-chain fungibility +/// across the Superchain. Gives the SuperchainTokenBridge mint and burn permissions. +/// @dev This contract inherits from Solady@v0.0.245 ERC20. Carefully review Solady's, +/// documentation including all warnings, comments and natSpec, before extending or +/// interacting with this contract. abstract contract SuperchainERC20 is ERC20, IERC7802, ISemver { /// @notice Semantic version. /// @custom:semver 1.0.0-beta.8 diff --git a/packages/contracts-bedrock/src/L2/SuperchainWETH.sol b/packages/contracts-bedrock/src/L2/SuperchainWETH.sol index 989d6d55ca2..761af7157fb 100644 --- a/packages/contracts-bedrock/src/L2/SuperchainWETH.sol +++ b/packages/contracts-bedrock/src/L2/SuperchainWETH.sol @@ -27,6 +27,9 @@ contract SuperchainWETH is WETH98, IERC7802, ISemver { /// @notice Thrown when attempting to relay a message and the cross domain message sender is not SuperchainWETH. error InvalidCrossDomainSender(); + /// @notice Thrown when trying to approve Permit2 with a non-infinite allowance. + error Permit2AllowanceIsFixedAtInfinity(); + /// @notice Emitted when ETH is sent from one chain to another. /// @param from Address of the sender. /// @param to Address of the recipient. @@ -67,6 +70,12 @@ contract SuperchainWETH is WETH98, IERC7802, ISemver { emit Transfer(_from, address(0), _amount); } + /// @inheritdoc WETH98 + function approve(address guy, uint256 wad) public virtual override returns (bool) { + if (guy == Preinstalls.Permit2 && wad != type(uint256).max) revert Permit2AllowanceIsFixedAtInfinity(); + return super.approve(guy, wad); + } + /// @notice Allows the SuperchainTokenBridge to mint tokens. /// @param _to Address to mint tokens to. /// @param _amount Amount of tokens to mint. diff --git a/packages/contracts-bedrock/src/universal/WETH98.sol b/packages/contracts-bedrock/src/universal/WETH98.sol index d0b597125b9..6c066ca7ff0 100644 --- a/packages/contracts-bedrock/src/universal/WETH98.sol +++ b/packages/contracts-bedrock/src/universal/WETH98.sol @@ -114,7 +114,7 @@ contract WETH98 { /// @param guy The address that is approved to transfer the WETH. /// @param wad The amount that is approved to transfer. /// @return True if the approval was successful. - function approve(address guy, uint256 wad) external returns (bool) { + function approve(address guy, uint256 wad) public virtual returns (bool) { _allowance[msg.sender][guy] = wad; emit Approval(msg.sender, guy, wad); return true; diff --git a/packages/contracts-bedrock/test/L2/CrossL2Inbox.t.sol b/packages/contracts-bedrock/test/L2/CrossL2Inbox.t.sol index c648e5b110f..492eeab8613 100644 --- a/packages/contracts-bedrock/test/L2/CrossL2Inbox.t.sol +++ b/packages/contracts-bedrock/test/L2/CrossL2Inbox.t.sol @@ -29,6 +29,7 @@ contract CrossL2InboxTest is CommonTest { crossL2Inbox = ICrossL2Inbox(Predeploys.CROSS_L2_INBOX); } + /// Tests that validateMessage succeeds for a non-deposit transaction. function testFuzz_validateMessage_succeeds(Identifier memory _id, bytes32 _messageHash) external { // Ensure is not a deposit transaction vm.mockCall({ @@ -45,6 +46,7 @@ contract CrossL2InboxTest is CommonTest { crossL2Inbox.validateMessage(_id, _messageHash); } + /// Tests that validateMessage reverts for a deposit transaction. function testFuzz_validateMessage_isDeposit_reverts(Identifier calldata _id, bytes32 _messageHash) external { // Ensure it is a deposit transaction vm.mockCall({ diff --git a/packages/contracts-bedrock/test/L2/SuperchainWETH.t.sol b/packages/contracts-bedrock/test/L2/SuperchainWETH.t.sol index e122e79794a..610b052430d 100644 --- a/packages/contracts-bedrock/test/L2/SuperchainWETH.t.sol +++ b/packages/contracts-bedrock/test/L2/SuperchainWETH.t.sol @@ -19,25 +19,22 @@ import { IL2ToL2CrossDomainMessenger } from "interfaces/L2/IL2ToL2CrossDomainMes /// @title SuperchainWETH_Test /// @notice Contract for testing the SuperchainWETH contract. contract SuperchainWETH_Test is CommonTest { - /// @notice Emitted when a transfer is made. event Transfer(address indexed src, address indexed dst, uint256 wad); - /// @notice Emitted when a deposit is made. event Deposit(address indexed dst, uint256 wad); - /// @notice Emitted when a withdrawal is made. event Withdrawal(address indexed src, uint256 wad); - /// @notice Emitted when a crosschain transfer mints tokens. event CrosschainMint(address indexed to, uint256 amount, address indexed sender); - /// @notice Emitted when a crosschain transfer burns tokens. event CrosschainBurn(address indexed from, uint256 amount, address indexed sender); event SendETH(address indexed from, address indexed to, uint256 amount, uint256 destination); event RelayETH(address indexed from, address indexed to, uint256 amount, uint256 source); + event Approval(address indexed src, address indexed guy, uint256 wad); + address internal constant ZERO_ADDRESS = address(0); /// @notice Test setup. @@ -296,6 +293,36 @@ contract SuperchainWETH_Test is CommonTest { assertEq(_allowance, _wad); } + /// @notice Tests that the `approve` function reverts when the spender is Permit2 and the allowance is not infinite. + function testFuzz_approve_permit2NonInfiniteAllowance_reverts(uint256 _wad) public { + vm.assume(_wad != type(uint256).max); + vm.expectRevert(ISuperchainWETH.Permit2AllowanceIsFixedAtInfinity.selector); + superchainWeth.approve(Preinstalls.Permit2, _wad); + } + + /// @notice Tests that the `approve` function succeeds when the spender is Permit2 and the allowance is infinite. + function testFuzz_approve_permit2InfiniteAllowance_succeeds(address _src) public { + vm.expectEmit(address(superchainWeth)); + emit Approval(_src, Preinstalls.Permit2, type(uint256).max); + + vm.prank(_src); + superchainWeth.approve(Preinstalls.Permit2, type(uint256).max); + assertEq(superchainWeth.allowance(_src, Preinstalls.Permit2), type(uint256).max); + } + + /// @notice Tests that the `approve` function succeeds correctly updating the allowance. + function testFuzz_approve_succeeds(address _src, address _guy, uint256 _wad) public { + if (_guy == Preinstalls.Permit2) _wad = type(uint256).max; + + vm.expectEmit(address(superchainWeth)); + emit Approval(_src, _guy, _wad); + + vm.prank(_src); + superchainWeth.approve(_guy, _wad); + + assertEq(superchainWeth.allowance(_src, _guy), _wad); + } + /// @notice Tests that `transferFrom` works when the caller (spender) is Permit2, without any explicit approval. /// @param _src The funds owner. /// @param _dst The address of the recipient.