From 9e1f077f7f9676f08649efbeba3cd633f5ce8280 Mon Sep 17 00:00:00 2001 From: Mark Tyneway Date: Thu, 12 Sep 2024 19:48:05 -0600 Subject: [PATCH] feat: mintable with permit (#11868) * contracts-bedrock: add permit functionality to mintable erc20 Adds `permit` functionality to `OptimismMintableERC20` tokens that are deployed by the `OptimismMintableERC20Factory`. * feat: max approve permit2 in ERC20Mintable This commit max approves permit2 automatically for all holders of the ERC20 bridge token. This enables all users on OP stack chains to skip an approval step for actions on protocols utilizing permit2 e.g. Uniswap * fix: constants first * semver-lock: update * snapshots: fix * contracts: add natspec * semver-lock: update * contracts-bedrock: fix nits * snapshots: update --------- Co-authored-by: Mark Toda --- packages/contracts-bedrock/semver-lock.json | 8 +- .../snapshots/abi/OptimismMintableERC20.json | 92 ++++++++++++++++++- .../storageLayout/OptimismMintableERC20.json | 14 +++ .../src/universal/OptimismMintableERC20.sol | 49 +++++++--- .../OptimismMintableERC20Factory.sol | 2 +- .../universal/OptimismMintableERC20.t.sol | 14 +++ 6 files changed, 159 insertions(+), 20 deletions(-) diff --git a/packages/contracts-bedrock/semver-lock.json b/packages/contracts-bedrock/semver-lock.json index 1d3f5c04399f..7013535f0ee8 100644 --- a/packages/contracts-bedrock/semver-lock.json +++ b/packages/contracts-bedrock/semver-lock.json @@ -208,12 +208,12 @@ "sourceCodeHash": "0xd1479c60087f352385b6d5379ef3cc07839f671d617626b4c94ece91da781ef2" }, "src/universal/OptimismMintableERC20.sol": { - "initCodeHash": "0xfc77e4db406c232d8b84a3f77b939fb08fa27852faa5f4b0d78d998402caf308", - "sourceCodeHash": "0xd7957c662ef03fc0cc3440a6ec6737a55f90b52a977262a260cd99fe96494267" + "initCodeHash": "0x28c88484e1932253d6d12954492ac8a70744dc15c84429089af9944e5b158fd9", + "sourceCodeHash": "0x740b4043436d1b314ee3ba145acfcde60b6abd8416ea594f2b8e890b5d0bce6b" }, "src/universal/OptimismMintableERC20Factory.sol": { - "initCodeHash": "0x1cc94179ce28fb34c8e28b8d2015b95588e93a45730dae9ee7da859a9f66e0e6", - "sourceCodeHash": "0x46d1d4a9ed1b1f4c60d42bf6c9982ffc72cbd759a4aae5246f89ccbb8699c2a1" + "initCodeHash": "0x3ebd2297c0af2856a432daf29d186d0751f7edb1c777abbe136953038cf5d1ba", + "sourceCodeHash": "0xf8425f65eb5520d55710907d67a9d6fa277263285e1b79ba299815d1c76919a3" }, "src/universal/OptimismMintableERC721.sol": { "initCodeHash": "0x5a995fc043f8268a6d5c6284ad85b0de21328cd47277114aeba2c03484deaf91", diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismMintableERC20.json b/packages/contracts-bedrock/snapshots/abi/OptimismMintableERC20.json index 3c6f5e9ab348..5a5763c73962 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismMintableERC20.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismMintableERC20.json @@ -43,6 +43,32 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "DOMAIN_SEPARATOR", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "PERMIT2", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "pure", + "type": "function" + }, { "inputs": [], "name": "REMOTE_TOKEN", @@ -60,12 +86,12 @@ "inputs": [ { "internalType": "address", - "name": "owner", + "name": "_owner", "type": "address" }, { "internalType": "address", - "name": "spender", + "name": "_spender", "type": "address" } ], @@ -272,6 +298,68 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "nonces", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "permit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "remoteToken", diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OptimismMintableERC20.json b/packages/contracts-bedrock/snapshots/storageLayout/OptimismMintableERC20.json index 418a98546cf7..2ad020cb1c4f 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/OptimismMintableERC20.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/OptimismMintableERC20.json @@ -33,5 +33,19 @@ "offset": 0, "slot": "4", "type": "string" + }, + { + "bytes": "32", + "label": "_nonces", + "offset": 0, + "slot": "5", + "type": "mapping(address => struct Counters.Counter)" + }, + { + "bytes": "32", + "label": "_PERMIT_TYPEHASH_DEPRECATED_SLOT", + "offset": 0, + "slot": "6", + "type": "bytes32" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/src/universal/OptimismMintableERC20.sol b/packages/contracts-bedrock/src/universal/OptimismMintableERC20.sol index dd207caf167f..838a21af5662 100644 --- a/packages/contracts-bedrock/src/universal/OptimismMintableERC20.sol +++ b/packages/contracts-bedrock/src/universal/OptimismMintableERC20.sol @@ -2,9 +2,11 @@ pragma solidity 0.8.15; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { ERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20Permit.sol"; import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import { ILegacyMintableERC20, IOptimismMintableERC20 } from "src/universal/interfaces/IOptimismMintableERC20.sol"; import { ISemver } from "src/universal/interfaces/ISemver.sol"; +import { Preinstalls } from "src/libraries/Preinstalls.sol"; /// @title OptimismMintableERC20 /// @notice OptimismMintableERC20 is a standard extension of the base ERC20 token contract designed @@ -12,7 +14,7 @@ import { ISemver } from "src/universal/interfaces/ISemver.sol"; /// use an OptimismMintablERC20 as the L2 representation of an L1 token, or vice-versa. /// Designed to be backwards compatible with the older StandardL2ERC20 token which was only /// meant for use on L2. -contract OptimismMintableERC20 is IOptimismMintableERC20, ILegacyMintableERC20, ERC20, ISemver { +contract OptimismMintableERC20 is IOptimismMintableERC20, ILegacyMintableERC20, ERC20Permit, ISemver { /// @notice Address of the corresponding version of this token on the remote chain. address public immutable REMOTE_TOKEN; @@ -39,8 +41,16 @@ contract OptimismMintableERC20 is IOptimismMintableERC20, ILegacyMintableERC20, } /// @notice Semantic version. - /// @custom:semver 1.3.1-beta.1 - string public constant version = "1.3.1-beta.1"; + /// @custom:semver 1.4.0-beta.1 + string public constant version = "1.4.0-beta.1"; + + /// @notice Getter function for the permit2 address. It deterministically deployed + /// so it will always be at the same address. It is also included as a preinstall, + /// so it exists in the genesis state of chains. + /// @return Address of permit2 on this network. + function PERMIT2() public pure returns (address) { + return Preinstalls.Permit2; + } /// @param _bridge Address of the L2 standard bridge. /// @param _remoteToken Address of the corresponding L1 token. @@ -54,12 +64,35 @@ contract OptimismMintableERC20 is IOptimismMintableERC20, ILegacyMintableERC20, uint8 _decimals ) ERC20(_name, _symbol) + ERC20Permit(_name) { REMOTE_TOKEN = _remoteToken; BRIDGE = _bridge; DECIMALS = _decimals; } + /// @dev Returns the number of decimals used to get its user representation. + /// For example, if `decimals` equals `2`, a balance of `505` tokens should + /// be displayed to a user as `5.05` (`505 / 10 ** 2`). + /// NOTE: This information is only used for _display_ purposes: it in + /// no way affects any of the arithmetic of the contract, including + /// {IERC20-balanceOf} and {IERC20-transfer}. + function decimals() public view override returns (uint8) { + return DECIMALS; + } + + /// @notice Returns the allowance for a spender on the owner's tokens. + /// If the spender is the permit2 address, returns the maximum uint256 value. + /// @param _owner owner of the tokens. + /// @param _spender spender of the tokens. + /// @return Allowance for the spender. + function allowance(address _owner, address _spender) public view override returns (uint256) { + if (_spender == PERMIT2()) { + return type(uint256).max; + } + return super.allowance(_owner, _spender); + } + /// @notice Allows the StandardBridge on this network to mint tokens. /// @param _to Address to mint tokens to. /// @param _amount Amount of tokens to mint. @@ -127,14 +160,4 @@ contract OptimismMintableERC20 is IOptimismMintableERC20, ILegacyMintableERC20, function bridge() public view returns (address) { return BRIDGE; } - - /// @dev Returns the number of decimals used to get its user representation. - /// For example, if `decimals` equals `2`, a balance of `505` tokens should - /// be displayed to a user as `5.05` (`505 / 10 ** 2`). - /// NOTE: This information is only used for _display_ purposes: it in - /// no way affects any of the arithmetic of the contract, including - /// {IERC20-balanceOf} and {IERC20-transfer}. - function decimals() public view override returns (uint8) { - return DECIMALS; - } } diff --git a/packages/contracts-bedrock/src/universal/OptimismMintableERC20Factory.sol b/packages/contracts-bedrock/src/universal/OptimismMintableERC20Factory.sol index 790c1f828dfb..6ae271603312 100644 --- a/packages/contracts-bedrock/src/universal/OptimismMintableERC20Factory.sol +++ b/packages/contracts-bedrock/src/universal/OptimismMintableERC20Factory.sol @@ -49,7 +49,7 @@ contract OptimismMintableERC20Factory is ISemver, Initializable, IOptimismERC20F /// is responsible for deploying OptimismMintableERC20 contracts. /// @notice Semantic version. /// @custom:semver 1.10.1-beta.1 - string public constant version = "1.10.1-beta.1"; + string public constant version = "1.10.1-beta.2"; /// @notice Constructs the OptimismMintableERC20Factory contract. constructor() { diff --git a/packages/contracts-bedrock/test/universal/OptimismMintableERC20.t.sol b/packages/contracts-bedrock/test/universal/OptimismMintableERC20.t.sol index fcacc13a05f7..1e84ce2958cf 100644 --- a/packages/contracts-bedrock/test/universal/OptimismMintableERC20.t.sol +++ b/packages/contracts-bedrock/test/universal/OptimismMintableERC20.t.sol @@ -46,6 +46,20 @@ contract OptimismMintableERC20_Test is Bridge_Initializer { assertEq(L2Token.balanceOf(alice), 100); } + function test_allowance_permit2_max() external view { + assertEq(L2Token.allowance(alice, L2Token.PERMIT2()), type(uint256).max); + } + + function test_permit2_transferFrom() external { + vm.prank(address(l2StandardBridge)); + L2Token.mint(alice, 100); + + assertEq(L2Token.balanceOf(bob), 0); + vm.prank(L2Token.PERMIT2()); + L2Token.transferFrom(alice, bob, 100); + assertEq(L2Token.balanceOf(bob), 100); + } + function test_mint_notBridge_reverts() external { // NOT the bridge vm.expectRevert("OptimismMintableERC20: only bridge can mint and burn");