diff --git a/.gitmodules b/.gitmodules index 21ecaedbb77a..5422163a9599 100644 --- a/.gitmodules +++ b/.gitmodules @@ -29,3 +29,6 @@ [submodule "packages/contracts-bedrock/lib/openzeppelin-contracts-v5"] path = packages/contracts-bedrock/lib/openzeppelin-contracts-v5 url = https://github.com/OpenZeppelin/openzeppelin-contracts +[submodule "packages/contracts-bedrock/lib/solady-v0.0.245"] + path = packages/contracts-bedrock/lib/solady-v0.0.245 + url = https://github.com/vectorized/solady diff --git a/packages/contracts-bedrock/foundry.toml b/packages/contracts-bedrock/foundry.toml index 86e7dcfe9511..b173f71abc33 100644 --- a/packages/contracts-bedrock/foundry.toml +++ b/packages/contracts-bedrock/foundry.toml @@ -17,6 +17,7 @@ remappings = [ '@rari-capital/solmate/=lib/solmate', '@lib-keccak/=lib/lib-keccak/contracts/lib', '@solady/=lib/solady/src', + '@solady-v0.0.245/=lib/solady-v0.0.245/src', 'forge-std/=lib/forge-std/src', 'ds-test/=lib/forge-std/lib/ds-test/src', 'safe-contracts/=lib/safe-contracts/contracts', diff --git a/packages/contracts-bedrock/lib/solady-v0.0.245 b/packages/contracts-bedrock/lib/solady-v0.0.245 new file mode 160000 index 000000000000..e0ef35adb0cc --- /dev/null +++ b/packages/contracts-bedrock/lib/solady-v0.0.245 @@ -0,0 +1 @@ +Subproject commit e0ef35adb0ccd1032794731a995cb599bba7b537 diff --git a/packages/contracts-bedrock/semver-lock.json b/packages/contracts-bedrock/semver-lock.json index 203656ed63c2..767a7974b399 100644 --- a/packages/contracts-bedrock/semver-lock.json +++ b/packages/contracts-bedrock/semver-lock.json @@ -108,8 +108,8 @@ "sourceCodeHash": "0x4bb08a8a9d5de37d1fb2dd8a9bcc483305510c32508f0be2d54757e6e257aedb" }, "src/L2/OptimismSuperchainERC20.sol": { - "initCodeHash": "0xd5c84e45746fd741d541a917ddc1cc0c7043c6b21d5c18040d4bc999d6a7b2db", - "sourceCodeHash": "0xf32130f0b46333daba062c50ff6dcfadce1f177ff753bed2374d499ea9c2d98a" + "initCodeHash": "0x4e25579079d73c93f1d494e1976334b77fc4ec181c67f376d8e2613c7b207f52", + "sourceCodeHash": "0xe41cf3b005f1ea007fc1b5f69f630be5f6ef12d6e5e94a50e3160b0ebe0a1613" }, "src/L2/OptimismSuperchainERC20Beacon.sol": { "initCodeHash": "0x23dba3ceb9e58646695c306996c9e15251ac79acc6339c1a93d10a4c79da6dab", @@ -125,7 +125,7 @@ }, "src/L2/SuperchainERC20.sol": { "initCodeHash": "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470", - "sourceCodeHash": "0x75d061633a141af11a19b86e599a1725dfae8d245dcddfb6bb244a50d5e53f96" + "sourceCodeHash": "0x6a384ccfb6f2f7316c1b33873a1630b5179e52475951d31771656e06d2b11519" }, "src/L2/SuperchainTokenBridge.sol": { "initCodeHash": "0xef7590c30630a75f105384e339e52758569c25a5aa0a5934c521e004b8f86220", diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json b/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json index f1b7f83e3b53..d6ad63fad9c3 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismSuperchainERC20.json @@ -570,6 +570,11 @@ "name": "NotInitializing", "type": "error" }, + { + "inputs": [], + "name": "Permit2AllowanceIsFixedAtInfinity", + "type": "error" + }, { "inputs": [], "name": "PermitExpired", diff --git a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol index 6e8ef9057325..a097bb736c84 100644 --- a/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol @@ -59,8 +59,8 @@ contract OptimismSuperchainERC20 is SuperchainERC20, Initializable, ERC165 { } /// @notice Semantic version. - /// @custom:semver 1.0.0-beta.6 - string public constant override version = "1.0.0-beta.6"; + /// @custom:semver 1.0.0-beta.7 + string public constant override version = "1.0.0-beta.7"; /// @notice Constructs the OptimismSuperchainERC20 contract. constructor() { @@ -141,4 +141,9 @@ contract OptimismSuperchainERC20 is SuperchainERC20, Initializable, ERC165 { function supportsInterface(bytes4 _interfaceId) public view virtual override returns (bool) { return _interfaceId == type(IOptimismSuperchainERC20).interfaceId || super.supportsInterface(_interfaceId); } + + /// @notice Sets Permit2 contract's allowance at infinity. + function _givePermit2InfiniteAllowance() internal view virtual override returns (bool) { + return true; + } } diff --git a/packages/contracts-bedrock/src/L2/SuperchainERC20.sol b/packages/contracts-bedrock/src/L2/SuperchainERC20.sol index 9ead2645828b..d723ed8d992b 100644 --- a/packages/contracts-bedrock/src/L2/SuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/SuperchainERC20.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.25; import { ICrosschainERC20 } from "src/L2/interfaces/ICrosschainERC20.sol"; import { ISemver } from "src/universal/interfaces/ISemver.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; -import { ERC20 } from "@solady/tokens/ERC20.sol"; +import { ERC20 } from "@solady-v0.0.245/tokens/ERC20.sol"; import { Unauthorized } from "src/libraries/errors/CommonErrors.sol"; /// @title SuperchainERC20 @@ -19,9 +19,9 @@ abstract contract SuperchainERC20 is ERC20, ICrosschainERC20, ISemver { } /// @notice Semantic version. - /// @custom:semver 1.0.0-beta.1 + /// @custom:semver 1.0.0-beta.2 function version() external view virtual returns (string memory) { - return "1.0.0-beta.1"; + return "1.0.0-beta.2"; } /// @notice Allows the SuperchainTokenBridge to mint tokens. diff --git a/packages/contracts-bedrock/src/L2/interfaces/ISuperchainERC20.sol b/packages/contracts-bedrock/src/L2/interfaces/ISuperchainERC20.sol index 4aad888535a7..ce066a27b88c 100644 --- a/packages/contracts-bedrock/src/L2/interfaces/ISuperchainERC20.sol +++ b/packages/contracts-bedrock/src/L2/interfaces/ISuperchainERC20.sol @@ -3,13 +3,13 @@ pragma solidity ^0.8.0; // Interfaces import { ICrosschainERC20 } from "src/L2/interfaces/ICrosschainERC20.sol"; -import { IERC20Solady } from "src/vendor/interfaces/IERC20Solady.sol"; +import { IERC20Solady as IERC20 } from "src/vendor/interfaces/IERC20Solady.sol"; import { ISemver } from "src/universal/interfaces/ISemver.sol"; /// @title ISuperchainERC20 /// @notice This interface is available on the SuperchainERC20 contract. /// @dev This interface is needed for the abstract SuperchainERC20 implementation but is not part of the standard -interface ISuperchainERC20 is ICrosschainERC20, IERC20Solady, ISemver { +interface ISuperchainERC20 is ICrosschainERC20, IERC20, ISemver { error Unauthorized(); function __constructor__() external; diff --git a/packages/contracts-bedrock/src/vendor/interfaces/IERC20Solady.sol b/packages/contracts-bedrock/src/vendor/interfaces/IERC20Solady.sol index 1e696ad23ac3..b05b906eec97 100644 --- a/packages/contracts-bedrock/src/vendor/interfaces/IERC20Solady.sol +++ b/packages/contracts-bedrock/src/vendor/interfaces/IERC20Solady.sol @@ -23,6 +23,9 @@ interface IERC20Solady { /// @dev The permit has expired. error PermitExpired(); + /// @dev The allowance of Permit2 is fixed at infinity. + error Permit2AllowanceIsFixedAtInfinity(); + /// @dev Emitted when `amount` tokens is transferred from `from` to `to`. event Transfer(address indexed from, address indexed to, uint256 amount); diff --git a/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol index 87d5cbb74b23..825e09bdd808 100644 --- a/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol +++ b/packages/contracts-bedrock/test/L2/OptimismSuperchainERC20.t.sol @@ -7,12 +7,14 @@ import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; // Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; -import { IERC20 } from "@openzeppelin/contracts-v5/token/ERC20/IERC20.sol"; +import { IERC20Solady as IERC20 } from "src/vendor/interfaces/IERC20Solady.sol"; + import { Initializable } from "@openzeppelin/contracts-v5/proxy/utils/Initializable.sol"; import { IERC165 } from "@openzeppelin/contracts-v5/utils/introspection/IERC165.sol"; import { IBeacon } from "@openzeppelin/contracts-v5/proxy/beacon/IBeacon.sol"; import { BeaconProxy } from "@openzeppelin/contracts-v5/proxy/beacon/BeaconProxy.sol"; import { Unauthorized } from "src/libraries/errors/CommonErrors.sol"; +import { Preinstalls } from "src/libraries/Preinstalls.sol"; // Target contract import { OptimismSuperchainERC20 } from "src/L2/OptimismSuperchainERC20.sol"; @@ -252,4 +254,64 @@ contract OptimismSuperchainERC20Test is Test { vm.assume(_interfaceId != type(IOptimismSuperchainERC20).interfaceId); assertFalse(optimismSuperchainERC20.supportsInterface(_interfaceId)); } + + /// @notice Tests that the allowance function returns the max uint256 value when the spender is Permit. + /// @param _randomCaller The address that will call the function - used to fuzz better since the behaviour should be + /// the same regardless of the caller. + /// @param _owner The funds owner. + function testFuzz_allowance_fromPermit2_succeeds(address _randomCaller, address _owner) public { + vm.prank(_randomCaller); + uint256 _allowance = optimismSuperchainERC20.allowance(_owner, Preinstalls.Permit2); + + assertEq(_allowance, type(uint256).max); + } + + /// @notice Tests that the allowance function returns the correct allowance when the spender is not Permit. + /// @param _randomCaller The address that will call the function - used to fuzz better + /// since the behaviour should be the same regardless of the caller. + /// @param _owner The funds owner. + /// @param _guy The address of the spender - It cannot be Permit2. + function testFuzz_allowance_succeeds(address _randomCaller, address _owner, address _guy, uint256 _amount) public { + // Assume + vm.assume(_guy != Preinstalls.Permit2); + + // Arrange + vm.prank(_owner); + optimismSuperchainERC20.approve(_guy, _amount); + + // Act + vm.prank(_randomCaller); + uint256 _allowance = optimismSuperchainERC20.allowance(_owner, _guy); + + // Assert + assertEq(_allowance, _amount); + } + + /// @notice Tests that `transferFrom` works when the caller (spender) is Permit2, without any explicit approval. + /// @param _owner The funds owner. + /// @param _recipient The address of the recipient. + /// @param _amount The amount of tokens to transfer. + function testFuzz_transferFrom_whenPermit2IsCaller_succeeds( + address _owner, + address _recipient, + uint256 _amount + ) + public + { + // Arrange + deal(address(optimismSuperchainERC20), _owner, _amount); + + vm.expectEmit(address(optimismSuperchainERC20)); + emit IERC20.Transfer(_owner, _recipient, _amount); + + // Act + vm.prank(Preinstalls.Permit2); + optimismSuperchainERC20.transferFrom(_owner, _recipient, _amount); + + // Assert + assertEq(optimismSuperchainERC20.balanceOf(_recipient), _amount); + // Handle the case where the source and destination are the same to check the source balance. + if (_owner != _recipient) assertEq(optimismSuperchainERC20.balanceOf(_owner), 0); + else assertEq(optimismSuperchainERC20.balanceOf(_owner), _amount); + } } diff --git a/packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol b/packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol index 999b0ad4ee88..07b92ae3ca7d 100644 --- a/packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol +++ b/packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol @@ -6,7 +6,7 @@ import { Test } from "forge-std/Test.sol"; // Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; -import { IERC20 } from "@openzeppelin/contracts-v5/token/ERC20/IERC20.sol"; +import { IERC20Solady as IERC20 } from "src/vendor/interfaces/IERC20Solady.sol"; // Target contract import { SuperchainERC20 } from "src/L2/SuperchainERC20.sol";