Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add OptimismMintableFiatTokenV2_2 for compatibility with IOptimismMintableERC20 #451

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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=
12 changes: 11 additions & 1 deletion @types/AnyFiatTokenV2Instance.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
38 changes: 38 additions & 0 deletions contracts/v2/IOptimismMintableFiatToken.sol
Original file line number Diff line number Diff line change
@@ -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;
}
74 changes: 74 additions & 0 deletions contracts/v2/OptimismMintableFiatTokenV2_2.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
48 changes: 48 additions & 0 deletions scripts/deploy/DeployImpl.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
}
}
16 changes: 15 additions & 1 deletion scripts/deploy/deploy-fiat-token.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand All @@ -49,6 +52,8 @@ contract DeployFiatToken is Script, DeployImpl {

uint256 private deployerPrivateKey;

address l1RemoteToken;

/**
* @notice initialize variables from environment
*/
Expand All @@ -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);
Expand All @@ -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);
}

/**
Expand All @@ -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));

Expand Down
36 changes: 36 additions & 0 deletions scripts/deploy/set-l2-standard-bridge.s.sol
Original file line number Diff line number Diff line change
@@ -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();
}
}
39 changes: 39 additions & 0 deletions scripts/deploy/upgrade-impl.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// 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 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();
}
}
4 changes: 3 additions & 1 deletion test/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
FiatTokenV2_1Instance,
FiatTokenV2_2Instance,
FiatTokenV2Instance,
OptimismMintableFiatTokenV2_2Instance,
} from "../../@types/generated";
import _ from "lodash";

Expand Down Expand Up @@ -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,
Expand Down
11 changes: 9 additions & 2 deletions test/helpers/storageSlots.behavior.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -37,9 +40,11 @@ export function usesOriginalStorageSlotPositions<
>({
Contract,
version,
constructorArgs,
}: {
Contract: Truffle.Contract<T>;
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];
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);
}
});
Expand Down
Loading