diff --git a/.env.example b/.env.example index 0f0a948d9..8b2254c25 100644 --- a/.env.example +++ b/.env.example @@ -79,3 +79,6 @@ BLACKLIST_FILE_NAME=blacklist.remote.json # [OPTIONAL] The API key to an Etherscan flavor block explorer. # ETHERSCAN_KEY= + +# The address of the corresponding token contract on L1 +L1_REMOTE_TOKEN= \ No newline at end of file diff --git a/@types/AnyFiatTokenV2Instance.d.ts b/@types/AnyFiatTokenV2Instance.d.ts index ad49b8938..9d18f3bf3 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 { OptimismMintableFiatTokenV2_2Instance } from "./generated/OptimismMintableFiatTokenV2_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 OptimismMintableFiatTokenV2_2InstanceExtended + extends OptimismMintableFiatTokenV2_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 + | OptimismMintableFiatTokenV2_2InstanceExtended; diff --git a/contracts/v2/IOptimismMintableFiatToken.sol b/contracts/v2/IOptimismMintableFiatToken.sol new file mode 100644 index 000000000..c0dbdd8d7 --- /dev/null +++ b/contracts/v2/IOptimismMintableFiatToken.sol @@ -0,0 +1,38 @@ +// 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; +} + +/** + * @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/OptimismMintableFiatTokenV2_2.sol b/contracts/v2/OptimismMintableFiatTokenV2_2.sol new file mode 100644 index 000000000..34a3245d3 --- /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(msg.sender, _amount); + emit Transfer(_from, address(0), _amount); + } +} diff --git a/scripts/deploy/DeployImpl.sol b/scripts/deploy/DeployImpl.sol index ea4bf00ad..08b9132dd 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 { + OptimismMintableFiatTokenV2_2 +} from "../../contracts/v2/OptimismMintableFiatTokenV2_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 + * @param l1RemoteToken token on the L1 corresponding to this bridged version of the token + * @return OptimismMintableFiatTokenV2_2 newly deployed or loaded instance + */ + function getOrDeployImpl(address impl, address l1RemoteToken) + internal + returns (FiatTokenV2_2) + { + OptimismMintableFiatTokenV2_2 fiatTokenV2_2; + + if (impl == address(0)) { + 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. + // Dummy values can be used here as the proxy contract will store the actual values + // for the deployed token. + 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 = 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 24d1fbded..473f5de3a 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 { + OptimismMintableFiatTokenV2_2 +} from "../../contracts/v2/OptimismMintableFiatTokenV2_2.sol"; import { MasterMinter } from "../../contracts/minting/MasterMinter.sol"; /** @@ -49,6 +52,8 @@ contract DeployFiatToken is Script, DeployImpl { uint256 private deployerPrivateKey; + address l1RemoteToken; + /** * @notice initialize variables from environment */ @@ -70,6 +75,8 @@ contract DeployFiatToken is Script, DeployImpl { deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); + l1RemoteToken = vm.envOr("L1_REMOTE_TOKEN", address(0)); + console.log("TOKEN_NAME: '%s'", tokenName); console.log("TOKEN_SYMBOL: '%s'", tokenSymbol); console.log("TOKEN_CURRENCY: '%s'", tokenCurrency); @@ -81,6 +88,7 @@ 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); } /** @@ -99,7 +107,13 @@ 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 l1RemoteToken is set, deploy an OptimimsFiatToken. + FiatTokenV2_2 fiatTokenV2_2; + if (l1RemoteToken != address(0)) { + fiatTokenV2_2 = getOrDeployImpl(_impl, l1RemoteToken); + } else { + fiatTokenV2_2 = getOrDeployImpl(_impl); + } FiatTokenProxy proxy = new FiatTokenProxy(address(fiatTokenV2_2)); 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..bce5ed251 --- /dev/null +++ b/scripts/deploy/set-l2-standard-bridge.s.sol @@ -0,0 +1,36 @@ +// 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 { + OptimismMintableFiatTokenV2_2 +} from "../../contracts/v2/OptimismMintableFiatTokenV2_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 masterMinterOwner, + OptimismMintableFiatTokenV2_2 optimismMintableFiatTokenV2_2 + ) external { + address l2StandardBridge = optimismMintableFiatTokenV2_2.bridge(); + if (l2StandardBridge == address(0)) { + revert("Expected no-zero bridge address"); + } + MasterMinter masterMinter = MasterMinter( + optimismMintableFiatTokenV2_2.masterMinter() + ); + + vm.startBroadcast(masterMinterOwner); + masterMinter.configureController(masterMinterOwner, l2StandardBridge); + masterMinter.configureMinter(type(uint256).max); + masterMinter.removeController(masterMinterOwner); + vm.stopBroadcast(); + } +} diff --git a/scripts/deploy/upgrade-impl.s.sol b/scripts/deploy/upgrade-impl.s.sol new file mode 100644 index 000000000..b42ed3d1c --- /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 UpgradeImpl is Script, DeployImpl, ScriptUtils { + 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(); + + return fiatTokenV2_2; + } +} 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 6f36d765a..bef272ea3 100644 --- a/test/helpers/storageSlots.behavior.ts +++ b/test/helpers/storageSlots.behavior.ts @@ -26,6 +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 OptimismMintableFiatTokenV2_2 = artifacts.require( + "OptimismMintableFiatTokenV2_2" +); export const STORAGE_SLOT_NUMBERS = { _deprecatedBlacklisted: 3, @@ -37,9 +40,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 +76,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 +118,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 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 new file mode 100644 index 000000000..475b194a2 --- /dev/null +++ b/test/v2/OptimismFiatTokenV2_2.test.ts @@ -0,0 +1,311 @@ +/** + * 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, + OptimismMintableFiatTokenV2_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 OptimismMintableFiatTokenV2_2 = artifacts.require( + "OptimismMintableFiatTokenV2_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: OptimismMintableFiatTokenV2_2InstanceExtended; + + const getFiatToken = ( + signatureBytesType: SignatureBytesType + ): (() => AnyFiatTokenV2Instance) => { + return () => { + initializeOverloadedMethods(fiatToken, signatureBytesType); + return fiatToken; + }; + }; + + before(async () => { + await linkLibraryToTokenContract(FiatTokenV2_1); + await linkLibraryToTokenContract(OptimismMintableFiatTokenV2_2); + }); + + beforeEach(async () => { + fiatToken = await OptimismMintableFiatTokenV2_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 OptimismMintableFiatTokenV2_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: OptimismMintableFiatTokenV2_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: OptimismMintableFiatTokenV2_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)); +}