From 89a7982443519a92979647cd8300fa2efa6bb84f Mon Sep 17 00:00:00 2001 From: alvrs Date: Wed, 24 Apr 2024 19:14:35 +0100 Subject: [PATCH 01/21] add OptimismFiatTokenV2_2 --- .env.example | 6 ++++ contracts/v2/IOptimismMintableERC20.sol | 21 +++++++++++ contracts/v2/OptimismFiatTokenV2_2.sol | 43 ++++++++++++++++++++++ package.json | 1 - scripts/deploy/DeployImpl.sol | 48 +++++++++++++++++++++++++ scripts/deploy/deploy-fiat-token.s.sol | 25 ++++++++++++- 6 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 contracts/v2/IOptimismMintableERC20.sol create mode 100644 contracts/v2/OptimismFiatTokenV2_2.sol diff --git a/.env.example b/.env.example index 0f0a948d9..c341f360a 100644 --- a/.env.example +++ b/.env.example @@ -79,3 +79,9 @@ BLACKLIST_FILE_NAME=blacklist.remote.json # [OPTIONAL] The API key to an Etherscan flavor block explorer. # ETHERSCAN_KEY= + +# The address of the Optimism L2StandardBridge +L2_STANDARD_BRIDGE= + +# The address of the corresponding token contract on L1 +L1_REMOTE_TOKEN= \ No newline at end of file diff --git a/contracts/v2/IOptimismMintableERC20.sol b/contracts/v2/IOptimismMintableERC20.sol new file mode 100644 index 000000000..67df247ed --- /dev/null +++ b/contracts/v2/IOptimismMintableERC20.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.6.12; + +import { IERC165 } from "@openzeppelin/contracts/introspection/IERC165.sol"; + +/** + * @title IOptimismMintableERC20 + * @notice This interface is available on the OptimismMintableERC20 contract. + * We declare it as a separate interface so that it can be used in + * custom implementations of OptimismMintableERC20. + * @notice From https://github.com/ethereum-optimism/optimism/blob/6b231760b3f352d5c4f6df8431b67d836f316f84/packages/contracts-bedrock/src/universal/IOptimismMintableERC20.sol#L10-L18 + */ +interface IOptimismMintableERC20 is IERC165 { + function remoteToken() external view returns (address); + + function bridge() external returns (address); + + function mint(address _to, uint256 _amount) external; + + function burn(address _from, uint256 _amount) external; +} diff --git a/contracts/v2/OptimismFiatTokenV2_2.sol b/contracts/v2/OptimismFiatTokenV2_2.sol new file mode 100644 index 000000000..a7eadb9dd --- /dev/null +++ b/contracts/v2/OptimismFiatTokenV2_2.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.6.12; + +import { FiatTokenV1 } from "../v1/FiatTokenV1.sol"; +import { FiatTokenV2_2 } from "./FiatTokenV2_2.sol"; +import { IOptimismMintableERC20 } from "./IOptimismMintableERC20.sol"; +import { IERC165 } from "@openzeppelin/contracts/introspection/IERC165.sol"; + +/** + * @title OptimismFiatTokenV2_2 + * @author Lattice (https://lattice.xyz) + * @notice Adds compatibility with IOptimismMintableERC20 to the Bridged USDC Standard, + * so it can be used with Optimism's StandardBridge. + * @dev This contract does not extend `IOptimismMintableERC20` to avoid the requirement to override `mint` and `burn` functions. + */ +contract OptimismFiatTokenV2_2 is FiatTokenV2_2, IERC165 { + address private immutable l1RemoteToken; + address private immutable l2StandardBridge; + + constructor(address _l1RemoteToken, address _l2StandardBridge) public { + l1RemoteToken = _l1RemoteToken; + l2StandardBridge = _l2StandardBridge; + } + + function remoteToken() external view returns (address) { + return l1RemoteToken; + } + + function bridge() external view returns (address) { + return l2StandardBridge; + } + + function supportsInterface(bytes4 interfaceId) + external + override + view + returns (bool) + { + return + interfaceId == type(IOptimismMintableERC20).interfaceId || + interfaceId == type(IERC165).interfaceId; + } +} diff --git a/package.json b/package.json index 435457790..188eb6d5c 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,6 @@ "url": "https://github.com/circlefin/stablecoin-evm/issues" }, "homepage": "https://github.com/circlefin/stablecoin-evm#readme", - "dependencies": {}, "devDependencies": { "@nomicfoundation/hardhat-chai-matchers": "2.0.2", "@nomicfoundation/hardhat-ethers": "3.0.4", diff --git a/scripts/deploy/DeployImpl.sol b/scripts/deploy/DeployImpl.sol index ea4bf00ad..f6152d453 100644 --- a/scripts/deploy/DeployImpl.sol +++ b/scripts/deploy/DeployImpl.sol @@ -19,6 +19,9 @@ pragma solidity 0.6.12; import { FiatTokenV2_2 } from "../../contracts/v2/FiatTokenV2_2.sol"; +import { + OptimismFiatTokenV2_2 +} from "../../contracts/v2/OptimismFiatTokenV2_2.sol"; /** * @notice A utility contract that exposes a re-useable getOrDeployImpl function. @@ -63,4 +66,49 @@ contract DeployImpl { return fiatTokenV2_2; } + + /** + * @notice helper function that either + * 1) deploys the implementation contract if the input is the zero address, or + * 2) loads an instance of an existing contract when input is not the zero address. + * + * @param impl configured of the implementation contract, where address(0) represents a new instance should be deployed + * @return FiatTokenV2_2 newly deployed or loaded instance + */ + function getOrDeployImpl( + address impl, + address l1RemoteToken, + address l2StandardBridge + ) internal returns (FiatTokenV2_2) { + OptimismFiatTokenV2_2 fiatTokenV2_2; + + if (impl == address(0)) { + fiatTokenV2_2 = new OptimismFiatTokenV2_2({ + _l1RemoteToken: l1RemoteToken, + _l2StandardBridge: l2StandardBridge + }); + + // Initializing the implementation contract with dummy values here prevents + // the contract from being reinitialized later on with different values. + // Dummy values can be used here as the proxy contract will store the actual values + // for the deployed token. + fiatTokenV2_2.initialize( + "", + "", + "", + 0, + THROWAWAY_ADDRESS, + THROWAWAY_ADDRESS, + THROWAWAY_ADDRESS, + THROWAWAY_ADDRESS + ); + fiatTokenV2_2.initializeV2(""); + fiatTokenV2_2.initializeV2_1(THROWAWAY_ADDRESS); + fiatTokenV2_2.initializeV2_2(new address[](0), ""); + } else { + fiatTokenV2_2 = OptimismFiatTokenV2_2(impl); + } + + return fiatTokenV2_2; + } } diff --git a/scripts/deploy/deploy-fiat-token.s.sol b/scripts/deploy/deploy-fiat-token.s.sol index 24d1fbded..c899dd6c3 100644 --- a/scripts/deploy/deploy-fiat-token.s.sol +++ b/scripts/deploy/deploy-fiat-token.s.sol @@ -23,6 +23,9 @@ import { Script } from "forge-std/Script.sol"; import { DeployImpl } from "./DeployImpl.sol"; import { FiatTokenProxy } from "../../contracts/v1/FiatTokenProxy.sol"; import { FiatTokenV2_2 } from "../../contracts/v2/FiatTokenV2_2.sol"; +import { + OptimismFiatTokenV2_2 +} from "../../contracts/v2/OptimismFiatTokenV2_2.sol"; import { MasterMinter } from "../../contracts/minting/MasterMinter.sol"; /** @@ -49,6 +52,9 @@ contract DeployFiatToken is Script, DeployImpl { uint256 private deployerPrivateKey; + address l1RemoteToken; + address l2StandardBridge; + /** * @notice initialize variables from environment */ @@ -70,6 +76,9 @@ contract DeployFiatToken is Script, DeployImpl { deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); + l1RemoteToken = vm.envAddress("L1_REMOTE_TOKEN"); + l2StandardBridge = vm.envAddress("L2_STANDARD_BRIDGE"); + console.log("TOKEN_NAME: '%s'", tokenName); console.log("TOKEN_SYMBOL: '%s'", tokenSymbol); console.log("TOKEN_CURRENCY: '%s'", tokenCurrency); @@ -81,6 +90,8 @@ contract DeployFiatToken is Script, DeployImpl { console.log("PAUSER_ADDRESS: '%s'", pauser); console.log("BLACKLISTER_ADDRESS: '%s'", blacklister); console.log("LOST_AND_FOUND_ADDRESS: '%s'", lostAndFound); + console.log("L1_REMOTE_TOKEN: '%s'", l1RemoteToken); + console.log("L2_STANDARD_BRIDGE: '%s'", l2StandardBridge); } /** @@ -99,13 +110,25 @@ contract DeployFiatToken is Script, DeployImpl { // If there is an existing implementation contract, // we can simply point the newly deployed proxy contract to it. // Otherwise, deploy the latest implementation contract code to the network. - FiatTokenV2_2 fiatTokenV2_2 = getOrDeployImpl(_impl); + // If l2StandardBridge and l1RemoteToken are set, deploy an OptimimsFiatToken. + FiatTokenV2_2 fiatTokenV2_2; + if (l1RemoteToken != address(0) && l2StandardBridge != address(0)) { + fiatTokenV2_2 = getOrDeployImpl({ + impl: _impl, + l1RemoteToken: l1RemoteToken, + l2StandardBridge: l2StandardBridge + }); + } else { + fiatTokenV2_2 = getOrDeployImpl(_impl); + } FiatTokenProxy proxy = new FiatTokenProxy(address(fiatTokenV2_2)); // Now that the proxy contract has been deployed, we can deploy the master minter. MasterMinter masterMinter = new MasterMinter(address(proxy)); + // TODO: If l2StandardBridge and l1RemoteToken are set, set the l2StandardBridge as minter. + // Change the master minter to be owned by the master minter owner masterMinter.transferOwnership(masterMinterOwner); From 9997137671f6c93cf0f5415c93e256d31ce1debe Mon Sep 17 00:00:00 2001 From: alvrs Date: Wed, 24 Apr 2024 19:18:01 +0100 Subject: [PATCH 02/21] add natspec --- scripts/deploy/DeployImpl.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/deploy/DeployImpl.sol b/scripts/deploy/DeployImpl.sol index f6152d453..32bc8f4ee 100644 --- a/scripts/deploy/DeployImpl.sol +++ b/scripts/deploy/DeployImpl.sol @@ -73,7 +73,9 @@ contract DeployImpl { * 2) loads an instance of an existing contract when input is not the zero address. * * @param impl configured of the implementation contract, where address(0) represents a new instance should be deployed - * @return FiatTokenV2_2 newly deployed or loaded instance + * @param l1RemoteToken token on the L1 corresponding to this bridged version of the token + * @param l2StandardBridge Optimism L2StandardBridge contract address on the L2 of the bridged token + * @return OptimismFiatTokenV2_2 newly deployed or loaded instance */ function getOrDeployImpl( address impl, From 9e471ede58127d29c0867550359ccb4577a7288f Mon Sep 17 00:00:00 2001 From: alvrs Date: Wed, 24 Apr 2024 19:27:06 +0100 Subject: [PATCH 03/21] use predeploy bridge --- .env.example | 3 --- contracts/v2/OptimismFiatTokenV2_2.sol | 10 +++++----- scripts/deploy/DeployImpl.sol | 15 +++++---------- scripts/deploy/deploy-fiat-token.s.sol | 12 +++--------- 4 files changed, 13 insertions(+), 27 deletions(-) diff --git a/.env.example b/.env.example index c341f360a..8b2254c25 100644 --- a/.env.example +++ b/.env.example @@ -80,8 +80,5 @@ BLACKLIST_FILE_NAME=blacklist.remote.json # [OPTIONAL] The API key to an Etherscan flavor block explorer. # ETHERSCAN_KEY= -# The address of the Optimism L2StandardBridge -L2_STANDARD_BRIDGE= - # The address of the corresponding token contract on L1 L1_REMOTE_TOKEN= \ No newline at end of file diff --git a/contracts/v2/OptimismFiatTokenV2_2.sol b/contracts/v2/OptimismFiatTokenV2_2.sol index a7eadb9dd..9446db068 100644 --- a/contracts/v2/OptimismFiatTokenV2_2.sol +++ b/contracts/v2/OptimismFiatTokenV2_2.sol @@ -15,19 +15,19 @@ import { IERC165 } from "@openzeppelin/contracts/introspection/IERC165.sol"; */ contract OptimismFiatTokenV2_2 is FiatTokenV2_2, IERC165 { address private immutable l1RemoteToken; - address private immutable l2StandardBridge; - constructor(address _l1RemoteToken, address _l2StandardBridge) public { + constructor(address _l1RemoteToken) public { l1RemoteToken = _l1RemoteToken; - l2StandardBridge = _l2StandardBridge; } function remoteToken() external view returns (address) { return l1RemoteToken; } - function bridge() external view returns (address) { - return l2StandardBridge; + function bridge() external pure returns (address) { + // OP Stack L2StandardBridge predeploy + // https://specs.optimism.io/protocol/predeploys.html + return address(0x4200000000000000000000000000000000000010); } function supportsInterface(bytes4 interfaceId) diff --git a/scripts/deploy/DeployImpl.sol b/scripts/deploy/DeployImpl.sol index 32bc8f4ee..cb91ed95a 100644 --- a/scripts/deploy/DeployImpl.sol +++ b/scripts/deploy/DeployImpl.sol @@ -74,21 +74,16 @@ contract DeployImpl { * * @param impl configured of the implementation contract, where address(0) represents a new instance should be deployed * @param l1RemoteToken token on the L1 corresponding to this bridged version of the token - * @param l2StandardBridge Optimism L2StandardBridge contract address on the L2 of the bridged token * @return OptimismFiatTokenV2_2 newly deployed or loaded instance */ - function getOrDeployImpl( - address impl, - address l1RemoteToken, - address l2StandardBridge - ) internal returns (FiatTokenV2_2) { + function getOrDeployImpl(address impl, address l1RemoteToken) + internal + returns (FiatTokenV2_2) + { OptimismFiatTokenV2_2 fiatTokenV2_2; if (impl == address(0)) { - fiatTokenV2_2 = new OptimismFiatTokenV2_2({ - _l1RemoteToken: l1RemoteToken, - _l2StandardBridge: l2StandardBridge - }); + fiatTokenV2_2 = new OptimismFiatTokenV2_2(l1RemoteToken); // Initializing the implementation contract with dummy values here prevents // the contract from being reinitialized later on with different values. diff --git a/scripts/deploy/deploy-fiat-token.s.sol b/scripts/deploy/deploy-fiat-token.s.sol index c899dd6c3..e2676544d 100644 --- a/scripts/deploy/deploy-fiat-token.s.sol +++ b/scripts/deploy/deploy-fiat-token.s.sol @@ -77,7 +77,6 @@ contract DeployFiatToken is Script, DeployImpl { deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); l1RemoteToken = vm.envAddress("L1_REMOTE_TOKEN"); - l2StandardBridge = vm.envAddress("L2_STANDARD_BRIDGE"); console.log("TOKEN_NAME: '%s'", tokenName); console.log("TOKEN_SYMBOL: '%s'", tokenSymbol); @@ -91,7 +90,6 @@ contract DeployFiatToken is Script, DeployImpl { console.log("BLACKLISTER_ADDRESS: '%s'", blacklister); console.log("LOST_AND_FOUND_ADDRESS: '%s'", lostAndFound); console.log("L1_REMOTE_TOKEN: '%s'", l1RemoteToken); - console.log("L2_STANDARD_BRIDGE: '%s'", l2StandardBridge); } /** @@ -110,14 +108,10 @@ contract DeployFiatToken is Script, DeployImpl { // If there is an existing implementation contract, // we can simply point the newly deployed proxy contract to it. // Otherwise, deploy the latest implementation contract code to the network. - // If l2StandardBridge and l1RemoteToken are set, deploy an OptimimsFiatToken. + // If l1RemoteToken is set, deploy an OptimimsFiatToken. FiatTokenV2_2 fiatTokenV2_2; - if (l1RemoteToken != address(0) && l2StandardBridge != address(0)) { - fiatTokenV2_2 = getOrDeployImpl({ - impl: _impl, - l1RemoteToken: l1RemoteToken, - l2StandardBridge: l2StandardBridge - }); + if (l1RemoteToken != address(0)) { + fiatTokenV2_2 = getOrDeployImpl(_impl, l1RemoteToken); } else { fiatTokenV2_2 = getOrDeployImpl(_impl); } From adcd09ded205d94ba3b49dd94bc1d199eeb74617 Mon Sep 17 00:00:00 2001 From: alvrs Date: Wed, 24 Apr 2024 19:30:46 +0100 Subject: [PATCH 04/21] call parent constructor --- contracts/v2/OptimismFiatTokenV2_2.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/v2/OptimismFiatTokenV2_2.sol b/contracts/v2/OptimismFiatTokenV2_2.sol index 9446db068..c1807938c 100644 --- a/contracts/v2/OptimismFiatTokenV2_2.sol +++ b/contracts/v2/OptimismFiatTokenV2_2.sol @@ -16,7 +16,7 @@ import { IERC165 } from "@openzeppelin/contracts/introspection/IERC165.sol"; contract OptimismFiatTokenV2_2 is FiatTokenV2_2, IERC165 { address private immutable l1RemoteToken; - constructor(address _l1RemoteToken) public { + constructor(address _l1RemoteToken) public FiatTokenV2_2() { l1RemoteToken = _l1RemoteToken; } From 224bba538de274f16b7ecda7c3fdfa043056a3c8 Mon Sep 17 00:00:00 2001 From: alvrs Date: Wed, 24 Apr 2024 21:46:25 +0100 Subject: [PATCH 05/21] add tests --- @types/AnyFiatTokenV2Instance.d.ts | 12 +- test/helpers/storageSlots.behavior.ts | 9 +- test/v2/OptimismFiatTokenV2_2.test.ts | 307 ++++++++++++++++++++++++++ 3 files changed, 325 insertions(+), 3 deletions(-) create mode 100644 test/v2/OptimismFiatTokenV2_2.test.ts diff --git a/@types/AnyFiatTokenV2Instance.d.ts b/@types/AnyFiatTokenV2Instance.d.ts index ad49b8938..3182e98a4 100644 --- a/@types/AnyFiatTokenV2Instance.d.ts +++ b/@types/AnyFiatTokenV2Instance.d.ts @@ -19,6 +19,7 @@ import { FiatTokenV2Instance } from "./generated/FiatTokenV2"; import { FiatTokenV2_1Instance } from "./generated/FiatTokenV2_1"; import { FiatTokenV2_2Instance } from "./generated/FiatTokenV2_2"; +import { OptimismFiatTokenV2_2Instance } from "./generated/OptimismFiatTokenV2_2"; export interface FiatTokenV2_2InstanceExtended extends FiatTokenV2_2Instance { permit?: typeof FiatTokenV2Instance.permit; @@ -27,7 +28,16 @@ export interface FiatTokenV2_2InstanceExtended extends FiatTokenV2_2Instance { cancelAuthorization?: typeof FiatTokenV2Instance.cancelAuthorization; } +export interface OptimismFiatTokenV2_2InstanceExtended + extends OptimismFiatTokenV2_2Instance { + permit?: typeof FiatTokenV2Instance.permit; + transferWithAuthorization?: typeof FiatTokenV2Instance.transferWithAuthorization; + receiveWithAuthorization?: typeof FiatTokenV2Instance.receiveWithAuthorization; + cancelAuthorization?: typeof FiatTokenV2Instance.cancelAuthorization; +} + export type AnyFiatTokenV2Instance = | FiatTokenV2Instance | FiatTokenV2_1Instance - | FiatTokenV2_2InstanceExtended; + | FiatTokenV2_2InstanceExtended + | OptimismFiatTokenV2_2InstanceExtended; diff --git a/test/helpers/storageSlots.behavior.ts b/test/helpers/storageSlots.behavior.ts index 6f36d765a..809d8fa82 100644 --- a/test/helpers/storageSlots.behavior.ts +++ b/test/helpers/storageSlots.behavior.ts @@ -26,6 +26,7 @@ const FiatTokenV1_1 = artifacts.require("FiatTokenV1_1"); const FiatTokenV2 = artifacts.require("FiatTokenV2"); const FiatTokenV2_1 = artifacts.require("FiatTokenV2_1"); const FiatTokenV2_2 = artifacts.require("FiatTokenV2_2"); +const OptimismFiatTokenV2_2 = artifacts.require("OptimismFiatTokenV2_2"); export const STORAGE_SLOT_NUMBERS = { _deprecatedBlacklisted: 3, @@ -37,9 +38,11 @@ export function usesOriginalStorageSlotPositions< >({ Contract, version, + constructorArgs, }: { Contract: Truffle.Contract; version: 1 | 1.1 | 2 | 2.1 | 2.2; + constructorArgs?: unknown[]; }): void { describe("uses original storage slot positions", () => { const [name, symbol, currency, decimals] = ["USD Coin", "USDC", "USD", 6]; @@ -71,7 +74,7 @@ export function usesOriginalStorageSlotPositions< let domainSeparator: string; beforeEach(async () => { - fiatToken = await Contract.new(); + fiatToken = await Contract.new(...(constructorArgs || [])); proxy = await FiatTokenProxy.new(fiatToken.address); await proxy.changeAdmin(proxyAdmin); @@ -113,7 +116,9 @@ export function usesOriginalStorageSlotPositions< await proxyAsFiatTokenV2_1.initializeV2_1(lostAndFound); } if (version >= 2.2) { - const proxyAsFiatTokenV2_2 = await FiatTokenV2_2.at(proxy.address); + const proxyAsFiatTokenV2_2 = constructorArgs + ? await OptimismFiatTokenV2_2.at(proxy.address) + : await FiatTokenV2_2.at(proxy.address); await proxyAsFiatTokenV2_2.initializeV2_2([], symbol); } }); diff --git a/test/v2/OptimismFiatTokenV2_2.test.ts b/test/v2/OptimismFiatTokenV2_2.test.ts new file mode 100644 index 000000000..a618057b8 --- /dev/null +++ b/test/v2/OptimismFiatTokenV2_2.test.ts @@ -0,0 +1,307 @@ +/** + * Copyright 2023 Circle Internet Financial, LTD. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import BN from "bn.js"; +import { + AnyFiatTokenV2Instance, + OptimismFiatTokenV2_2InstanceExtended, +} from "../../@types/AnyFiatTokenV2Instance"; +import { + expectRevert, + generateAccounts, + initializeToVersion, + linkLibraryToTokenContract, +} from "../helpers"; +import { HARDHAT_ACCOUNTS, POW_2_255_BN } from "../helpers/constants"; +import { + STORAGE_SLOT_NUMBERS, + addressMappingSlot, + readSlot, + usesOriginalStorageSlotPositions, +} from "../helpers/storageSlots.behavior"; +import { behavesLikeFiatTokenV2 } from "./v2.behavior"; +import { + SignatureBytesType, + permitSignature, + permitSignatureV22, + transferWithAuthorizationSignature, + transferWithAuthorizationSignatureV22, + cancelAuthorizationSignature, + cancelAuthorizationSignatureV22, + receiveWithAuthorizationSignature, + receiveWithAuthorizationSignatureV22, +} from "./GasAbstraction/helpers"; +import { encodeCall } from "../v1/helpers/tokenTest"; +import { behavesLikeFiatTokenV22 } from "./v2_2.behavior"; + +const FiatTokenProxy = artifacts.require("FiatTokenProxy"); +const FiatTokenV2_1 = artifacts.require("FiatTokenV2_1"); +const OptimismFiatTokenV2_2 = artifacts.require("OptimismFiatTokenV2_2"); + +describe("OptimismFiatTokenV2_2", () => { + const newSymbol = "USDCUSDC"; + const fiatTokenOwner = HARDHAT_ACCOUNTS[9]; + const lostAndFound = HARDHAT_ACCOUNTS[2]; + const proxyOwnerAccount = HARDHAT_ACCOUNTS[14]; + const l1RemoteToken = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"; + + let fiatToken: OptimismFiatTokenV2_2InstanceExtended; + + const getFiatToken = ( + signatureBytesType: SignatureBytesType + ): (() => AnyFiatTokenV2Instance) => { + return () => { + initializeOverloadedMethods(fiatToken, signatureBytesType); + return fiatToken; + }; + }; + + before(async () => { + await linkLibraryToTokenContract(FiatTokenV2_1); + await linkLibraryToTokenContract(OptimismFiatTokenV2_2); + }); + + beforeEach(async () => { + fiatToken = await OptimismFiatTokenV2_2.new(l1RemoteToken); + await initializeToVersion(fiatToken, "2.1", fiatTokenOwner, lostAndFound); + }); + + describe("initializeV2_2", () => { + it("disallows calling initializeV2_2 twice", async () => { + await fiatToken.initializeV2_2([], newSymbol); + await expectRevert(fiatToken.initializeV2_2([], newSymbol)); + }); + + it("should update symbol", async () => { + await fiatToken.initializeV2_2([], newSymbol); + expect(await fiatToken.symbol()).to.eql(newSymbol); + }); + + it("should blacklist all accountsToBlacklist", async () => { + const [unblacklistedAccount, ...accountsToBlacklist] = generateAccounts( + 10 + ); + + // Prepare a proxy that's tied to a V2_1 implementation so that we can blacklist + // the account in _deprecatedBlacklisted first. + const _fiatTokenV2_1 = await FiatTokenV2_1.new(); + const _proxy = await FiatTokenProxy.new(_fiatTokenV2_1.address, { + from: proxyOwnerAccount, + }); + const _proxyAsV2_1 = await FiatTokenV2_1.at(_proxy.address); + await initializeToVersion( + _proxyAsV2_1, + "2.1", + fiatTokenOwner, + lostAndFound + ); + await Promise.all( + accountsToBlacklist.map((a) => + _proxyAsV2_1.blacklist(a, { from: fiatTokenOwner }) + ) + ); + + // Sanity check that _deprecatedBlacklisted is set, and balanceAndBlacklistStates is not set for + // every accountsToBlacklist. + expect( + ( + await readDeprecatedBlacklisted(_proxy.address, accountsToBlacklist) + ).every((result) => result === 1) + ).to.be.true; + expect( + ( + await readBalanceAndBlacklistStates( + _proxy.address, + accountsToBlacklist + ) + ).every((result) => result.eq(new BN(0))) + ).to.be.true; + + // Sanity check that _deprecatedBlacklisted is set, and balanceAndBlacklistStates is not set + // for `address(this)` + expect( + (await readDeprecatedBlacklisted(_proxy.address, [_proxy.address]))[0] + ).to.equal(1); + expect( + ( + await readBalanceAndBlacklistStates(_proxy.address, [_proxy.address]) + )[0].eq(new BN(0)) + ).to.be.true; + + // Call the initializeV2_2 function through an upgrade call. + const initializeData = encodeCall( + "initializeV2_2", + ["address[]", "string"], + [accountsToBlacklist, newSymbol] + ); + await _proxy.upgradeToAndCall(fiatToken.address, initializeData, { + from: proxyOwnerAccount, + }); + + // Validate that isBlacklisted returns true for every accountsToBlacklist. + const _proxyAsV2_2 = await OptimismFiatTokenV2_2.at(_proxy.address); + const areAccountsBlacklisted = await Promise.all( + accountsToBlacklist.map((account) => + _proxyAsV2_2.isBlacklisted(account) + ) + ); + expect(areAccountsBlacklisted.every((b: boolean) => b)).to.be.true; + + // Validate that _deprecatedBlacklisted is unset, and balanceAndBlacklistStates is set for every + // accountsToBlacklist. + expect( + ( + await readDeprecatedBlacklisted(_proxy.address, accountsToBlacklist) + ).every((result) => result === 0) + ).to.be.true; + expect( + ( + await readBalanceAndBlacklistStates( + _proxy.address, + accountsToBlacklist + ) + ).every((result) => result.eq(POW_2_255_BN)) + ).to.be.true; + + // Validate that _deprecatedBlacklisted is unset, and balanceAndBlacklistStates is set for + // `address(this)` + expect( + (await readDeprecatedBlacklisted(_proxy.address, [_proxy.address]))[0] + ).to.equal(0); + expect( + ( + await readBalanceAndBlacklistStates(_proxy.address, [_proxy.address]) + )[0].eq(POW_2_255_BN) + ).to.be.true; + + // Sanity check that an unblacklisted account does not get blacklisted. + expect(await _proxyAsV2_2.isBlacklisted(unblacklistedAccount)).to.be + .false; + }); + + it("should revert if an accountToBlacklist was not blacklisted", async () => { + const accountsToBlacklist = generateAccounts(1); + await expectRevert( + fiatToken.initializeV2_2(accountsToBlacklist, newSymbol), + "FiatTokenV2_2: Blacklisting previously unblacklisted account!" + ); + + // Sanity check that the account is not blacklisted after revert. + expect(await fiatToken.isBlacklisted(accountsToBlacklist[0])).to.be.false; + }); + }); + + describe("initialized contract", () => { + beforeEach(async () => { + await fiatToken.initializeV2_2([], newSymbol); + }); + + behavesLikeFiatTokenV2(2.2, getFiatToken(SignatureBytesType.Unpacked)); + + behavesLikeFiatTokenV22(getFiatToken(SignatureBytesType.Packed)); + console.log("before"); + usesOriginalStorageSlotPositions({ + Contract: OptimismFiatTokenV2_2, + version: 2.2, + constructorArgs: [l1RemoteToken], + }); + console.log("after"); + }); +}); + +/** + * With v2.2 we introduce overloaded functions for `permit`, + * `transferWithAuthorization`, `receiveWithAuthorization`, + * and `cancelAuthorization`. + * + * Since function overloading isn't supported by Javascript, + * the typechain library generates type interfaces for overloaded functions differently. + * For instance, we can no longer access the `permit` function with + * `fiattoken.permit`. Instead, we need to need to use the full function signature e.g. + * `fiattoken.methods["permit(address,address,uint256,uint256,uint8,bytes32,bytes32)"]` OR + * `fiattoken.methods["permit(address,address,uint256,uint256,bytes)"]` (v22 interface). + * + * To preserve type-coherence and reuse test suites written for v2 & v2.1 contracts, + * here we re-assign the overloaded method definition to the method name shorthand. + */ +export function initializeOverloadedMethods( + fiatToken: OptimismFiatTokenV2_2InstanceExtended, + signatureBytesType: SignatureBytesType +): void { + if (signatureBytesType == SignatureBytesType.Unpacked) { + fiatToken.permit = fiatToken.methods[permitSignature]; + fiatToken.transferWithAuthorization = + fiatToken.methods[transferWithAuthorizationSignature]; + fiatToken.receiveWithAuthorization = + fiatToken.methods[receiveWithAuthorizationSignature]; + fiatToken.cancelAuthorization = + fiatToken.methods[cancelAuthorizationSignature]; + } else { + fiatToken.permit = fiatToken.methods[permitSignatureV22]; + fiatToken.transferWithAuthorization = + fiatToken.methods[transferWithAuthorizationSignatureV22]; + fiatToken.receiveWithAuthorization = + fiatToken.methods[receiveWithAuthorizationSignatureV22]; + fiatToken.cancelAuthorization = + fiatToken.methods[cancelAuthorizationSignatureV22]; + } +} + +/** + * Helper method to read the _deprecatedBlacklisted map. + * @param proxyOrImplementation the address of the proxy or implementation contract. + * @param accounts the accounts to read states for. + * @returns the results (in order) from reading the map. + */ +async function readDeprecatedBlacklisted( + proxyOrImplementation: string, + accounts: string[] +): Promise { + return ( + await Promise.all( + accounts.map((a) => + readSlot( + proxyOrImplementation, + addressMappingSlot(a, STORAGE_SLOT_NUMBERS._deprecatedBlacklisted) + ) + ) + ) + ).map((result) => parseInt(result, 16)); +} + +/** + * Helper method to read the balanceAndBlacklistStates map. + * @param proxyOrImplementation the address of the proxy or implementation contract. + * @param accounts the accounts to read states for. + * @returns the results (in order) from reading the map. + */ +async function readBalanceAndBlacklistStates( + proxyOrImplementation: string, + accounts: string[] +): Promise { + return ( + await Promise.all( + accounts.map((a) => + readSlot( + proxyOrImplementation, + addressMappingSlot(a, STORAGE_SLOT_NUMBERS.balanceAndBlacklistStates) + ) + ) + ) + ).map((result) => new BN(result.slice(2), 16)); +} From 4e1a0721e8d835301b9a78a4bd6af6c64958b6e2 Mon Sep 17 00:00:00 2001 From: alvrs Date: Wed, 24 Apr 2024 21:52:35 +0100 Subject: [PATCH 06/21] use kwargs style syntax --- scripts/deploy/DeployImpl.sol | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/scripts/deploy/DeployImpl.sol b/scripts/deploy/DeployImpl.sol index cb91ed95a..7672113d6 100644 --- a/scripts/deploy/DeployImpl.sol +++ b/scripts/deploy/DeployImpl.sol @@ -89,19 +89,22 @@ contract DeployImpl { // the contract from being reinitialized later on with different values. // Dummy values can be used here as the proxy contract will store the actual values // for the deployed token. - fiatTokenV2_2.initialize( - "", - "", - "", - 0, - THROWAWAY_ADDRESS, - THROWAWAY_ADDRESS, - THROWAWAY_ADDRESS, - THROWAWAY_ADDRESS - ); - fiatTokenV2_2.initializeV2(""); - fiatTokenV2_2.initializeV2_1(THROWAWAY_ADDRESS); - fiatTokenV2_2.initializeV2_2(new address[](0), ""); + fiatTokenV2_2.initialize({ + tokenName: "", + tokenSymbol: "", + tokenCurrency: "", + tokenDecimals: 0, + newMasterMinter: THROWAWAY_ADDRESS, + newPauser: THROWAWAY_ADDRESS, + newBlacklister: THROWAWAY_ADDRESS, + newOwner: THROWAWAY_ADDRESS + }); + fiatTokenV2_2.initializeV2({ newName: "" }); + fiatTokenV2_2.initializeV2_1({ lostAndFound: THROWAWAY_ADDRESS }); + fiatTokenV2_2.initializeV2_2({ + accountsToBlacklist: new address[](0), + newSymbol: "" + }); } else { fiatTokenV2_2 = OptimismFiatTokenV2_2(impl); } From d2887f41379f8f857610300bb0eb3b2e674e6bf8 Mon Sep 17 00:00:00 2001 From: alvrs Date: Wed, 24 Apr 2024 21:55:45 +0100 Subject: [PATCH 07/21] revert changes to package.json --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 188eb6d5c..435457790 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "url": "https://github.com/circlefin/stablecoin-evm/issues" }, "homepage": "https://github.com/circlefin/stablecoin-evm#readme", + "dependencies": {}, "devDependencies": { "@nomicfoundation/hardhat-chai-matchers": "2.0.2", "@nomicfoundation/hardhat-ethers": "3.0.4", From 5144f94a4d35be9f3450b5d48ffa418daa6186c9 Mon Sep 17 00:00:00 2001 From: alvrs Date: Wed, 24 Apr 2024 21:56:49 +0100 Subject: [PATCH 08/21] remove unused variable --- scripts/deploy/deploy-fiat-token.s.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/deploy/deploy-fiat-token.s.sol b/scripts/deploy/deploy-fiat-token.s.sol index e2676544d..4a5f660c4 100644 --- a/scripts/deploy/deploy-fiat-token.s.sol +++ b/scripts/deploy/deploy-fiat-token.s.sol @@ -53,7 +53,6 @@ contract DeployFiatToken is Script, DeployImpl { uint256 private deployerPrivateKey; address l1RemoteToken; - address l2StandardBridge; /** * @notice initialize variables from environment From 312f27d688e4634839c68a5c04b327ee8d6376d8 Mon Sep 17 00:00:00 2001 From: alvrs Date: Wed, 24 Apr 2024 22:46:33 +0100 Subject: [PATCH 09/21] remove todo --- scripts/deploy/deploy-fiat-token.s.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/deploy/deploy-fiat-token.s.sol b/scripts/deploy/deploy-fiat-token.s.sol index 4a5f660c4..02dac93c5 100644 --- a/scripts/deploy/deploy-fiat-token.s.sol +++ b/scripts/deploy/deploy-fiat-token.s.sol @@ -120,8 +120,6 @@ contract DeployFiatToken is Script, DeployImpl { // Now that the proxy contract has been deployed, we can deploy the master minter. MasterMinter masterMinter = new MasterMinter(address(proxy)); - // TODO: If l2StandardBridge and l1RemoteToken are set, set the l2StandardBridge as minter. - // Change the master minter to be owned by the master minter owner masterMinter.transferOwnership(masterMinterOwner); From 174f0e439aa9eaaefdee852380d2bf5ea4cf557b Mon Sep 17 00:00:00 2001 From: alvrs Date: Wed, 24 Apr 2024 22:49:12 +0100 Subject: [PATCH 10/21] add script to set token's bridge address --- scripts/deploy/set-l2-standard-bridge.s.sol | 35 +++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 scripts/deploy/set-l2-standard-bridge.s.sol diff --git a/scripts/deploy/set-l2-standard-bridge.s.sol b/scripts/deploy/set-l2-standard-bridge.s.sol new file mode 100644 index 000000000..9dfc3e077 --- /dev/null +++ b/scripts/deploy/set-l2-standard-bridge.s.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.6.12; + +import "forge-std/console.sol"; // solhint-disable no-global-import, no-console +import { Script } from "forge-std/Script.sol"; +import { + OptimismFiatTokenV2_2 +} from "../../contracts/v2/OptimismFiatTokenV2_2.sol"; +import { MasterMinter } from "../../contracts/minting/MasterMinter.sol"; + +/** + * A utility script to set the token's l2StandardBridge as the minter + */ +contract SetL2StandardBridge is Script { + /** + * @notice main function that will be run by forge + */ + function run(address owner, OptimismFiatTokenV2_2 optimismFiatTokenV2_2) + external + { + vm.startBroadcast(owner); + address l2StandardBridge = optimismFiatTokenV2_2.bridge(); + if (l2StandardBridge == address(0)) { + revert("Expected no-zero bridge address"); + } + MasterMinter masterMinter = MasterMinter( + optimismFiatTokenV2_2.masterMinter() + ); + masterMinter.configureController(owner, l2StandardBridge); + masterMinter.configureMinter(type(uint256).max); + masterMinter.removeController(owner); + + vm.stopBroadcast(); + } +} From a9e51319a5dc90097626bab9e374f0a14af61b0c Mon Sep 17 00:00:00 2001 From: alvrs Date: Wed, 24 Apr 2024 22:53:36 +0100 Subject: [PATCH 11/21] update param names --- scripts/deploy/set-l2-standard-bridge.s.sol | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/scripts/deploy/set-l2-standard-bridge.s.sol b/scripts/deploy/set-l2-standard-bridge.s.sol index 9dfc3e077..e7615be87 100644 --- a/scripts/deploy/set-l2-standard-bridge.s.sol +++ b/scripts/deploy/set-l2-standard-bridge.s.sol @@ -15,10 +15,11 @@ contract SetL2StandardBridge is Script { /** * @notice main function that will be run by forge */ - function run(address owner, OptimismFiatTokenV2_2 optimismFiatTokenV2_2) - external - { - vm.startBroadcast(owner); + function run( + address masterMinterOwner, + OptimismFiatTokenV2_2 optimismFiatTokenV2_2 + ) external { + vm.startBroadcast(masterMinterOwner); address l2StandardBridge = optimismFiatTokenV2_2.bridge(); if (l2StandardBridge == address(0)) { revert("Expected no-zero bridge address"); @@ -26,9 +27,9 @@ contract SetL2StandardBridge is Script { MasterMinter masterMinter = MasterMinter( optimismFiatTokenV2_2.masterMinter() ); - masterMinter.configureController(owner, l2StandardBridge); + masterMinter.configureController(masterMinterOwner, l2StandardBridge); masterMinter.configureMinter(type(uint256).max); - masterMinter.removeController(owner); + masterMinter.removeController(masterMinterOwner); vm.stopBroadcast(); } From 670f67487a1035e4ac12181dd42c0556852ecdf8 Mon Sep 17 00:00:00 2001 From: alvrs Date: Wed, 24 Apr 2024 23:21:51 +0100 Subject: [PATCH 12/21] make l1 token optional --- scripts/deploy/deploy-fiat-token.s.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/deploy/deploy-fiat-token.s.sol b/scripts/deploy/deploy-fiat-token.s.sol index 02dac93c5..94c71c448 100644 --- a/scripts/deploy/deploy-fiat-token.s.sol +++ b/scripts/deploy/deploy-fiat-token.s.sol @@ -75,7 +75,7 @@ contract DeployFiatToken is Script, DeployImpl { deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); - l1RemoteToken = vm.envAddress("L1_REMOTE_TOKEN"); + l1RemoteToken = vm.envOr("L1_REMOTE_TOKEN", address(0)); console.log("TOKEN_NAME: '%s'", tokenName); console.log("TOKEN_SYMBOL: '%s'", tokenSymbol); From 2e295a2da78fa5754dd159cf930b462d1b2a6ea6 Mon Sep 17 00:00:00 2001 From: alvrs Date: Thu, 25 Apr 2024 00:30:09 +0100 Subject: [PATCH 13/21] move view function out of broadcast --- scripts/deploy/set-l2-standard-bridge.s.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/deploy/set-l2-standard-bridge.s.sol b/scripts/deploy/set-l2-standard-bridge.s.sol index e7615be87..06aacd891 100644 --- a/scripts/deploy/set-l2-standard-bridge.s.sol +++ b/scripts/deploy/set-l2-standard-bridge.s.sol @@ -19,18 +19,18 @@ contract SetL2StandardBridge is Script { address masterMinterOwner, OptimismFiatTokenV2_2 optimismFiatTokenV2_2 ) external { - vm.startBroadcast(masterMinterOwner); address l2StandardBridge = optimismFiatTokenV2_2.bridge(); if (l2StandardBridge == address(0)) { revert("Expected no-zero bridge address"); } + + vm.startBroadcast(masterMinterOwner); MasterMinter masterMinter = MasterMinter( optimismFiatTokenV2_2.masterMinter() ); masterMinter.configureController(masterMinterOwner, l2StandardBridge); masterMinter.configureMinter(type(uint256).max); masterMinter.removeController(masterMinterOwner); - vm.stopBroadcast(); } } From 4466a7ed9f4988e328f316c81de9d80989489d01 Mon Sep 17 00:00:00 2001 From: alvrs Date: Thu, 25 Apr 2024 00:31:51 +0100 Subject: [PATCH 14/21] move view function out of broadcast --- scripts/deploy/set-l2-standard-bridge.s.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/deploy/set-l2-standard-bridge.s.sol b/scripts/deploy/set-l2-standard-bridge.s.sol index 06aacd891..2b29a66de 100644 --- a/scripts/deploy/set-l2-standard-bridge.s.sol +++ b/scripts/deploy/set-l2-standard-bridge.s.sol @@ -23,11 +23,11 @@ contract SetL2StandardBridge is Script { if (l2StandardBridge == address(0)) { revert("Expected no-zero bridge address"); } - - vm.startBroadcast(masterMinterOwner); MasterMinter masterMinter = MasterMinter( optimismFiatTokenV2_2.masterMinter() ); + + vm.startBroadcast(masterMinterOwner); masterMinter.configureController(masterMinterOwner, l2StandardBridge); masterMinter.configureMinter(type(uint256).max); masterMinter.removeController(masterMinterOwner); From 470317504ee54138a0688fbec1d281ef1ecf51b3 Mon Sep 17 00:00:00 2001 From: alvrs Date: Thu, 1 Aug 2024 14:46:39 +0100 Subject: [PATCH 15/21] fix: add burn function with _from param --- @types/AnyFiatTokenV2Instance.d.ts | 8 +- ...C20.sol => IOptimismMintableFiatToken.sol} | 17 +++++ contracts/v2/OptimismFiatTokenV2_2.sol | 43 ----------- .../v2/OptimismMintableFiatTokenV2_2.sol | 74 +++++++++++++++++++ scripts/deploy/DeployImpl.sol | 12 +-- scripts/deploy/deploy-fiat-token.s.sol | 4 +- scripts/deploy/set-l2-standard-bridge.s.sol | 10 +-- test/helpers/index.ts | 4 +- test/helpers/storageSlots.behavior.ts | 6 +- test/misc/gas.ts | 12 ++- test/v2/OptimismFiatTokenV2_2.test.ts | 22 +++--- 11 files changed, 136 insertions(+), 76 deletions(-) rename contracts/v2/{IOptimismMintableERC20.sol => IOptimismMintableFiatToken.sol} (52%) delete mode 100644 contracts/v2/OptimismFiatTokenV2_2.sol create mode 100644 contracts/v2/OptimismMintableFiatTokenV2_2.sol diff --git a/@types/AnyFiatTokenV2Instance.d.ts b/@types/AnyFiatTokenV2Instance.d.ts index 3182e98a4..9d18f3bf3 100644 --- a/@types/AnyFiatTokenV2Instance.d.ts +++ b/@types/AnyFiatTokenV2Instance.d.ts @@ -19,7 +19,7 @@ import { FiatTokenV2Instance } from "./generated/FiatTokenV2"; import { FiatTokenV2_1Instance } from "./generated/FiatTokenV2_1"; import { FiatTokenV2_2Instance } from "./generated/FiatTokenV2_2"; -import { OptimismFiatTokenV2_2Instance } from "./generated/OptimismFiatTokenV2_2"; +import { OptimismMintableFiatTokenV2_2Instance } from "./generated/OptimismMintableFiatTokenV2_2"; export interface FiatTokenV2_2InstanceExtended extends FiatTokenV2_2Instance { permit?: typeof FiatTokenV2Instance.permit; @@ -28,8 +28,8 @@ export interface FiatTokenV2_2InstanceExtended extends FiatTokenV2_2Instance { cancelAuthorization?: typeof FiatTokenV2Instance.cancelAuthorization; } -export interface OptimismFiatTokenV2_2InstanceExtended - extends OptimismFiatTokenV2_2Instance { +export interface OptimismMintableFiatTokenV2_2InstanceExtended + extends OptimismMintableFiatTokenV2_2Instance { permit?: typeof FiatTokenV2Instance.permit; transferWithAuthorization?: typeof FiatTokenV2Instance.transferWithAuthorization; receiveWithAuthorization?: typeof FiatTokenV2Instance.receiveWithAuthorization; @@ -40,4 +40,4 @@ export type AnyFiatTokenV2Instance = | FiatTokenV2Instance | FiatTokenV2_1Instance | FiatTokenV2_2InstanceExtended - | OptimismFiatTokenV2_2InstanceExtended; + | OptimismMintableFiatTokenV2_2InstanceExtended; diff --git a/contracts/v2/IOptimismMintableERC20.sol b/contracts/v2/IOptimismMintableFiatToken.sol similarity index 52% rename from contracts/v2/IOptimismMintableERC20.sol rename to contracts/v2/IOptimismMintableFiatToken.sol index 67df247ed..c0dbdd8d7 100644 --- a/contracts/v2/IOptimismMintableERC20.sol +++ b/contracts/v2/IOptimismMintableFiatToken.sol @@ -19,3 +19,20 @@ interface IOptimismMintableERC20 is IERC165 { function burn(address _from, uint256 _amount) external; } + +/** + * @title IOptimismMintableFiatToken + * @author Lattice (https://lattice.xyz) + * @notice This interface adds the functions from IOptimismMintableERC20 missing from FiatTokenV2_2. + * It doesn't include `mint(address _to, uint256 _amount)`, as this function already exists + * on FiatTokenV2_2 (from FiatTokenV1), and can't be overridden. The only difference is a + * (bool) return type for the FiatTokenV1 version, which doesn't matter for consumers of + * IOptimismMintableERC20 that don't expect a return type. + */ +interface IOptimismMintableFiatToken is IERC165 { + function remoteToken() external view returns (address); + + function bridge() external returns (address); + + function burn(address _from, uint256 _amount) external; +} diff --git a/contracts/v2/OptimismFiatTokenV2_2.sol b/contracts/v2/OptimismFiatTokenV2_2.sol deleted file mode 100644 index c1807938c..000000000 --- a/contracts/v2/OptimismFiatTokenV2_2.sol +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.6.12; - -import { FiatTokenV1 } from "../v1/FiatTokenV1.sol"; -import { FiatTokenV2_2 } from "./FiatTokenV2_2.sol"; -import { IOptimismMintableERC20 } from "./IOptimismMintableERC20.sol"; -import { IERC165 } from "@openzeppelin/contracts/introspection/IERC165.sol"; - -/** - * @title OptimismFiatTokenV2_2 - * @author Lattice (https://lattice.xyz) - * @notice Adds compatibility with IOptimismMintableERC20 to the Bridged USDC Standard, - * so it can be used with Optimism's StandardBridge. - * @dev This contract does not extend `IOptimismMintableERC20` to avoid the requirement to override `mint` and `burn` functions. - */ -contract OptimismFiatTokenV2_2 is FiatTokenV2_2, IERC165 { - address private immutable l1RemoteToken; - - constructor(address _l1RemoteToken) public FiatTokenV2_2() { - l1RemoteToken = _l1RemoteToken; - } - - function remoteToken() external view returns (address) { - return l1RemoteToken; - } - - function bridge() external pure returns (address) { - // OP Stack L2StandardBridge predeploy - // https://specs.optimism.io/protocol/predeploys.html - return address(0x4200000000000000000000000000000000000010); - } - - function supportsInterface(bytes4 interfaceId) - external - override - view - returns (bool) - { - return - interfaceId == type(IOptimismMintableERC20).interfaceId || - interfaceId == type(IERC165).interfaceId; - } -} diff --git a/contracts/v2/OptimismMintableFiatTokenV2_2.sol b/contracts/v2/OptimismMintableFiatTokenV2_2.sol new file mode 100644 index 000000000..e5c194c96 --- /dev/null +++ b/contracts/v2/OptimismMintableFiatTokenV2_2.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.6.12; + +import { FiatTokenV1 } from "../v1/FiatTokenV1.sol"; +import { FiatTokenV2_2 } from "./FiatTokenV2_2.sol"; +import { + IOptimismMintableERC20, + IOptimismMintableFiatToken +} from "./IOptimismMintableFiatToken.sol"; +import { IERC165 } from "@openzeppelin/contracts/introspection/IERC165.sol"; + +/** + * @title OptimismMintableFiatTokenV2_2 + * @author Lattice (https://lattice.xyz) + * @notice Adds compatibility with IOptimismMintableERC20 to the Bridged USDC Standard, + * so it can be used with Optimism's StandardBridge. + */ +contract OptimismMintableFiatTokenV2_2 is + FiatTokenV2_2, + IOptimismMintableFiatToken +{ + address private immutable l1RemoteToken; + + constructor(address _l1RemoteToken) public FiatTokenV2_2() { + l1RemoteToken = _l1RemoteToken; + } + + function remoteToken() external override view returns (address) { + return l1RemoteToken; + } + + function bridge() external override returns (address) { + // OP Stack L2StandardBridge predeploy + // https://specs.optimism.io/protocol/predeploys.html + return address(0x4200000000000000000000000000000000000010); + } + + function supportsInterface(bytes4 interfaceId) + external + override + view + returns (bool) + { + return + interfaceId == type(IOptimismMintableERC20).interfaceId || + interfaceId == type(IERC165).interfaceId; + } + + /** + * @notice Allows a minter to burn tokens for the given account. + * @dev The caller must be a minter, must not be blacklisted, and the amount to burn + * should be less than or equal to the account's balance. + * The function is a requirement for IOptimismMintableERC20. + * It is mostly equivalent to FiatTokenV1.burn, with the only change being + * the additional _from parameter to burn from instead of burning from msg.sender. + * @param _amount the amount of tokens to be burned. + */ + function burn(address _from, uint256 _amount) + external + override + whenNotPaused + onlyMinters + notBlacklisted(msg.sender) + { + uint256 balance = _balanceOf(_from); + require(_amount > 0, "FiatToken: burn amount not greater than 0"); + require(balance >= _amount, "FiatToken: burn amount exceeds balance"); + + totalSupply_ = totalSupply_.sub(_amount); + _setBalance(_from, balance.sub(_amount)); + emit Burn(_from, _amount); + emit Transfer(_from, address(0), _amount); + } +} diff --git a/scripts/deploy/DeployImpl.sol b/scripts/deploy/DeployImpl.sol index 7672113d6..08b9132dd 100644 --- a/scripts/deploy/DeployImpl.sol +++ b/scripts/deploy/DeployImpl.sol @@ -20,8 +20,8 @@ pragma solidity 0.6.12; import { FiatTokenV2_2 } from "../../contracts/v2/FiatTokenV2_2.sol"; import { - OptimismFiatTokenV2_2 -} from "../../contracts/v2/OptimismFiatTokenV2_2.sol"; + OptimismMintableFiatTokenV2_2 +} from "../../contracts/v2/OptimismMintableFiatTokenV2_2.sol"; /** * @notice A utility contract that exposes a re-useable getOrDeployImpl function. @@ -74,16 +74,16 @@ contract DeployImpl { * * @param impl configured of the implementation contract, where address(0) represents a new instance should be deployed * @param l1RemoteToken token on the L1 corresponding to this bridged version of the token - * @return OptimismFiatTokenV2_2 newly deployed or loaded instance + * @return OptimismMintableFiatTokenV2_2 newly deployed or loaded instance */ function getOrDeployImpl(address impl, address l1RemoteToken) internal returns (FiatTokenV2_2) { - OptimismFiatTokenV2_2 fiatTokenV2_2; + OptimismMintableFiatTokenV2_2 fiatTokenV2_2; if (impl == address(0)) { - fiatTokenV2_2 = new OptimismFiatTokenV2_2(l1RemoteToken); + fiatTokenV2_2 = new OptimismMintableFiatTokenV2_2(l1RemoteToken); // Initializing the implementation contract with dummy values here prevents // the contract from being reinitialized later on with different values. @@ -106,7 +106,7 @@ contract DeployImpl { newSymbol: "" }); } else { - fiatTokenV2_2 = OptimismFiatTokenV2_2(impl); + fiatTokenV2_2 = OptimismMintableFiatTokenV2_2(impl); } return fiatTokenV2_2; diff --git a/scripts/deploy/deploy-fiat-token.s.sol b/scripts/deploy/deploy-fiat-token.s.sol index 94c71c448..473f5de3a 100644 --- a/scripts/deploy/deploy-fiat-token.s.sol +++ b/scripts/deploy/deploy-fiat-token.s.sol @@ -24,8 +24,8 @@ import { DeployImpl } from "./DeployImpl.sol"; import { FiatTokenProxy } from "../../contracts/v1/FiatTokenProxy.sol"; import { FiatTokenV2_2 } from "../../contracts/v2/FiatTokenV2_2.sol"; import { - OptimismFiatTokenV2_2 -} from "../../contracts/v2/OptimismFiatTokenV2_2.sol"; + OptimismMintableFiatTokenV2_2 +} from "../../contracts/v2/OptimismMintableFiatTokenV2_2.sol"; import { MasterMinter } from "../../contracts/minting/MasterMinter.sol"; /** diff --git a/scripts/deploy/set-l2-standard-bridge.s.sol b/scripts/deploy/set-l2-standard-bridge.s.sol index 2b29a66de..bce5ed251 100644 --- a/scripts/deploy/set-l2-standard-bridge.s.sol +++ b/scripts/deploy/set-l2-standard-bridge.s.sol @@ -4,8 +4,8 @@ pragma solidity 0.6.12; import "forge-std/console.sol"; // solhint-disable no-global-import, no-console import { Script } from "forge-std/Script.sol"; import { - OptimismFiatTokenV2_2 -} from "../../contracts/v2/OptimismFiatTokenV2_2.sol"; + OptimismMintableFiatTokenV2_2 +} from "../../contracts/v2/OptimismMintableFiatTokenV2_2.sol"; import { MasterMinter } from "../../contracts/minting/MasterMinter.sol"; /** @@ -17,14 +17,14 @@ contract SetL2StandardBridge is Script { */ function run( address masterMinterOwner, - OptimismFiatTokenV2_2 optimismFiatTokenV2_2 + OptimismMintableFiatTokenV2_2 optimismMintableFiatTokenV2_2 ) external { - address l2StandardBridge = optimismFiatTokenV2_2.bridge(); + address l2StandardBridge = optimismMintableFiatTokenV2_2.bridge(); if (l2StandardBridge == address(0)) { revert("Expected no-zero bridge address"); } MasterMinter masterMinter = MasterMinter( - optimismFiatTokenV2_2.masterMinter() + optimismMintableFiatTokenV2_2.masterMinter() ); vm.startBroadcast(masterMinterOwner); diff --git a/test/helpers/index.ts b/test/helpers/index.ts index 4874b0896..c6338e8ba 100644 --- a/test/helpers/index.ts +++ b/test/helpers/index.ts @@ -26,6 +26,7 @@ import { FiatTokenV2_1Instance, FiatTokenV2_2Instance, FiatTokenV2Instance, + OptimismMintableFiatTokenV2_2Instance, } from "../../@types/generated"; import _ from "lodash"; @@ -140,7 +141,8 @@ export async function initializeToVersion( | FiatTokenV1_1Instance | FiatTokenV2Instance | FiatTokenV2_1Instance - | FiatTokenV2_2Instance, + | FiatTokenV2_2Instance + | OptimismMintableFiatTokenV2_2Instance, version: "1" | "1.1" | "2" | "2.1" | "2.2", fiatTokenOwner: string, lostAndFound: string, diff --git a/test/helpers/storageSlots.behavior.ts b/test/helpers/storageSlots.behavior.ts index 809d8fa82..bef272ea3 100644 --- a/test/helpers/storageSlots.behavior.ts +++ b/test/helpers/storageSlots.behavior.ts @@ -26,7 +26,9 @@ const FiatTokenV1_1 = artifacts.require("FiatTokenV1_1"); const FiatTokenV2 = artifacts.require("FiatTokenV2"); const FiatTokenV2_1 = artifacts.require("FiatTokenV2_1"); const FiatTokenV2_2 = artifacts.require("FiatTokenV2_2"); -const OptimismFiatTokenV2_2 = artifacts.require("OptimismFiatTokenV2_2"); +const OptimismMintableFiatTokenV2_2 = artifacts.require( + "OptimismMintableFiatTokenV2_2" +); export const STORAGE_SLOT_NUMBERS = { _deprecatedBlacklisted: 3, @@ -117,7 +119,7 @@ export function usesOriginalStorageSlotPositions< } if (version >= 2.2) { const proxyAsFiatTokenV2_2 = constructorArgs - ? await OptimismFiatTokenV2_2.at(proxy.address) + ? await OptimismMintableFiatTokenV2_2.at(proxy.address) : await FiatTokenV2_2.at(proxy.address); await proxyAsFiatTokenV2_2.initializeV2_2([], symbol); } diff --git a/test/misc/gas.ts b/test/misc/gas.ts index 2650f45d9..ea2dd1a17 100644 --- a/test/misc/gas.ts +++ b/test/misc/gas.ts @@ -119,13 +119,17 @@ describe(`gas costs for version ${TARGET_VERSION}`, () => { }); it("burn() entire balance", async () => { - const tx = await fiatToken.burn(entireBalance, { from: fiatTokenOwner }); - console.log(consoleMessage, tx.receipt.gasUsed); + if ("burn" in fiatToken) { + const tx = await fiatToken.burn(entireBalance, { from: fiatTokenOwner }); + console.log(consoleMessage, tx.receipt.gasUsed); + } }); it("burn() partial balance", async () => { - const tx = await fiatToken.burn(partialBalance, { from: fiatTokenOwner }); - console.log(consoleMessage, tx.receipt.gasUsed); + if ("burn" in fiatToken) { + const tx = await fiatToken.burn(partialBalance, { from: fiatTokenOwner }); + console.log(consoleMessage, tx.receipt.gasUsed); + } }); it("transfer() where both parties have a balance before and after", async () => { diff --git a/test/v2/OptimismFiatTokenV2_2.test.ts b/test/v2/OptimismFiatTokenV2_2.test.ts index a618057b8..475b194a2 100644 --- a/test/v2/OptimismFiatTokenV2_2.test.ts +++ b/test/v2/OptimismFiatTokenV2_2.test.ts @@ -19,7 +19,7 @@ import BN from "bn.js"; import { AnyFiatTokenV2Instance, - OptimismFiatTokenV2_2InstanceExtended, + OptimismMintableFiatTokenV2_2InstanceExtended, } from "../../@types/AnyFiatTokenV2Instance"; import { expectRevert, @@ -51,16 +51,18 @@ import { behavesLikeFiatTokenV22 } from "./v2_2.behavior"; const FiatTokenProxy = artifacts.require("FiatTokenProxy"); const FiatTokenV2_1 = artifacts.require("FiatTokenV2_1"); -const OptimismFiatTokenV2_2 = artifacts.require("OptimismFiatTokenV2_2"); +const OptimismMintableFiatTokenV2_2 = artifacts.require( + "OptimismMintableFiatTokenV2_2" +); -describe("OptimismFiatTokenV2_2", () => { +describe("OptimismMintableFiatTokenV2_2", () => { const newSymbol = "USDCUSDC"; const fiatTokenOwner = HARDHAT_ACCOUNTS[9]; const lostAndFound = HARDHAT_ACCOUNTS[2]; const proxyOwnerAccount = HARDHAT_ACCOUNTS[14]; const l1RemoteToken = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"; - let fiatToken: OptimismFiatTokenV2_2InstanceExtended; + let fiatToken: OptimismMintableFiatTokenV2_2InstanceExtended; const getFiatToken = ( signatureBytesType: SignatureBytesType @@ -73,11 +75,11 @@ describe("OptimismFiatTokenV2_2", () => { before(async () => { await linkLibraryToTokenContract(FiatTokenV2_1); - await linkLibraryToTokenContract(OptimismFiatTokenV2_2); + await linkLibraryToTokenContract(OptimismMintableFiatTokenV2_2); }); beforeEach(async () => { - fiatToken = await OptimismFiatTokenV2_2.new(l1RemoteToken); + fiatToken = await OptimismMintableFiatTokenV2_2.new(l1RemoteToken); await initializeToVersion(fiatToken, "2.1", fiatTokenOwner, lostAndFound); }); @@ -154,7 +156,9 @@ describe("OptimismFiatTokenV2_2", () => { }); // Validate that isBlacklisted returns true for every accountsToBlacklist. - const _proxyAsV2_2 = await OptimismFiatTokenV2_2.at(_proxy.address); + const _proxyAsV2_2 = await OptimismMintableFiatTokenV2_2.at( + _proxy.address + ); const areAccountsBlacklisted = await Promise.all( accountsToBlacklist.map((account) => _proxyAsV2_2.isBlacklisted(account) @@ -216,7 +220,7 @@ describe("OptimismFiatTokenV2_2", () => { behavesLikeFiatTokenV22(getFiatToken(SignatureBytesType.Packed)); console.log("before"); usesOriginalStorageSlotPositions({ - Contract: OptimismFiatTokenV2_2, + Contract: OptimismMintableFiatTokenV2_2, version: 2.2, constructorArgs: [l1RemoteToken], }); @@ -240,7 +244,7 @@ describe("OptimismFiatTokenV2_2", () => { * here we re-assign the overloaded method definition to the method name shorthand. */ export function initializeOverloadedMethods( - fiatToken: OptimismFiatTokenV2_2InstanceExtended, + fiatToken: OptimismMintableFiatTokenV2_2InstanceExtended, signatureBytesType: SignatureBytesType ): void { if (signatureBytesType == SignatureBytesType.Unpacked) { From fd2c580e396a03d73511fed4623d354ff24bb036 Mon Sep 17 00:00:00 2001 From: alvrs Date: Thu, 1 Aug 2024 15:21:58 +0100 Subject: [PATCH 16/21] add upgrade script --- scripts/deploy/upgrade-impl.s.sol | 41 +++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 scripts/deploy/upgrade-impl.s.sol diff --git a/scripts/deploy/upgrade-impl.s.sol b/scripts/deploy/upgrade-impl.s.sol new file mode 100644 index 000000000..b64ac0f38 --- /dev/null +++ b/scripts/deploy/upgrade-impl.s.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.6.12; + +import "forge-std/console.sol"; // solhint-disable no-global-import, no-console +import { Script } from "forge-std/Script.sol"; +import { ScriptUtils } from "./ScriptUtils.sol"; +import { DeployImpl } from "./DeployImpl.sol"; +import { FiatTokenProxy } from "../../contracts/v1/FiatTokenProxy.sol"; +import { FiatTokenV2_2 } from "../../contracts/v2/FiatTokenV2_2.sol"; + +contract DeployImplAndUpgrader is Script, DeployImpl, ScriptUtils { + address private immutable THROWAWAY_ADDRESS = address(1); + + address private proxy; + address private impl; + address private proxyAdmin; + address private l1RemoteToken; + + /** + * @notice initialize variables from environment + */ + function setUp() public { + proxy = vm.envOr("FIAT_TOKEN_PROXY_ADDRESS", address(0)); + impl = vm.envOr("FIAT_TOKEN_IMPLEMENTATION_ADDRESS", address(0)); + proxyAdmin = vm.envAddress("PROXY_ADMIN_ADDRESS"); + l1RemoteToken = vm.envOr("L1_REMOTE_TOKEN", address(0)); + + console.log("FIAT_TOKEN_PROXY_ADDRESS: '%s'", proxy); + console.log("FIAT_TOKEN_IMPLEMENTATION_ADDRESS: '%s'", impl); + console.log("PROXY_ADMIN_ADDRESS: '%s'", proxyAdmin); + console.log("L1_REMOTE_TOKEN: '%s'", l1RemoteToken); + } + + function run() external returns (FiatTokenV2_2) { + vm.startBroadcast(proxyAdmin); + FiatTokenProxy _proxy = FiatTokenProxy(payable(proxy)); + FiatTokenV2_2 fiatTokenV2_2 = getOrDeployImpl(impl, l1RemoteToken); + _proxy.upgradeTo(address(fiatTokenV2_2)); + vm.stopBroadcast(); + } +} From 2e6dd3fdeade42a06600b542f09ba310724b289f Mon Sep 17 00:00:00 2001 From: alvrs Date: Thu, 1 Aug 2024 15:27:46 +0100 Subject: [PATCH 17/21] remove unused variable --- scripts/deploy/upgrade-impl.s.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/deploy/upgrade-impl.s.sol b/scripts/deploy/upgrade-impl.s.sol index b64ac0f38..73d6967e9 100644 --- a/scripts/deploy/upgrade-impl.s.sol +++ b/scripts/deploy/upgrade-impl.s.sol @@ -9,8 +9,6 @@ import { FiatTokenProxy } from "../../contracts/v1/FiatTokenProxy.sol"; import { FiatTokenV2_2 } from "../../contracts/v2/FiatTokenV2_2.sol"; contract DeployImplAndUpgrader is Script, DeployImpl, ScriptUtils { - address private immutable THROWAWAY_ADDRESS = address(1); - address private proxy; address private impl; address private proxyAdmin; From 9edbf7972cdaf4e071191256ad21f57fd924456f Mon Sep 17 00:00:00 2001 From: alvrs Date: Thu, 1 Aug 2024 16:58:10 +0100 Subject: [PATCH 18/21] emit msg.sender as minter --- contracts/v2/OptimismMintableFiatTokenV2_2.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/v2/OptimismMintableFiatTokenV2_2.sol b/contracts/v2/OptimismMintableFiatTokenV2_2.sol index e5c194c96..34a3245d3 100644 --- a/contracts/v2/OptimismMintableFiatTokenV2_2.sol +++ b/contracts/v2/OptimismMintableFiatTokenV2_2.sol @@ -68,7 +68,7 @@ contract OptimismMintableFiatTokenV2_2 is totalSupply_ = totalSupply_.sub(_amount); _setBalance(_from, balance.sub(_amount)); - emit Burn(_from, _amount); + emit Burn(msg.sender, _amount); emit Transfer(_from, address(0), _amount); } } From 67ed7e6bee6576a13528f4ea332a6b5a9d3aa2a3 Mon Sep 17 00:00:00 2001 From: alvrs Date: Thu, 1 Aug 2024 17:16:23 +0100 Subject: [PATCH 19/21] respect blacklist in burn --- contracts/v2/OptimismMintableFiatTokenV2_2.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/v2/OptimismMintableFiatTokenV2_2.sol b/contracts/v2/OptimismMintableFiatTokenV2_2.sol index 34a3245d3..1bb7157a7 100644 --- a/contracts/v2/OptimismMintableFiatTokenV2_2.sol +++ b/contracts/v2/OptimismMintableFiatTokenV2_2.sol @@ -61,6 +61,7 @@ contract OptimismMintableFiatTokenV2_2 is whenNotPaused onlyMinters notBlacklisted(msg.sender) + notBlacklisted(_from) { uint256 balance = _balanceOf(_from); require(_amount > 0, "FiatToken: burn amount not greater than 0"); From 891d66e55529b59e84c66d8a625785bc76c238f7 Mon Sep 17 00:00:00 2001 From: alvrs Date: Thu, 1 Aug 2024 17:23:04 +0100 Subject: [PATCH 20/21] minor upgrade script polish --- scripts/deploy/upgrade-impl.s.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/deploy/upgrade-impl.s.sol b/scripts/deploy/upgrade-impl.s.sol index 73d6967e9..b42ed3d1c 100644 --- a/scripts/deploy/upgrade-impl.s.sol +++ b/scripts/deploy/upgrade-impl.s.sol @@ -8,7 +8,7 @@ import { DeployImpl } from "./DeployImpl.sol"; import { FiatTokenProxy } from "../../contracts/v1/FiatTokenProxy.sol"; import { FiatTokenV2_2 } from "../../contracts/v2/FiatTokenV2_2.sol"; -contract DeployImplAndUpgrader is Script, DeployImpl, ScriptUtils { +contract UpgradeImpl is Script, DeployImpl, ScriptUtils { address private proxy; address private impl; address private proxyAdmin; @@ -35,5 +35,7 @@ contract DeployImplAndUpgrader is Script, DeployImpl, ScriptUtils { FiatTokenV2_2 fiatTokenV2_2 = getOrDeployImpl(impl, l1RemoteToken); _proxy.upgradeTo(address(fiatTokenV2_2)); vm.stopBroadcast(); + + return fiatTokenV2_2; } } From fb583945325efd3d427f61d64b831ec80d0e021b Mon Sep 17 00:00:00 2001 From: alvrs Date: Thu, 1 Aug 2024 17:25:13 +0100 Subject: [PATCH 21/21] ignore _from blacklist in burn --- contracts/v2/OptimismMintableFiatTokenV2_2.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/v2/OptimismMintableFiatTokenV2_2.sol b/contracts/v2/OptimismMintableFiatTokenV2_2.sol index 1bb7157a7..34a3245d3 100644 --- a/contracts/v2/OptimismMintableFiatTokenV2_2.sol +++ b/contracts/v2/OptimismMintableFiatTokenV2_2.sol @@ -61,7 +61,6 @@ contract OptimismMintableFiatTokenV2_2 is whenNotPaused onlyMinters notBlacklisted(msg.sender) - notBlacklisted(_from) { uint256 balance = _balanceOf(_from); require(_amount > 0, "FiatToken: burn amount not greater than 0");