diff --git a/package.json b/package.json index e057a27..9cf4f5d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "frax-template", - "version": "1.3.3", + "name": "frax-tokens", + "version": "1.0.0", "description": "", "directories": { "lib": "lib", @@ -19,7 +19,6 @@ "author": "Frax Finance", "license": "ISC", "devDependencies": { - "@openzeppelin/contracts": "^5.4.0", "husky": "^8.0.3", "lint-staged": "^13.3.0", "prettier": "^3.6.2", @@ -34,6 +33,8 @@ "ds-test": "github:dapphub/ds-test", "forge-std": "github:foundry-rs/forge-std#60acb7aaadcce2d68e52986a0a66fe79f07d138f", "frax-standard-solidity": "github:FraxFinance/frax-standard-solidity", - "solidity-bytes-utils": "github:GNSPS/solidity-bytes-utils" + "solidity-bytes-utils": "github:GNSPS/solidity-bytes-utils", + "@openzeppelin/contracts-5.2.0": "npm:@openzeppelin/contracts@5.2.0", + "@openzeppelin/contracts-upgradeable-5.2.0": "npm:@openzeppelin/contracts-upgradeable@5.2.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 398b933..26ce314 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,12 @@ settings: importers: .: dependencies: + "@openzeppelin/contracts-5.2.0": + specifier: npm:@openzeppelin/contracts@5.2.0 + version: "@openzeppelin/contracts@5.2.0" + "@openzeppelin/contracts-upgradeable-5.2.0": + specifier: npm:@openzeppelin/contracts-upgradeable@5.2.0 + version: "@openzeppelin/contracts-upgradeable@5.2.0(@openzeppelin/contracts@5.4.0)" ds-test: specifier: github:dapphub/ds-test version: https://codeload.github.com/dapphub/ds-test/tar.gz/e282159d5170298eb2455a6c05280ab5a73a4ef0 @@ -20,9 +26,6 @@ importers: specifier: github:GNSPS/solidity-bytes-utils version: https://codeload.github.com/GNSPS/solidity-bytes-utils/tar.gz/fc502455bb2a7e26a743378df042612dd50d1eb9 devDependencies: - "@openzeppelin/contracts": - specifier: ^5.4.0 - version: 5.4.0 husky: specifier: ^8.0.3 version: 8.0.3 @@ -98,6 +101,16 @@ packages: resolution: { integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== } + "@openzeppelin/contracts-upgradeable@5.2.0": + resolution: + { integrity: sha512-mZIu9oa4tQTlGiOJHk6D3LdJlqFqF6oNOSn6S6UVJtzfs9UsY9/dhMEbAVTwElxUtJnjpf6yA062+oBp+eOyPg== } + peerDependencies: + "@openzeppelin/contracts": 5.2.0 + + "@openzeppelin/contracts@5.2.0": + resolution: + { integrity: sha512-bxjNie5z89W1Ea0NZLZluFh8PrFNn9DH8DQlujEok2yjsOlraUPKID5p1Wk3qdNbf6XkQ1Os2RvfiHrrXLHWKA== } + "@openzeppelin/contracts@5.4.0": resolution: { integrity: sha512-eCYgWnLg6WO+X52I16TZt8uEjbtdkgLC0SUX/xnAksjjrQI4Xfn4iBRoI5j55dmlOhDv1Y7BoR3cU7e3WWhC6A== } @@ -1187,6 +1200,12 @@ snapshots: "@jridgewell/resolve-uri": 3.1.2 "@jridgewell/sourcemap-codec": 1.5.4 + "@openzeppelin/contracts-upgradeable@5.2.0(@openzeppelin/contracts@5.4.0)": + dependencies: + "@openzeppelin/contracts": 5.4.0 + + "@openzeppelin/contracts@5.2.0": {} + "@openzeppelin/contracts@5.4.0": {} "@prettier/sync@0.3.0(prettier@3.6.2)": diff --git a/remappings.txt b/remappings.txt index 92f1540..4fad1d4 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,4 +1,5 @@ frax-std/=node_modules/frax-standard-solidity/src/ @prb/test/=node_modules/@prb/test/ forge-std/=node_modules/forge-std/src/ -ds-test/=node_modules/ds-test/src/ \ No newline at end of file +ds-test/=node_modules/ds-test/src/ +@openzeppelin/=node_modules/@openzeppelin/ \ No newline at end of file diff --git a/src/contracts/Counter.sol b/src/contracts/Counter.sol deleted file mode 100644 index 78c631c..0000000 --- a/src/contracts/Counter.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; - -contract Counter is ERC20 { - uint256 public number; - - constructor() ERC20("Counter", "COUNTER") { - number = 0; - } - - function setNumber(uint256 newNumber) public { - number = newNumber; - } - - function increment() public { - number++; - } -} diff --git a/src/contracts/fraxtal/fpi/FPI.sol b/src/contracts/fraxtal/fpi/FPI.sol new file mode 100644 index 0000000..843e831 --- /dev/null +++ b/src/contracts/fraxtal/fpi/FPI.sol @@ -0,0 +1,25 @@ +pragma solidity ^0.8.0; + +import { ERC20PermitPermissionedOptiMintable } from "src/contracts/fraxtal/shared/ERC20PermitPermissionedOptiMintable.sol"; + +contract FPI is ERC20PermitPermissionedOptiMintable { + /// @param _creator_address The contract creator + /// @param _timelock_address The timelock + /// @param _bridge Address of the L2 standard bridge + /// @param _remoteToken Address of the corresponding L1 token + constructor( + address _creator_address, + address _timelock_address, + address _bridge, + address _remoteToken + ) + ERC20PermitPermissionedOptiMintable( + _creator_address, + _timelock_address, + _bridge, + _remoteToken, + "Frax Price Index", + "FPI" + ) + {} +} diff --git a/src/contracts/fraxtal/fpi/IFPI.sol b/src/contracts/fraxtal/fpi/IFPI.sol new file mode 100644 index 0000000..04f880c --- /dev/null +++ b/src/contracts/fraxtal/fpi/IFPI.sol @@ -0,0 +1,7 @@ +pragma solidity ^0.8.0; + +import { IERC20PermitPermissionedOptiMintable } from "src/contracts/fraxtal/shared/interfaces/IERC20PermitPermissionedOptiMintable.sol"; + +/// @title IFPI +/// @notice Interface for the FPI contract +interface IFPI is IERC20PermitPermissionedOptiMintable {} diff --git a/src/contracts/fraxtal/frxBTC/FrxBTC.sol b/src/contracts/fraxtal/frxBTC/FrxBTC.sol new file mode 100644 index 0000000..92aa64f --- /dev/null +++ b/src/contracts/fraxtal/frxBTC/FrxBTC.sol @@ -0,0 +1,25 @@ +pragma solidity ^0.8.0; + +import { ERC20PermitPermissionedOptiMintable } from "src/contracts/fraxtal/shared/ERC20PermitPermissionedOptiMintable.sol"; + +contract FrxBTC is ERC20PermitPermissionedOptiMintable { + /// @param _creator_address The contract creator + /// @param _timelock_address The timelock + /// @param _bridge Address of the L2 standard bridge + /// @param _remoteToken Address of the corresponding L1 token + constructor( + address _creator_address, + address _timelock_address, + address _bridge, + address _remoteToken + ) + ERC20PermitPermissionedOptiMintable( + _creator_address, + _timelock_address, + _bridge, + _remoteToken, + "Frax Bitcoin", + "frxBTC" + ) + {} +} diff --git a/src/contracts/fraxtal/frxBTC/IFrxBTC.sol b/src/contracts/fraxtal/frxBTC/IFrxBTC.sol new file mode 100644 index 0000000..cc955dd --- /dev/null +++ b/src/contracts/fraxtal/frxBTC/IFrxBTC.sol @@ -0,0 +1,7 @@ +pragma solidity ^0.8.0; + +import { IERC20PermitPermissionedOptiMintable } from "src/contracts/fraxtal/shared/interfaces/IERC20PermitPermissionedOptiMintable.sol"; + +/// @title IFrxBTC +/// @notice Interface for the frxUSD contract +interface IFrxBTC is IERC20PermitPermissionedOptiMintable {} diff --git a/src/contracts/fraxtal/frxETH/FrxETH.sol b/src/contracts/fraxtal/frxETH/FrxETH.sol new file mode 100644 index 0000000..b20fe54 --- /dev/null +++ b/src/contracts/fraxtal/frxETH/FrxETH.sol @@ -0,0 +1,5 @@ +pragma solidity ^0.8.0; + +import { ERC20ExWrappedPPOM } from "src/contracts/fraxtal/shared/ERC20ExWrappedPPOM.sol"; + +contract FrxETH is ERC20ExWrappedPPOM {} diff --git a/src/contracts/fraxtal/frxETH/IFrxETH.sol b/src/contracts/fraxtal/frxETH/IFrxETH.sol new file mode 100644 index 0000000..434672d --- /dev/null +++ b/src/contracts/fraxtal/frxETH/IFrxETH.sol @@ -0,0 +1,7 @@ +pragma solidity ^0.8.0; + +import { IERC20PermitPermissionedOptiMintable } from "src/contracts/fraxtal/shared/interfaces/IERC20PermitPermissionedOptiMintable.sol"; + +interface IFrxETH is IERC20PermitPermissionedOptiMintable { + function adjustTotalSupply(int256 _newTotalSupplyDiff) external; +} diff --git a/src/contracts/fraxtal/frxUSD/FrxUSD.sol b/src/contracts/fraxtal/frxUSD/FrxUSD.sol new file mode 100644 index 0000000..a84346a --- /dev/null +++ b/src/contracts/fraxtal/frxUSD/FrxUSD.sol @@ -0,0 +1,147 @@ +pragma solidity ^0.8.0; + +import { ERC20PermitPermissionedOptiMintable } from "src/contracts/fraxtal/shared/ERC20PermitPermissionedOptiMintable.sol"; + +contract FrxUSD is ERC20PermitPermissionedOptiMintable { + /// @notice Mapping indicating which addresses are frozen + mapping(address => bool) public isFrozen; + + /// @notice Whether or not the contract is paused + bool public isPaused; + + /// @param _creator_address The contract creator + /// @param _timelock_address The timelock + /// @param _bridge Address of the L2 standard bridge + /// @param _remoteToken Address of the corresponding L1 token + constructor( + address _creator_address, + address _timelock_address, + address _bridge, + address _remoteToken + ) + ERC20PermitPermissionedOptiMintable( + _creator_address, + _timelock_address, + _bridge, + _remoteToken, + "Frax USD", + "frxUSD" + ) + {} + + /// @notice External admin gated function to unfreeze a set of accounts + /// @param _owners Array of accounts to be unfrozen + function thawMany(address[] memory _owners) external onlyOwner { + uint256 len = _owners.length; + for (uint256 i; i < len; ++i) { + _thaw(_owners[i]); + } + } + + /// @notice External admin gated function to unfreeze an account + /// @param _owner The account to be unfrozen + function thaw(address _owner) external onlyOwner { + _thaw(_owner); + } + + /// @notice External admin gated function to batch freeze a set of accounts + /// @param _owners Array of accounts to be frozen + function freezeMany(address[] memory _owners) external onlyOwner { + uint256 len = _owners.length; + for (uint256 i; i < len; ++i) { + _freeze(_owners[i]); + } + } + + /// @notice External admin gated function to freeze a given account + /// @param _owner The account to be + function freeze(address _owner) external onlyOwner { + _freeze(_owner); + } + + /// @notice External admin gated function to batch burn balance from a set of accounts + /// @param _owners Array of accounts whose balances will be burned + /// @param _amounts Array of amounts corresponding to the balances to be burned + /// @dev if `_amount` == 0, entire balance will be burned + function burnMany(address[] memory _owners, uint256[] memory _amounts) external onlyOwner { + uint256 lenOwner = _owners.length; + if (_owners.length != _amounts.length) revert ArrayMisMatch(); + for (uint256 i; i < lenOwner; ++i) { + if (_amounts[i] == 0) _amounts[i] = balanceOf(_owners[i]); + _burn(_owners[i], _amounts[i]); + } + } + + /// @notice External admin gated function to burn balance from a given account + /// @param _owner The account whose balance will be burned + /// @param _amount The amount of balance to burn + /// @dev if `_amount` == 0, entire balance will be burned + function burnFrxUsd(address _owner, uint256 _amount) external onlyOwner { + if (_amount == 0) _amount = balanceOf(_owner); + _burn(_owner, _amount); + } + + /// @notice External admin gated pause function + function pause() external onlyOwner { + isPaused = true; + emit Paused(); + } + + /// @notice External admin gated unpause function + function unpause() external onlyOwner { + isPaused = false; + emit Unpaused(); + } + + /* ========== Internals For Admin Gated ========== */ + + /// @notice Internal helper function to freeze an account + /// @param _owner The account to 'frozen' + function _freeze(address _owner) internal { + isFrozen[_owner] = true; + emit AccountFrozen(_owner); + } + + /// @notice Internal helper function to unfreeze an account + /// @param _owner The account to unfreeze + function _thaw(address _owner) internal { + isFrozen[_owner] = false; + emit AccountThawed(_owner); + } + + /* ========== Overrides ========== */ + + /// @notice override for base internal `_update(address,address,uint256)` + /// implements `paused` and `frozen` transfer logic + /// @param from The address from which balance is originating + /// @param to The address whose balance will be incremented + /// @param value The amount to increment/decrement the balances of + /// @dev Owner can bypass pause and freeze checks + function _update(address from, address to, uint256 value) internal override { + if (msg.sender != owner) { + if (isPaused) revert IsPaused(); + if (isFrozen[to] || isFrozen[from] || isFrozen[msg.sender]) revert IsFrozen(); + } + super._update(from, to, value); + } + + /* ========== EVENTS ========== */ + /// @notice Event Emitted when the contract is paused + event Paused(); + + /// @notice Event Emitted when the contract is unpaused + event Unpaused(); + + /// @notice Event Emitted when an address is frozen + /// @param account The account being frozen + event AccountFrozen(address account); + + /// @notice Event Emitted when an address is unfrozen + /// @param account The account being thawed + event AccountThawed(address account); + + /* ========== ERRORS ========== */ + error ArrayMisMatch(); + error IsPaused(); + error IsFrozen(); +} diff --git a/src/contracts/fraxtal/frxUSD/IFrxUSD.sol b/src/contracts/fraxtal/frxUSD/IFrxUSD.sol new file mode 100644 index 0000000..30d7c7a --- /dev/null +++ b/src/contracts/fraxtal/frxUSD/IFrxUSD.sol @@ -0,0 +1,32 @@ +pragma solidity ^0.8.0; + +import { IERC20PermitPermissionedOptiMintable } from "src/contracts/fraxtal/shared/interfaces/IERC20PermitPermissionedOptiMintable.sol"; + +/// @title IFrxUSD +/// @notice Interface for the frxUSD contract +interface IFrxUSD is IERC20PermitPermissionedOptiMintable { + /// @dev state variables + function isFrozen(address account) external view returns (bool); + function isPaused() external view returns (bool); + + /// @dev admin functions + function thawMany(address[] memory _owners) external; + function thaw(address _owner) external; + function freezeMany(address[] memory _owners) external; + function freeze(address _owner) external; + function burnMany(address[] memory _owners, uint256[] memory _amounts) external; + function burnFrxUsd(address _owner, uint256 _amount) external; + function pause() external; + function unpause() external; + + /// @dev events + event Paused(); + event Unpaused(); + event AccountFrozen(address account); + event AccountThawed(address account); + + /// @dev errors + error ArrayMisMatch(); + error IsPaused(); + error IsFrozen(); +} diff --git a/src/contracts/fraxtal/sfrxETH/ISfrxETH.sol b/src/contracts/fraxtal/sfrxETH/ISfrxETH.sol new file mode 100644 index 0000000..10b7c7f --- /dev/null +++ b/src/contracts/fraxtal/sfrxETH/ISfrxETH.sol @@ -0,0 +1,7 @@ +pragma solidity ^0.8.0; + +import { IERC20PermitPermissionedOptiMintable } from "src/contracts/fraxtal/shared/interfaces/IERC20PermitPermissionedOptiMintable.sol"; + +/// @title ISfrxETH +/// @notice Interface for the frxETH contract +interface ISfrxETH is IERC20PermitPermissionedOptiMintable {} diff --git a/src/contracts/fraxtal/sfrxETH/SfrxETH.sol b/src/contracts/fraxtal/sfrxETH/SfrxETH.sol new file mode 100644 index 0000000..dd17ab0 --- /dev/null +++ b/src/contracts/fraxtal/sfrxETH/SfrxETH.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.8.0; + +import { ERC20PermitPermissionedOptiMintable } from "src/contracts/fraxtal/shared/ERC20PermitPermissionedOptiMintable.sol"; + +contract SfrxETH is ERC20PermitPermissionedOptiMintable { + /// @param _creator_address The contract creator + /// @param _timelock_address The timelock + /// @param _bridge Address of the L2 standard bridge + /// @param _remoteToken Address of the corresponding L1 token + constructor( + address _creator_address, + address _timelock_address, + address _bridge, + address _remoteToken + ) + ERC20PermitPermissionedOptiMintable( + _creator_address, + _timelock_address, + _bridge, + _remoteToken, + "Staked Frax Ether", + "sfrxETH" + ) + {} +} diff --git a/src/contracts/fraxtal/sfrxUSD/ISfrxUSD.sol b/src/contracts/fraxtal/sfrxUSD/ISfrxUSD.sol new file mode 100644 index 0000000..9d3db0c --- /dev/null +++ b/src/contracts/fraxtal/sfrxUSD/ISfrxUSD.sol @@ -0,0 +1,7 @@ +pragma solidity ^0.8.0; + +import { IERC20PermitPermissionedOptiMintable } from "src/contracts/fraxtal/shared/interfaces/IERC20PermitPermissionedOptiMintable.sol"; + +/// @title ISfrxUSD +/// @notice Interface for the sfrxUSD contract +interface ISfrxUSD is IERC20PermitPermissionedOptiMintable {} diff --git a/src/contracts/fraxtal/sfrxUSD/SfrxUSD.sol b/src/contracts/fraxtal/sfrxUSD/SfrxUSD.sol new file mode 100644 index 0000000..24d40c2 --- /dev/null +++ b/src/contracts/fraxtal/sfrxUSD/SfrxUSD.sol @@ -0,0 +1,25 @@ +pragma solidity ^0.8.0; + +import { ERC20PermitPermissionedOptiMintable } from "src/contracts/fraxtal/shared/ERC20PermitPermissionedOptiMintable.sol"; + +contract SfrxUSD is ERC20PermitPermissionedOptiMintable { + /// @param _creator_address The contract creator + /// @param _timelock_address The timelock + /// @param _bridge Address of the L2 standard bridge + /// @param _remoteToken Address of the corresponding L1 token + constructor( + address _creator_address, + address _timelock_address, + address _bridge, + address _remoteToken + ) + ERC20PermitPermissionedOptiMintable( + _creator_address, + _timelock_address, + _bridge, + _remoteToken, + "Staked Frax USD", + "sfrxUSD" + ) + {} +} diff --git a/src/contracts/fraxtal/shared/EIP712StoragePad.sol b/src/contracts/fraxtal/shared/EIP712StoragePad.sol new file mode 100644 index 0000000..498a7a4 --- /dev/null +++ b/src/contracts/fraxtal/shared/EIP712StoragePad.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +import { EIP712 } from "@openzeppelin/contracts-5.2.0/utils/cryptography/EIP712.sol"; + +// ==================================================================== +// | ______ _______ | +// | / _____________ __ __ / ____(_____ ____ _____ ________ | +// | / /_ / ___/ __ `| |/_/ / /_ / / __ \/ __ `/ __ \/ ___/ _ \ | +// | / __/ / / / /_/ _> < / __/ / / / / / /_/ / / / / /__/ __/ | +// | /_/ /_/ \__,_/_/|_| /_/ /_/_/ /_/\__,_/_/ /_/\___/\___/ | +// | | +// ==================================================================== +// ========================= EIP712StoragePad ========================= +// ==================================================================== +// Used to preserve storage order + +// Frax Finance: https://github.com/FraxFinance + +// Primary Author(s) +// Travis Moore: https://github.com/FortisFortuna + +contract EIP712StoragePad { + // Old EIP-712 State Variables + // ============================================= + string private DEPRECATED___nameFallback; + string private DEPRECATED___versionFallback; + + // Constructor + // ============================================= + + constructor() {} +} diff --git a/src/contracts/fraxtal/shared/ERC20ExPPOMWrapped.sol b/src/contracts/fraxtal/shared/ERC20ExPPOMWrapped.sol new file mode 100644 index 0000000..7106333 --- /dev/null +++ b/src/contracts/fraxtal/shared/ERC20ExPPOMWrapped.sol @@ -0,0 +1,347 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// ==================================================================== +// | ______ _______ | +// | / _____________ __ __ / ____(_____ ____ _____ ________ | +// | / /_ / ___/ __ `| |/_/ / /_ / / __ \/ __ `/ __ \/ ___/ _ \ | +// | / __/ / / / /_/ _> < / __/ / / / / / /_/ / / / / /__/ __/ | +// | /_/ /_/ \__,_/_/|_| /_/ /_/_/ /_/\__,_/_/ /_/\___/\___/ | +// | | +// ==================================================================== +// ======================== ERC20ExPPOMWrapped ======================== +// ==================================================================== +// Converts a ERC20PermitPermissionedOptiMintable to a WETH-like contract. +// Combines OZ's ERC20Permit and EIP721 into one contract. +// EIP712's _cached & _hashed immutables needed to be converted to private variables so _buildDomainSeparator w, +// as the token name & symbol changed to "Wrapped Frax" and "wFRAX" respectively + +// Frax Finance: https://github.com/FraxFinance + +// Primary Author(s) +// Travis Moore: https://github.com/FortisFortuna + +// Reviewer(s) / Contributor(s) +// Dennis: https://github.com/denett +// Sam Kazemian: https://github.com/samkazemian +// +// +import { ECDSA } from "@openzeppelin/contracts-5.2.0/utils/cryptography/ECDSA.sol"; +import { EIP712StoragePad } from "src/contracts/fraxtal/shared/EIP712StoragePad.sol"; +import { ERC20 } from "@openzeppelin/contracts-5.2.0/token/ERC20/ERC20.sol"; +import { IERC20Permit } from "@openzeppelin/contracts-5.2.0/token/ERC20/extensions/IERC20Permit.sol"; +import { IERC5267 } from "@openzeppelin/contracts-5.2.0/interfaces/IERC5267.sol"; +import { ILegacyMintableERC20 } from "src/contracts/fraxtal/shared/interfaces/ILegacyMintableERC20.sol"; +import { IOptimismMintableERC20 } from "src/contracts/fraxtal/shared/interfaces/IOptimismMintableERC20.sol"; +import { ISemver } from "src/contracts/fraxtal/shared/interfaces/ISemver.sol"; +import { Initializable } from "@openzeppelin/contracts-upgradeable-5.2.0/proxy/utils/Initializable.sol"; +import { MessageHashUtils } from "@openzeppelin/contracts-5.2.0/utils/cryptography/MessageHashUtils.sol"; +import { Nonces } from "@openzeppelin/contracts-5.2.0/utils/Nonces.sol"; +import { ShortStrings, ShortString } from "@openzeppelin/contracts-5.2.0/utils/ShortStrings.sol"; + +/// @title New contract for wFRAX, which is the old FXS token (now the gas token) wrapped and renamed. +/** + * @notice Has Openzeppelin's ERC20Permit with Synthetix's Owned. + * Added WETH9-like features. + * Has OZ's ERC20Permit + * Has OZ's EIP721 + * To preserve storage patterns, some variables are no longer used but need to be included. + */ +contract ERC20ExPPOMWrapped is Initializable, ERC20, IERC20Permit, IERC5267, ISemver, EIP712StoragePad, Nonces { + using ShortStrings for *; + + // ERC20Permit + // ======================================= + + // mapping(address => Counters.Counter) private _nonces; + + bytes32 private constant PERMIT_TYPEHASH = + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + + // ORIGINAL STATE + // ======================================= + + /// @notice [DEPRECATED] The owner address + address private DEPRECATED___owner; + + /// @notice [DEPRECATED] The nominated owner address + address private DEPRECATED___nominated_owner; + + /// @notice [DEPRECATED] The timelock address + address private DEPRECATED___timelock_address; + + // /// @notice [DEPRECATED] Address of the L2 StandardBridge on this network. + // address public immutable DEPRECATED___BRIDGE; + + // /// @notice [DEPRECATED] Address of the corresponding version of this token on the remote chain. + // address public immutable DEPRECATED___REMOTE_TOKEN; + + /// @notice [DEPRECATED] Array of the non-bridge minters + address[] private DEPRECATED___minters_array; + + /// @notice [DEPRECATED] Mapping of the non-bridge minters + /// @dev Mapping is used for faster verification + mapping(address => bool) private DEPRECATED___minters; + + // EIP721 + // ======================================= + bytes32 private constant _TYPE_HASH = + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + + string private _nameFallback; + string private _versionFallback; + + bytes32 private _cachedDomainSeparator; + uint256 private _cachedChainId; + address private _cachedThis; + + bytes32 private _hashedName; + bytes32 private _hashedVersion; + + ShortString private _SStrName; + ShortString private _SStrVersion; + + // ISemver + // ======================================= + + /// @custom:semver 1.0.1 + string public version; + + // ============================================================================== + // CONSTRUCTOR + // ============================================================================== + + constructor() ERC20("Dummy Name", "DUMMY") { + _disableInitializers(); + } + + /// @notice Initializer. + /// @param _nameIn ERC20 name + /// @param _symbolIn ERC20 symbol + /// @param _versionIn Version + function initialize(string memory _nameIn, string memory _symbolIn, string memory _versionIn) public initializer { + // Set version + version = _versionIn; + + // Overwrite ERC20 _name and _symbol + //-------------------------------------- + // Make sure _nameIn and _symbolIn are below 31 bytes + uint256 _nameLength = bytes(_nameIn).length; + uint256 _symbolLength = bytes(_symbolIn).length; + if ((_nameLength >= 32) || (_symbolLength >= 32)) { + revert("Name and/or symbol must be lt 32 bytes"); + } + + // Write to the storage slots + // https://ethereum.stackexchange.com/questions/126269/how-to-store-and-retrieve-string-which-is-more-than-32-bytesor-could-be-less-th + assembly { + // If string length <= 31 we store a short array + // length storage variable layout : + // bytes 0 - 31 : string data + // byte 32 : length * 2 + // data storage variable is UNUSED in this case + sstore(3, or(mload(add(_nameIn, 0x20)), mul(_nameLength, 2))) + sstore(4, or(mload(add(_symbolIn, 0x20)), mul(_symbolLength, 2))) + } + + // Set EIP712 variables + //-------------------------------------- + _SStrName = _nameIn.toShortStringWithFallback(_nameFallback); + _SStrVersion = _versionIn.toShortStringWithFallback(_versionFallback); + _hashedName = keccak256(bytes(_nameIn)); + _hashedVersion = keccak256(bytes(_versionIn)); + _cachedChainId = block.chainid; + _cachedDomainSeparator = _buildDomainSeparator(); + _cachedThis = address(this); + + // Clear old values + // Might not be necessary + //-------------------------------------- + DEPRECATED___owner = address(0); + DEPRECATED___nominated_owner = address(0); + DEPRECATED___timelock_address = address(0); + + // Loop through the minter array and set each mapping to false, and each minter_array value to 0x0 + for (uint256 i = 0; i < DEPRECATED___minters_array.length; i++) { + DEPRECATED___minters[DEPRECATED___minters_array[i]] = false; + delete DEPRECATED___minters_array[i]; + } + } + + /* ========== EIP712 FUNCTIONS ========== */ + + /** + * @dev Returns the domain separator for the current chain. + */ + function _domainSeparatorV4() internal view returns (bytes32) { + if (address(this) == _cachedThis && block.chainid == _cachedChainId) { + return _cachedDomainSeparator; + } else { + return _buildDomainSeparator(); + } + } + + function _buildDomainSeparator() private view returns (bytes32) { + return keccak256(abi.encode(_TYPE_HASH, _hashedName, _hashedVersion, block.chainid, address(this))); + } + + /** + * @dev Given an already https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct[hashed struct], this + * function returns the hash of the fully encoded EIP712 message for this domain. + * + * This hash can be used together with {ECDSA-recover} to obtain the signer of a message. For example: + * + * ```solidity + * bytes32 digest = _hashTypedDataV4(keccak256(abi.encode( + * keccak256("Mail(address to,string contents)"), + * mailTo, + * keccak256(bytes(mailContents)) + * ))); + * address signer = ECDSA.recover(digest, signature); + * ``` + */ + function _hashTypedDataV4(bytes32 structHash) internal view returns (bytes32) { + return MessageHashUtils.toTypedDataHash(_domainSeparatorV4(), structHash); + } + + /** + * @dev See {EIP-5267}. + * + * _Available since v4.9._ + */ + function eip712Domain() + public + view + virtual + override + returns ( + bytes1 fields, + string memory name, + string memory version_, + uint256 chainId, + address verifyingContract, + bytes32 salt, + uint256[] memory extensions + ) + { + return ( + hex"0f", // 01111 + _SStrName.toStringWithFallback(_nameFallback), + _SStrVersion.toStringWithFallback(_versionFallback), + block.chainid, + address(this), + bytes32(0), + new uint256[](0) + ); + } + + /* ========== ERC20Permit FUNCTIONS ========== */ + + /** + * @dev Permit deadline has expired. + */ + error ERC2612ExpiredSignature(uint256 deadline); + + /** + * @dev Mismatched signature. + */ + error ERC2612InvalidSigner(address signer, address owner); + + /** + * @inheritdoc IERC20Permit + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual { + if (block.timestamp > deadline) { + revert ERC2612ExpiredSignature(deadline); + } + + bytes32 structHash = keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline)); + + bytes32 hash = _hashTypedDataV4(structHash); + + address signer = ECDSA.recover(hash, v, r, s); + if (signer != owner) { + revert ERC2612InvalidSigner(signer, owner); + } + + _approve(owner, spender, value); + } + + /** + * @inheritdoc IERC20Permit + */ + function nonces(address owner) public view virtual override(IERC20Permit, Nonces) returns (uint256) { + return super.nonces(owner); + } + + /** + * @inheritdoc IERC20Permit + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view virtual returns (bytes32) { + return _domainSeparatorV4(); + } + + /* ========== BACKWARDS COMPATIBILITY ========== */ + /// @notice Backwards compatibility function for burn. Calls withdraw + /// @param _value Amount of tokens to burn / withdraw + function burn(uint256 _value) public { + withdraw(_value); + } + + /* ========== WETH9-STYLE FUNCTIONS ========== */ + + /// @notice Donate ETH for nothing in return + function donate() public payable { + // Do nothing + } + + /// @notice Fallback gas token deposit + fallback() external payable { + deposit(); + } + + receive() external payable { + deposit(); + } + + /// @notice Deposit gas token for wrapped ERC20. Uses msg.value + function deposit() public payable { + // Accept the gas tokens and mint the ERC20 to the sender + _mint(msg.sender, msg.value); + + emit Deposit(msg.sender, msg.value); + } + + /// @notice Withdraw ERC20 for gas token. + /// @param wad Amount of gas token to receive / wrapped ERC20 to burn. + function withdraw(uint256 wad) public { + // Will revert if sender does not have enough ERC20 tokens + _burn(msg.sender, wad); + + // Give the sender the gas tokens + payable(msg.sender).transfer(wad); + + emit Withdrawal(msg.sender, wad); + } + + /* ========== EVENTS ========== */ + + /// @notice Emitted when the gas token is wrapped + /// @param dst Sender/depositor + /// @param wad Amount of tokens wrapped + event Deposit(address indexed dst, uint256 wad); + + /// @notice Emitted when the gas token is unwrapped + /// @param src Sender / withdrawer + /// @param wad Amount of tokens unwrapped + event Withdrawal(address indexed src, uint256 wad); +} diff --git a/src/contracts/fraxtal/shared/ERC20ExWrappedPPOM.sol b/src/contracts/fraxtal/shared/ERC20ExWrappedPPOM.sol new file mode 100644 index 0000000..bdbee16 --- /dev/null +++ b/src/contracts/fraxtal/shared/ERC20ExWrappedPPOM.sol @@ -0,0 +1,557 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// ==================================================================== +// | ______ _______ | +// | / _____________ __ __ / ____(_____ ____ _____ ________ | +// | / /_ / ___/ __ `| |/_/ / /_ / / __ \/ __ `/ __ \/ ___/ _ \ | +// | / __/ / / / /_/ _> < / __/ / / / / / /_/ / / / / /__/ __/ | +// | /_/ /_/ \__,_/_/|_| /_/ /_/_/ /_/\__,_/_/ /_/\___/\___/ | +// | | +// ==================================================================== +// ======================== ERC20ExWrappedPPOM ======================== +// ==================================================================== +// Converts a WETH-like into an ERC20PermitPermissionedOptiMintable. +// WETH and ERC20 state vars were in different orders, so needed to correctly account for that to preserve data +// Combines OZ's ERC20Permit and EIP721 into one contract. This was needed because of upgrade issues +// EIP712's _cached & _hashed immutables needed to be converted to private variables so _buildDomainSeparator works, +// as the token name & symbol changed to "Frax Ether" and "frxETH" respectively + +// Frax Finance: https://github.com/FraxFinance + +// Primary Author(s) +// Travis Moore: https://github.com/FortisFortuna + +// Reviewer(s) / Contributor(s) +// Dennis: https://github.com/denett +// Sam Kazemian: https://github.com/samkazemian +// +// +import { ECDSA } from "@openzeppelin/contracts-5.2.0/utils/cryptography/ECDSA.sol"; +import { ERC20Burnable } from "@openzeppelin/contracts-5.2.0/token/ERC20/extensions/ERC20Burnable.sol"; +import { ERC20Permit, ERC20 } from "@openzeppelin/contracts-5.2.0/token/ERC20/extensions/ERC20Permit.sol"; +import { ERC20ReorderedState } from "src/contracts/fraxtal/shared/ERC20ReorderedState.sol"; +import { IERC165 } from "@openzeppelin/contracts-5.2.0/utils/introspection/IERC165.sol"; +import { IERC20 } from "@openzeppelin/contracts-5.2.0/token/ERC20/IERC20.sol"; +import { IERC20Permit } from "@openzeppelin/contracts-5.2.0/token/ERC20/extensions/IERC20Permit.sol"; +import { IERC5267 } from "@openzeppelin/contracts-5.2.0/interfaces/IERC5267.sol"; +import { ILegacyMintableERC20 } from "src/contracts/fraxtal/shared/interfaces/ILegacyMintableERC20.sol"; +import { IOptimismMintableERC20 } from "src/contracts/fraxtal/shared/interfaces/IOptimismMintableERC20.sol"; +import { ISemver } from "src/contracts/fraxtal/shared/interfaces/ISemver.sol"; +import { Initializable } from "@openzeppelin/contracts-upgradeable-5.2.0/proxy/utils/Initializable.sol"; +import { MessageHashUtils } from "@openzeppelin/contracts-5.2.0/utils/cryptography/MessageHashUtils.sol"; +import { Nonces } from "@openzeppelin/contracts-5.2.0/utils/Nonces.sol"; +import { OwnedV2 } from "src/contracts/fraxtal/shared/OwnedV2.sol"; +import { ShortStrings, ShortString } from "@openzeppelin/contracts-5.2.0/utils/ShortStrings.sol"; +import { EIP712StoragePad } from "src/contracts/fraxtal/shared/EIP712StoragePad.sol"; + +/// @title New contract for frxETH. Formerly wfrxETH. +/** + * @notice Combines Openzeppelin's ERC20Permit and ERC20Burnable with Synthetix's Owned and Optimism's OptimismMintableERC20. + * Also includes a list of authorized minters + */ +/// @dev ERC20PermitPermissionedOptiMintable adheres to EIP-712/EIP-2612 and can use permits +contract ERC20ExWrappedPPOM is + Initializable, + IERC20, + IERC20Permit, + EIP712StoragePad, + Nonces, + ERC20ReorderedState, + OwnedV2, + IOptimismMintableERC20, + ILegacyMintableERC20, + IERC5267, + ISemver +{ + using ShortStrings for *; + + // EIP721 + // ======================================= + bytes32 private constant PERMIT_TYPEHASH = + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + + bytes32 private constant _TYPE_HASH = + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + + string private _nameFallback; + string private _versionFallback; + + bytes32 private _cachedDomainSeparator; + uint256 private _cachedChainId; + address private _cachedThis; + + bytes32 private _hashedName; + bytes32 private _hashedVersion; + + ShortString private _SStrName; + ShortString private _SStrVersion; + + // ERC20PermitPermissionedOptiMintable + // ======================================= + /// @notice The timelock address + address public timelock_address; + + /// @notice Array of the non-bridge minters + address[] public minters_array; + + /// @notice Mapping of the non-bridge minters + /// @dev Mapping is used for faster verification + mapping(address => bool) public minters; + + /// @notice Address of the L2 StandardBridge on this network. + address public BRIDGE; + + /// @notice Address of the corresponding version of this token on the remote chain. + address public REMOTE_TOKEN; + + // ISemver + // ======================================= + /// @custom:semver 1.0.0 + string public version = "1.0.0"; + + /* ========== CONSTRUCTOR ========== */ + + // /// @custom:semver 1.0.0 + // /// @param _creator_address The contract creator + // /// @param _timelock_address The timelock + // /// @param _bridge Address of the L2 standard bridge + // /// @param _remoteToken Address of the corresponding L1 token + // /// @param _name ERC20 name + // /// @param _symbol ERC20 symbol + // constructor( + // address _creator_address, + // address _timelock_address, + // address _bridge, + // address _remoteToken, + // string memory _name, + // string memory _symbol + // ) EIP712StoragePad(_name) ERC20ReorderedState(_name, _symbol) OwnedV2(_creator_address) { + // REMOTE_TOKEN = _remoteToken; + // BRIDGE = _bridge; + // timelock_address = _timelock_address; + // } + + constructor() ERC20ReorderedState("Dummy Token", "DUMMY") OwnedV2(msg.sender) { + _disableInitializers(); + } + + /// @notice Initializer. + /// @param _timelock_address The timelock + /// @param _bridge Address of the L2 standard bridge + /// @param _remoteToken Address of the corresponding L1 token + /// @param _initTotalSupply The totalSupply + /// @param _nameIn ERC20 name + /// @param _symbolIn ERC20 symbol + /// @param _versionIn Version + function initialize( + address _creator_address, + address _timelock_address, + address _bridge, + address _remoteToken, + uint256 _initTotalSupply, + string memory _nameIn, + string memory _symbolIn, + string memory _versionIn + ) public initializer { + // Set version + version = _versionIn; + + // Overwrite _totalSupply storage + //-------------------------------------- + assembly { + sstore(9, _initTotalSupply) + } + + // Overwrite ERC20 _name and _symbol storage + //-------------------------------------- + // Make sure _nameIn and _symbolIn are below 31 bytes + uint256 _nameLength = bytes(_nameIn).length; + uint256 _symbolLength = bytes(_symbolIn).length; + if ((_nameLength >= 32) || (_symbolLength >= 32)) { + revert("Name and/or symbol must be lt 32 bytes"); + } + + // Write to the storage slots + // https://ethereum.stackexchange.com/questions/126269/how-to-store-and-retrieve-string-which-is-more-than-32-bytesor-could-be-less-th + assembly { + // If string length <= 31 we store a short array + // length storage variable layout : + // bytes 0 - 31 : string data + // byte 32 : length * 2 + // data storage variable is UNUSED in this case + sstore(4, or(mload(add(_nameIn, 0x20)), mul(_nameLength, 2))) + sstore(5, or(mload(add(_symbolIn, 0x20)), mul(_symbolLength, 2))) + } + + // Set EIP712 variables + //-------------------------------------- + _SStrName = _nameIn.toShortStringWithFallback(_nameFallback); + _SStrVersion = _versionIn.toShortStringWithFallback(_versionFallback); + _hashedName = keccak256(bytes(_nameIn)); + _hashedVersion = keccak256(bytes(_versionIn)); + _cachedChainId = block.chainid; + _cachedDomainSeparator = _buildDomainSeparator(); + _cachedThis = address(this); + + // Set owner and timelock + //-------------------------------------- + owner = _creator_address; + timelock_address = _timelock_address; + + // Set BRIDGE and REMOTE_TOKEN + //-------------------------------------- + REMOTE_TOKEN = _remoteToken; + BRIDGE = _bridge; + + // Move existing gas tokens to the _creator_address + //-------------------------------------- + (bool success, ) = _creator_address.call{ value: address(this).balance }(""); + if (!success) { + revert TransferFailed(); + } + } + + /* ========== MODIFIERS ========== */ + + /// @notice A modifier that only allows the contract owner or the timelock to call + modifier onlyByOwnGov() { + require(msg.sender == timelock_address || msg.sender == owner, "Not owner or timelock"); + _; + } + + /// @notice A modifier that only allows a non-bridge minter to call + modifier onlyMinters() { + require(minters[msg.sender] == true, "Only minters"); + _; + } + + /// @notice A modifier that only allows the bridge to call + modifier onlyBridge() { + require(msg.sender == BRIDGE, "OptimismMintableERC20: only bridge can mint and burn"); + _; + } + + /* ========== LEGACY VIEWS ========== */ + + /// @custom:legacy + /// @notice Legacy getter for the remote token. Use REMOTE_TOKEN going forward. + /// @return address The L1 remote token address + function l1Token() public view returns (address) { + return REMOTE_TOKEN; + } + + /// @custom:legacy + /// @notice Legacy getter for the bridge. Use BRIDGE going forward. + /// @return address The bridge address + function l2Bridge() public view returns (address) { + return BRIDGE; + } + + /// @custom:legacy + /// @notice Legacy getter for REMOTE_TOKEN + /// @return address The L1 remote token address + function remoteToken() public view returns (address) { + return REMOTE_TOKEN; + } + + /// @custom:legacy + /// @notice Legacy getter for BRIDGE + /// @return address The bridge address + function bridge() public view returns (address) { + return BRIDGE; + } + + /// @notice ERC165 interface check function. + /// @param _interfaceId Interface ID to check. + /// @return Whether or not the interface is supported by this contract. + function supportsInterface(bytes4 _interfaceId) external pure virtual returns (bool) { + bytes4 iface1 = type(IERC165).interfaceId; + // Interface corresponding to the legacy L2StandardERC20. + bytes4 iface2 = type(ILegacyMintableERC20).interfaceId; + // Interface corresponding to the updated OptimismMintableERC20 (this contract). + bytes4 iface3 = type(IOptimismMintableERC20).interfaceId; + return _interfaceId == iface1 || _interfaceId == iface2 || _interfaceId == iface3; + } + + /* ========== RESTRICTED FUNCTIONS [BRIDGE] ========== */ + + /// @notice Allows the StandardBridge on this network to mint tokens. + /// @param _to Address to mint tokens to. + /// @param _amount Amount of tokens to mint. + function mint( + address _to, + uint256 _amount + ) external virtual override(IOptimismMintableERC20, ILegacyMintableERC20) onlyBridge { + _mint(_to, _amount); + emit Mint(_to, _amount); + } + + /// @notice Allows the StandardBridge on this network to burn tokens. No approval needed + /// @param _from Address to burn tokens from. + /// @param _amount Amount of tokens to burn. + function burn( + address _from, + uint256 _amount + ) external virtual override(IOptimismMintableERC20, ILegacyMintableERC20) onlyBridge { + _burn(_from, _amount); + emit Burn(_from, _amount); + } + + /* ========== RESTRICTED FUNCTIONS [NON-BRIDGE MINTERS] ========== */ + + /// @notice Sames as burnFrom. Left here for backwards-compatibility. Used by non-bridge minters to burn tokens. Must have approval first. + /// @param b_address Address of the account to burn from + /// @param b_amount Amount of tokens to burn + function minter_burn_from(address b_address, uint256 b_amount) public onlyMinters { + burnFrom(b_address, b_amount); + emit TokenMinterBurned(b_address, msg.sender, b_amount); + } + + /// @notice Used by non-bridge minters to mint new tokens + /// @param m_address Address of the account to mint to + /// @param m_amount Amount of tokens to mint + function minter_mint(address m_address, uint256 m_amount) public onlyMinters { + _mint(m_address, m_amount); + emit TokenMinterMinted(msg.sender, m_address, m_amount); + } + + /// @notice Adds a non-bridge minter + /// @param minter_address Address of minter to add + function addMinter(address minter_address) public onlyByOwnGov { + require(minter_address != address(0), "Zero address detected"); + + require(minters[minter_address] == false, "Address already exists"); + minters[minter_address] = true; + minters_array.push(minter_address); + + emit MinterAdded(minter_address); + } + + /// @notice Removes a non-bridge minter + /// @param minter_address Address of minter to remove + function removeMinter(address minter_address) public onlyByOwnGov { + require(minter_address != address(0), "Zero address detected"); + require(minters[minter_address] == true, "Address non-existent"); + + // Delete from the mapping + delete minters[minter_address]; + + // 'Delete' from the array by setting the address to 0x0 + for (uint256 i = 0; i < minters_array.length; i++) { + if (minters_array[i] == minter_address) { + minters_array[i] = address(0); // This will leave a null in the array and keep the indices the same + break; + } + } + + emit MinterRemoved(minter_address); + } + + // ERC20Burnable Functions + // ============================================= + /** + * @dev Destroys a `value` amount of tokens from the caller. + * + * See {ERC20-_burn}. + */ + function burn(uint256 value) public virtual { + _burn(_msgSender(), value); + } + + /** + * @dev Destroys a `value` amount of tokens from `account`, deducting from + * the caller's allowance. + * + * See {ERC20-_burn} and {ERC20-allowance}. + * + * Requirements: + * + * - the caller must have allowance for ``accounts``'s tokens of at least + * `value`. + */ + function burnFrom(address account, uint256 value) public virtual { + _spendAllowance(account, _msgSender(), value); + _burn(account, value); + } + + /* ========== EIP712 FUNCTIONS ========== */ + + /** + * @dev Returns the domain separator for the current chain. + */ + function _domainSeparatorV4() internal view returns (bytes32) { + if (address(this) == _cachedThis && block.chainid == _cachedChainId) { + return _cachedDomainSeparator; + } else { + return _buildDomainSeparator(); + } + } + + function _buildDomainSeparator() private view returns (bytes32) { + return keccak256(abi.encode(_TYPE_HASH, _hashedName, _hashedVersion, block.chainid, address(this))); + } + + /** + * @dev Given an already https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct[hashed struct], this + * function returns the hash of the fully encoded EIP712 message for this domain. + * + * This hash can be used together with {ECDSA-recover} to obtain the signer of a message. For example: + * + * ```solidity + * bytes32 digest = _hashTypedDataV4(keccak256(abi.encode( + * keccak256("Mail(address to,string contents)"), + * mailTo, + * keccak256(bytes(mailContents)) + * ))); + * address signer = ECDSA.recover(digest, signature); + * ``` + */ + function _hashTypedDataV4(bytes32 structHash) internal view returns (bytes32) { + return MessageHashUtils.toTypedDataHash(_domainSeparatorV4(), structHash); + } + + /** + * @dev See {EIP-5267}. + * + * _Available since v4.9._ + */ + function eip712Domain() + public + view + virtual + override + returns ( + bytes1 fields, + string memory name, + string memory version, + uint256 chainId, + address verifyingContract, + bytes32 salt, + uint256[] memory extensions + ) + { + return ( + hex"0f", // 01111 + _SStrName.toStringWithFallback(_nameFallback), + _SStrVersion.toStringWithFallback(_versionFallback), + block.chainid, + address(this), + bytes32(0), + new uint256[](0) + ); + } + + /* ========== ERC20Permit FUNCTIONS ========== */ + + /** + * @dev Permit deadline has expired. + */ + error ERC2612ExpiredSignature(uint256 deadline); + + /** + * @dev Mismatched signature. + */ + error ERC2612InvalidSigner(address signer, address owner); + + /** + * @inheritdoc IERC20Permit + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual { + if (block.timestamp > deadline) { + revert ERC2612ExpiredSignature(deadline); + } + + bytes32 structHash = keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline)); + + bytes32 hash = _hashTypedDataV4(structHash); + + address signer = ECDSA.recover(hash, v, r, s); + if (signer != owner) { + revert ERC2612InvalidSigner(signer, owner); + } + + _approve(owner, spender, value); + } + + /** + * @inheritdoc IERC20Permit + */ + function nonces(address owner) public view virtual override(IERC20Permit, Nonces) returns (uint256) { + return super.nonces(owner); + } + + /** + * @inheritdoc IERC20Permit + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view virtual returns (bytes32) { + return _domainSeparatorV4(); + } + + /* ========== RESTRICTED FUNCTIONS [ADMIN-RELATED] ========== */ + + /// @notice Adjust the totalSupply + function adjustTotalSupply(int256 _newTotalSupplyDiff) public onlyByOwnGov { + if (_newTotalSupplyDiff < 0) { + _totalSupply -= uint256(-_newTotalSupplyDiff); + } else { + _totalSupply += uint256(_newTotalSupplyDiff); + } + } + + /// @notice Sets the timelock address + /// @param _timelock_address Address of the timelock + function setTimelock(address _timelock_address) public onlyByOwnGov { + require(_timelock_address != address(0), "Zero address detected"); + timelock_address = _timelock_address; + emit TimelockChanged(_timelock_address); + } + + /* ========== EVENTS ========== */ + + /// @notice Emitted whenever the bridge burns tokens from an account + /// @param account Address of the account tokens are being burned from + /// @param amount Amount of tokens burned + event Burn(address indexed account, uint256 amount); + + /// @notice Emitted whenever the bridge mints tokens to an account + /// @param account Address of the account tokens are being minted for + /// @param amount Amount of tokens minted. + event Mint(address indexed account, uint256 amount); + + /// @notice Emitted when a non-bridge minter is added + /// @param minter_address Address of the new minter + event MinterAdded(address minter_address); + + /// @notice Emitted when a non-bridge minter is removed + /// @param minter_address Address of the removed minter + event MinterRemoved(address minter_address); + + /// @notice Emitted when the timelock address changes + /// @param timelock_address Address of the new timelock + event TimelockChanged(address timelock_address); + + /// @notice Emitted when a non-bridge minter burns tokens + /// @param from The account whose tokens are burned + /// @param to The minter doing the burning + /// @param amount Amount of tokens burned + event TokenMinterBurned(address indexed from, address indexed to, uint256 amount); + + /// @notice Emitted when a non-bridge minter mints tokens + /// @param from The minter doing the minting + /// @param to The account that gets the newly minted tokens + /// @param amount Amount of tokens minted + event TokenMinterMinted(address indexed from, address indexed to, uint256 amount); + + /// @notice Error for when the gas token withdrawal in the initializer fails + error TransferFailed(); +} diff --git a/src/contracts/fraxtal/shared/ERC20PermitPermissionedOptiMintable.sol b/src/contracts/fraxtal/shared/ERC20PermitPermissionedOptiMintable.sol new file mode 100644 index 0000000..b357b77 --- /dev/null +++ b/src/contracts/fraxtal/shared/ERC20PermitPermissionedOptiMintable.sol @@ -0,0 +1,251 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { IERC20 } from "@openzeppelin/contracts-5.2.0/token/ERC20/IERC20.sol"; +import { ERC20Permit, ERC20 } from "@openzeppelin/contracts-5.2.0/token/ERC20/extensions/ERC20Permit.sol"; +import { ERC20Burnable } from "@openzeppelin/contracts-5.2.0/token/ERC20/extensions/ERC20Burnable.sol"; +import { IERC165 } from "@openzeppelin/contracts-5.2.0/utils/introspection/IERC165.sol"; +import { ILegacyMintableERC20 } from "src/contracts/fraxtal/shared/interfaces/ILegacyMintableERC20.sol"; +import { IOptimismMintableERC20 } from "src/contracts/fraxtal/shared/interfaces/IOptimismMintableERC20.sol"; +import { ISemver } from "src/contracts/fraxtal/shared/interfaces/ISemver.sol"; +import { OwnedV2 } from "src/contracts/fraxtal/shared/OwnedV2.sol"; + +/// @title Parent contract for frxETH.sol, but also CrossChainCanonicalV2 +/** + * @notice Combines Openzeppelin's ERC20Permit and ERC20Burnable with Synthetix's Owned and Optimism's OptimismMintableERC20. + * Also includes a list of authorized minters + */ +/// @dev ERC20PermitPermissionedOptiMintable adheres to EIP-712/EIP-2612 and can use permits +contract ERC20PermitPermissionedOptiMintable is + ERC20Permit, + ERC20Burnable, + OwnedV2, + IOptimismMintableERC20, + ILegacyMintableERC20, + ISemver +{ + /// @custom:semver 1.0.0 + function version() public pure virtual returns (string memory) { + return "1.0.0"; + } + + /// @notice The timelock address + address public timelock_address; + + /// @notice Address of the L2 StandardBridge on this network. + address public immutable BRIDGE; + + /// @notice Address of the corresponding version of this token on the remote chain. + address public immutable REMOTE_TOKEN; + + /// @notice Array of the non-bridge minters + address[] public minters_array; + + /// @notice Mapping of the non-bridge minters + /// @dev Mapping is used for faster verification + mapping(address => bool) public minters; + + /* ========== CONSTRUCTOR ========== */ + + /// @custom:semver 1.0.0 + /// @param _creator_address The contract creator + /// @param _timelock_address The timelock + /// @param _bridge Address of the L2 standard bridge + /// @param _remoteToken Address of the corresponding L1 token + /// @param _name ERC20 name + /// @param _symbol ERC20 symbol + constructor( + address _creator_address, + address _timelock_address, + address _bridge, + address _remoteToken, + string memory _name, + string memory _symbol + ) ERC20(_name, _symbol) ERC20Permit(_name) OwnedV2(_creator_address) { + REMOTE_TOKEN = _remoteToken; + BRIDGE = _bridge; + timelock_address = _timelock_address; + } + + /* ========== MODIFIERS ========== */ + + /// @notice A modifier that only allows the contract owner or the timelock to call + modifier onlyByOwnGov() { + require(msg.sender == timelock_address || msg.sender == owner, "Not owner or timelock"); + _; + } + + /// @notice A modifier that only allows a non-bridge minter to call + modifier onlyMinters() { + require(minters[msg.sender] == true, "Only minters"); + _; + } + + /// @notice A modifier that only allows the bridge to call + modifier onlyBridge() { + require(msg.sender == BRIDGE, "OptimismMintableERC20: only bridge can mint and burn"); + _; + } + + /* ========== LEGACY VIEWS ========== */ + + /// @custom:legacy + /// @notice Legacy getter for the remote token. Use REMOTE_TOKEN going forward. + /// @return address The L1 remote token address + function l1Token() public view returns (address) { + return REMOTE_TOKEN; + } + + /// @custom:legacy + /// @notice Legacy getter for the bridge. Use BRIDGE going forward. + /// @return address The bridge address + function l2Bridge() public view returns (address) { + return BRIDGE; + } + + /// @custom:legacy + /// @notice Legacy getter for REMOTE_TOKEN + /// @return address The L1 remote token address + function remoteToken() public view returns (address) { + return REMOTE_TOKEN; + } + + /// @custom:legacy + /// @notice Legacy getter for BRIDGE + /// @return address The bridge address + function bridge() public view returns (address) { + return BRIDGE; + } + + /// @notice ERC165 interface check function. + /// @param _interfaceId Interface ID to check. + /// @return Whether or not the interface is supported by this contract. + function supportsInterface(bytes4 _interfaceId) external pure virtual returns (bool) { + bytes4 iface1 = type(IERC165).interfaceId; + // Interface corresponding to the legacy L2StandardERC20. + bytes4 iface2 = type(ILegacyMintableERC20).interfaceId; + // Interface corresponding to the updated OptimismMintableERC20 (this contract). + bytes4 iface3 = type(IOptimismMintableERC20).interfaceId; + return _interfaceId == iface1 || _interfaceId == iface2 || _interfaceId == iface3; + } + + /* ========== RESTRICTED FUNCTIONS [BRIDGE] ========== */ + + /// @notice Allows the StandardBridge on this network to mint tokens. + /// @param _to Address to mint tokens to. + /// @param _amount Amount of tokens to mint. + function mint( + address _to, + uint256 _amount + ) external virtual override(IOptimismMintableERC20, ILegacyMintableERC20) onlyBridge { + _mint(_to, _amount); + emit Mint(_to, _amount); + } + + /// @notice Allows the StandardBridge on this network to burn tokens. + /// @param _from Address to burn tokens from. + /// @param _amount Amount of tokens to burn. + function burn( + address _from, + uint256 _amount + ) external virtual override(IOptimismMintableERC20, ILegacyMintableERC20) onlyBridge { + _burn(_from, _amount); + emit Burn(_from, _amount); + } + + /* ========== RESTRICTED FUNCTIONS [NON-BRIDGE MINTERS] ========== */ + + /// @notice Used by non-bridge minters to burn tokens + /// @param b_address Address of the account to burn from + /// @param b_amount Amount of tokens to burn + function minter_burn_from(address b_address, uint256 b_amount) public onlyMinters { + super.burnFrom(b_address, b_amount); + emit TokenMinterBurned(b_address, msg.sender, b_amount); + } + + /// @notice Used by non-bridge minters to mint new tokens + /// @param m_address Address of the account to mint to + /// @param m_amount Amount of tokens to mint + function minter_mint(address m_address, uint256 m_amount) public onlyMinters { + super._mint(m_address, m_amount); + emit TokenMinterMinted(msg.sender, m_address, m_amount); + } + + /// @notice Adds a non-bridge minter + /// @param minter_address Address of minter to add + function addMinter(address minter_address) public onlyByOwnGov { + require(minter_address != address(0), "Zero address detected"); + + require(minters[minter_address] == false, "Address already exists"); + minters[minter_address] = true; + minters_array.push(minter_address); + + emit MinterAdded(minter_address); + } + + /// @notice Removes a non-bridge minter + /// @param minter_address Address of minter to remove + function removeMinter(address minter_address) public onlyByOwnGov { + require(minter_address != address(0), "Zero address detected"); + require(minters[minter_address] == true, "Address non-existent"); + + // Delete from the mapping + delete minters[minter_address]; + + // 'Delete' from the array by setting the address to 0x0 + for (uint256 i = 0; i < minters_array.length; i++) { + if (minters_array[i] == minter_address) { + minters_array[i] = address(0); // This will leave a null in the array and keep the indices the same + break; + } + } + + emit MinterRemoved(minter_address); + } + + /* ========== RESTRICTED FUNCTIONS [ADMIN-RELATED] ========== */ + + /// @notice Sets the timelock address + /// @param _timelock_address Address of the timelock + function setTimelock(address _timelock_address) public onlyByOwnGov { + require(_timelock_address != address(0), "Zero address detected"); + timelock_address = _timelock_address; + emit TimelockChanged(_timelock_address); + } + + /* ========== EVENTS ========== */ + + /// @notice Emitted whenever the bridge burns tokens from an account + /// @param account Address of the account tokens are being burned from + /// @param amount Amount of tokens burned + event Burn(address indexed account, uint256 amount); + + /// @notice Emitted whenever the bridge mints tokens to an account + /// @param account Address of the account tokens are being minted for + /// @param amount Amount of tokens minted. + event Mint(address indexed account, uint256 amount); + + /// @notice Emitted when a non-bridge minter is added + /// @param minter_address Address of the new minter + event MinterAdded(address minter_address); + + /// @notice Emitted when a non-bridge minter is removed + /// @param minter_address Address of the removed minter + event MinterRemoved(address minter_address); + + /// @notice Emitted when the timelock address changes + /// @param timelock_address Address of the removed timelock + event TimelockChanged(address timelock_address); + + /// @notice Emitted when a non-bridge minter burns tokens + /// @param from The account whose tokens are burned + /// @param to The minter doing the burning + /// @param amount Amount of tokens burned + event TokenMinterBurned(address indexed from, address indexed to, uint256 amount); + + /// @notice Emitted when a non-bridge minter mints tokens + /// @param from The minter doing the minting + /// @param to The account that gets the newly minted tokens + /// @param amount Amount of tokens minted + event TokenMinterMinted(address indexed from, address indexed to, uint256 amount); +} diff --git a/src/contracts/fraxtal/shared/ERC20ReorderedState.sol b/src/contracts/fraxtal/shared/ERC20ReorderedState.sol new file mode 100644 index 0000000..b666565 --- /dev/null +++ b/src/contracts/fraxtal/shared/ERC20ReorderedState.sol @@ -0,0 +1,324 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/ERC20.sol) + +pragma solidity ^0.8.20; + +import { IERC20 } from "@openzeppelin/contracts-5.2.0/token/ERC20/IERC20.sol"; +import { IERC20Metadata } from "@openzeppelin/contracts-5.2.0/token/ERC20/extensions/IERC20Metadata.sol"; +import { Context } from "@openzeppelin/contracts-5.2.0/utils/Context.sol"; +import { IERC20Errors } from "@openzeppelin/contracts-5.2.0/interfaces/draft-IERC6093.sol"; + +/** + * @dev Implementation of the {IERC20} interface. + * FRAX: Reorder the state variables to allow an ex WETH9-like contract to be forked over and preserve state + * + * This implementation is agnostic to the way tokens are created. This means + * that a supply mechanism has to be added in a derived contract using {_mint}. + * + * TIP: For a detailed writeup see our guide + * https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[How + * to implement supply mechanisms]. + * + * The default value of {decimals} is 18. To change this, you should override + * this function so it returns a different value. + * + * We have followed general OpenZeppelin Contracts guidelines: functions revert + * instead returning `false` on failure. This behavior is nonetheless + * conventional and does not conflict with the expectations of ERC20 + * applications. + * + * Additionally, an {Approval} event is emitted on calls to {transferFrom}. + * This allows applications to reconstruct the allowance for all accounts just + * by listening to said events. Other implementations of the EIP may not emit + * these events, as it isn't required by the specification. + */ +abstract contract ERC20ReorderedState is Context, IERC20, IERC20Metadata, IERC20Errors { + /** + * @dev In previous versions `_PERMIT_TYPEHASH` was declared as `immutable`. + * However, to ensure consistency with the upgradeable transpiler, we will continue + * to reserve a slot. + * @custom:oz-renamed-from _PERMIT_TYPEHASH + */ + // solhint-disable-next-line var-name-mixedcase + bytes32 private _PERMIT_TYPEHASH_DEPRECATED_SLOT; + + string private _name; + string private _symbol; + uint8 private _decimals = 18; + mapping(address account => uint256) private _balances; + mapping(address account => mapping(address spender => uint256)) private _allowances; + uint256 public _totalSupply; + + /** + * @dev Sets the values for {name} and {symbol}. + * + * All two of these values are immutable: they can only be set once during + * construction. + */ + constructor(string memory name_, string memory symbol_) { + _name = name_; + _symbol = symbol_; + } + + /** + * @dev Returns the name of the token. + */ + function name() public view virtual returns (string memory) { + return _name; + } + + /** + * @dev Returns the symbol of the token, usually a shorter version of the + * name. + */ + function symbol() public view virtual returns (string memory) { + return _symbol; + } + + /** + * @dev Returns the number of decimals used to get its user representation. + * For example, if `decimals` equals `2`, a balance of `505` tokens should + * be displayed to a user as `5.05` (`505 / 10 ** 2`). + * + * Tokens usually opt for a value of 18, imitating the relationship between + * Ether and Wei. This is the default value returned by this function, unless + * it's overridden. + * + * NOTE: This information is only used for _display_ purposes: it in + * no way affects any of the arithmetic of the contract, including + * {IERC20-balanceOf} and {IERC20-transfer}. + */ + function decimals() public view virtual returns (uint8) { + return _decimals; + } + + /** + * @dev See {IERC20-totalSupply}. + */ + function totalSupply() public view virtual returns (uint256) { + return _totalSupply; + } + + /** + * @dev See {IERC20-balanceOf}. + */ + function balanceOf(address account) public view virtual returns (uint256) { + return _balances[account]; + } + + /** + * @dev See {IERC20-transfer}. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - the caller must have a balance of at least `value`. + */ + function transfer(address to, uint256 value) public virtual returns (bool) { + address owner = _msgSender(); + _transfer(owner, to, value); + return true; + } + + /** + * @dev See {IERC20-allowance}. + */ + function allowance(address owner, address spender) public view virtual returns (uint256) { + return _allowances[owner][spender]; + } + + /** + * @dev See {IERC20-approve}. + * + * NOTE: If `value` is the maximum `uint256`, the allowance is not updated on + * `transferFrom`. This is semantically equivalent to an infinite approval. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function approve(address spender, uint256 value) public virtual returns (bool) { + address owner = _msgSender(); + _approve(owner, spender, value); + return true; + } + + /** + * @dev See {IERC20-transferFrom}. + * + * Emits an {Approval} event indicating the updated allowance. This is not + * required by the EIP. See the note at the beginning of {ERC20}. + * + * NOTE: Does not update the allowance if the current allowance + * is the maximum `uint256`. + * + * Requirements: + * + * - `from` and `to` cannot be the zero address. + * - `from` must have a balance of at least `value`. + * - the caller must have allowance for ``from``'s tokens of at least + * `value`. + */ + function transferFrom(address from, address to, uint256 value) public virtual returns (bool) { + address spender = _msgSender(); + _spendAllowance(from, spender, value); + _transfer(from, to, value); + return true; + } + + /** + * @dev Moves a `value` amount of tokens from `from` to `to`. + * + * This internal function is equivalent to {transfer}, and can be used to + * e.g. implement automatic token fees, slashing mechanisms, etc. + * + * Emits a {Transfer} event. + * + * NOTE: This function is not virtual, {_update} should be overridden instead. + */ + function _transfer(address from, address to, uint256 value) internal { + if (from == address(0)) { + revert ERC20InvalidSender(address(0)); + } + if (to == address(0)) { + revert ERC20InvalidReceiver(address(0)); + } + _update(from, to, value); + } + + /** + * @dev Transfers a `value` amount of tokens from `from` to `to`, or alternatively mints (or burns) if `from` + * (or `to`) is the zero address. All customizations to transfers, mints, and burns should be done by overriding + * this function. + * + * Emits a {Transfer} event. + */ + function _update(address from, address to, uint256 value) internal virtual { + if (from == address(0)) { + // Overflow check required: The rest of the code assumes that totalSupply never overflows + _totalSupply += value; + } else { + uint256 fromBalance = _balances[from]; + if (fromBalance < value) { + revert ERC20InsufficientBalance(from, fromBalance, value); + } + unchecked { + // Overflow not possible: value <= fromBalance <= totalSupply. + _balances[from] = fromBalance - value; + } + } + + if (to == address(0)) { + unchecked { + // Overflow not possible: value <= totalSupply or value <= fromBalance <= totalSupply. + _totalSupply -= value; + } + } else { + unchecked { + // Overflow not possible: balance + value is at most totalSupply, which we know fits into a uint256. + _balances[to] += value; + } + } + + emit Transfer(from, to, value); + } + + /** + * @dev Creates a `value` amount of tokens and assigns them to `account`, by transferring it from address(0). + * Relies on the `_update` mechanism + * + * Emits a {Transfer} event with `from` set to the zero address. + * + * NOTE: This function is not virtual, {_update} should be overridden instead. + */ + function _mint(address account, uint256 value) internal { + if (account == address(0)) { + revert ERC20InvalidReceiver(address(0)); + } + _update(address(0), account, value); + } + + /** + * @dev Destroys a `value` amount of tokens from `account`, lowering the total supply. + * Relies on the `_update` mechanism. + * + * Emits a {Transfer} event with `to` set to the zero address. + * + * NOTE: This function is not virtual, {_update} should be overridden instead + */ + function _burn(address account, uint256 value) internal { + if (account == address(0)) { + revert ERC20InvalidSender(address(0)); + } + _update(account, address(0), value); + } + + /** + * @dev Sets `value` as the allowance of `spender` over the `owner` s tokens. + * + * This internal function is equivalent to `approve`, and can be used to + * e.g. set automatic allowances for certain subsystems, etc. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `owner` cannot be the zero address. + * - `spender` cannot be the zero address. + * + * Overrides to this logic should be done to the variant with an additional `bool emitEvent` argument. + */ + function _approve(address owner, address spender, uint256 value) internal { + _approve(owner, spender, value, true); + } + + /** + * @dev Variant of {_approve} with an optional flag to enable or disable the {Approval} event. + * + * By default (when calling {_approve}) the flag is set to true. On the other hand, approval changes made by + * `_spendAllowance` during the `transferFrom` operation set the flag to false. This saves gas by not emitting any + * `Approval` event during `transferFrom` operations. + * + * Anyone who wishes to continue emitting `Approval` events on the`transferFrom` operation can force the flag to + * true using the following override: + * ``` + * function _approve(address owner, address spender, uint256 value, bool) internal virtual override { + * super._approve(owner, spender, value, true); + * } + * ``` + * + * Requirements are the same as {_approve}. + */ + function _approve(address owner, address spender, uint256 value, bool emitEvent) internal virtual { + if (owner == address(0)) { + revert ERC20InvalidApprover(address(0)); + } + if (spender == address(0)) { + revert ERC20InvalidSpender(address(0)); + } + _allowances[owner][spender] = value; + if (emitEvent) { + emit Approval(owner, spender, value); + } + } + + /** + * @dev Updates `owner` s allowance for `spender` based on spent `value`. + * + * Does not update the allowance value in case of infinite allowance. + * Revert if not enough allowance is available. + * + * Does not emit an {Approval} event. + */ + function _spendAllowance(address owner, address spender, uint256 value) internal virtual { + uint256 currentAllowance = allowance(owner, spender); + if (currentAllowance != type(uint256).max) { + if (currentAllowance < value) { + revert ERC20InsufficientAllowance(spender, currentAllowance, value); + } + unchecked { + _approve(owner, spender, currentAllowance - value, false); + } + } + } +} diff --git a/src/contracts/fraxtal/shared/OwnedV2.sol b/src/contracts/fraxtal/shared/OwnedV2.sol new file mode 100644 index 0000000..7e97a7d --- /dev/null +++ b/src/contracts/fraxtal/shared/OwnedV2.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.8.4; + +// https://docs.synthetix.io/contracts/Owned +contract OwnedV2 { + error OwnerCannotBeZero(); + error InvalidOwnershipAcceptance(); + error OnlyOwner(); + + address public owner; + address public nominatedOwner; + + constructor(address _owner) { + // require(_owner != address(0), "Owner address cannot be 0"); + if (_owner == address(0)) revert OwnerCannotBeZero(); + owner = _owner; + emit OwnerChanged(address(0), _owner); + } + + function nominateNewOwner(address _owner) external onlyOwner { + nominatedOwner = _owner; + emit OwnerNominated(_owner); + } + + function acceptOwnership() external { + // require(msg.sender == nominatedOwner, "You must be nominated before you can accept ownership"); + if (msg.sender != nominatedOwner) revert InvalidOwnershipAcceptance(); + emit OwnerChanged(owner, nominatedOwner); + owner = nominatedOwner; + nominatedOwner = address(0); + } + + modifier onlyOwner() { + // require(msg.sender == owner, "Only the contract owner may perform this action"); + if (msg.sender != owner) revert OnlyOwner(); + _; + } + + event OwnerNominated(address newOwner); + event OwnerChanged(address oldOwner, address newOwner); +} diff --git a/src/contracts/fraxtal/shared/interfaces/IERC20PermitPermissionedOptiMintable.sol b/src/contracts/fraxtal/shared/interfaces/IERC20PermitPermissionedOptiMintable.sol new file mode 100644 index 0000000..6aa559c --- /dev/null +++ b/src/contracts/fraxtal/shared/interfaces/IERC20PermitPermissionedOptiMintable.sol @@ -0,0 +1,54 @@ +pragma solidity ^0.8.0; + +import { IERC20 } from "@openzeppelin/contracts-5.2.0/token/ERC20/IERC20.sol"; +import { IERC20Permit } from "@openzeppelin/contracts-5.2.0/token/ERC20/extensions/IERC20Permit.sol"; +import { IMinter } from "src/contracts/interfaces/IMinter.sol"; +import { ILegacyMintableERC20 } from "src/contracts/fraxtal/shared/interfaces/ILegacyMintableERC20.sol"; +import { IOptimismMintableERC20 } from "src/contracts/fraxtal/shared/interfaces/IOptimismMintableERC20.sol"; +import { ISemver } from "src/contracts/fraxtal/shared/interfaces/ISemver.sol"; + +interface IERC20PermitPermissionedOptiMintable is + IERC20, + IERC20Permit, + IMinter, + ILegacyMintableERC20, + IOptimismMintableERC20, + ISemver +{ + /// @dev state variables + function timelock_address() external view returns (address); + function BRIDGE() external view returns (address); + function REMOTE_TOKEN() external view returns (address); + + /// @dev OwnedV2 + function owner() external view returns (address); + function nominatedOwner() external view returns (address); + function nominateNewOwner(address _owner) external; + function acceptOwnership() external; + + error OwnerCannotBeZero(); + error InvalidOwnershipAcceptance(); + error OnlyOwner(); + + event OwnerNominated(address newOwner); + event OwnerChanged(address oldOwner, address newOwner); + + /// @dev ERC20Burnable + function burn(uint256 value) external; + function burnFrom(address account, uint256 value) external; + + /// @dev erc165 interface check function + function supportsInterface(bytes4 interfaceId) external view returns (bool); + + /// @dev restricted functions (bridge) + function mint(address _to, uint256 _amount) external override(ILegacyMintableERC20, IOptimismMintableERC20); + function burn(address _from, uint256 _amount) external override(ILegacyMintableERC20, IOptimismMintableERC20); + + /// @dev restricted functions (timelock) + function setTimelock(address _timelock_address) external; + + /// @dev events + event Burn(address indexed account, uint256 amount); + event Mint(address indexed account, uint256 amount); + event TimelockChanged(address timelock_address); +} diff --git a/src/contracts/fraxtal/shared/interfaces/ILegacyMintableERC20.sol b/src/contracts/fraxtal/shared/interfaces/ILegacyMintableERC20.sol new file mode 100644 index 0000000..3f2bfaf --- /dev/null +++ b/src/contracts/fraxtal/shared/interfaces/ILegacyMintableERC20.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { IERC165 } from "@openzeppelin/contracts-5.2.0/utils/introspection/IERC165.sol"; +/// @custom:legacy +/// @title ILegacyMintableERC20 +/// @notice This interface was available on the legacy L2StandardERC20 contract. +/// It remains available on the OptimismMintableERC20 contract for +/// backwards compatibility. + +interface ILegacyMintableERC20 is IERC165 { + function l1Token() external view returns (address); + + function mint(address _to, uint256 _amount) external; + + function burn(address _from, uint256 _amount) external; +} diff --git a/src/contracts/fraxtal/shared/interfaces/IOptimismMintableERC20.sol b/src/contracts/fraxtal/shared/interfaces/IOptimismMintableERC20.sol new file mode 100644 index 0000000..994b2e6 --- /dev/null +++ b/src/contracts/fraxtal/shared/interfaces/IOptimismMintableERC20.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { IERC165 } from "@openzeppelin/contracts-5.2.0/utils/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. +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/src/contracts/fraxtal/shared/interfaces/ISemver.sol b/src/contracts/fraxtal/shared/interfaces/ISemver.sol new file mode 100644 index 0000000..22a5dde --- /dev/null +++ b/src/contracts/fraxtal/shared/interfaces/ISemver.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/// @title ISemver +/// @notice ISemver is a simple contract for ensuring that contracts are +/// versioned using semantic versioning. +interface ISemver { + /// @notice Getter for the semantic version of the contract. This is not + /// meant to be used onchain but instead meant to be used by offchain + /// tooling. + /// @return Semver contract version as a string. + function version() external view returns (string memory); +} diff --git a/src/contracts/fraxtal/wfrax/IWFRAX.sol b/src/contracts/fraxtal/wfrax/IWFRAX.sol new file mode 100644 index 0000000..2975ca3 --- /dev/null +++ b/src/contracts/fraxtal/wfrax/IWFRAX.sol @@ -0,0 +1,22 @@ +pragma solidity ^0.8.0; + +import { IERC20 } from "@openzeppelin/contracts-5.2.0/token/ERC20/IERC20.sol"; +import { IERC20Permit } from "@openzeppelin/contracts-5.2.0/token/ERC20/extensions/IERC20Permit.sol"; +import { IERC5267 } from "@openzeppelin/contracts-5.2.0/interfaces/IERC5267.sol"; +import { ISemver } from "src/contracts/fraxtal/shared/interfaces/ISemver.sol"; + +interface IWFRAX is IERC20, IERC20Permit, IERC5267, ISemver { + /// @dev functions + function burn(uint256 _value) external; + function donate() external payable; + function deposit() external payable; + function withdraw(uint256 wad) external; + + /// @dev errors + error ERC2612ExpiredSignature(uint256 deadline); + error ERC2612InvalidSignature(); + + /// @dev events + event Deposit(address indexed dst, uint256 wad); + event Withdrawal(address indexed src, uint256 wad); +} diff --git a/src/contracts/fraxtal/wfrax/WFRAX.sol b/src/contracts/fraxtal/wfrax/WFRAX.sol new file mode 100644 index 0000000..f712983 --- /dev/null +++ b/src/contracts/fraxtal/wfrax/WFRAX.sol @@ -0,0 +1,5 @@ +pragma solidity ^0.8.0; + +import { ERC20ExPPOMWrapped } from "src/contracts/fraxtal/shared/ERC20ExPPOMWrapped.sol"; + +contract WFRAX is ERC20ExPPOMWrapped {} diff --git a/src/contracts/interfaces/IMinter.sol b/src/contracts/interfaces/IMinter.sol new file mode 100644 index 0000000..a36ea46 --- /dev/null +++ b/src/contracts/interfaces/IMinter.sol @@ -0,0 +1,22 @@ +pragma solidity ^0.8.0; + +interface IMinter { + // state variables + function minters_array(uint256) external view returns (address); + function minters(address) external view returns (bool); + + // state changers + function minter_burn_from(address b_address, uint256 b_amount) external; + function minter_mint(address m_address, uint256 m_amount) external; + function addMinter(address minter_address) external; + function removeMinter(address minter_address) external; + + // errors + error OnlyMinters(); + + // events + event MinterAdded(address minter_address); + event MinterRemoved(address minter_address); + event TokenMinterBurned(address indexed from, address indexed to, uint256 amount); + event TokenMinterMinted(address indexed from, address indexed to, uint256 amount); +} diff --git a/src/script/.gitkeep b/src/script/.gitkeep new file mode 100644 index 0000000..c005c10 --- /dev/null +++ b/src/script/.gitkeep @@ -0,0 +1,14 @@ +// Copyright 2025 carter +// +// 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 +// +// https://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. + diff --git a/src/script/DeployCounter.s.sol b/src/script/DeployCounter.s.sol deleted file mode 100644 index 3bb7605..0000000 --- a/src/script/DeployCounter.s.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: ISC -pragma solidity ^0.8.19; - -import { BaseScript } from "frax-std/BaseScript.sol"; -import { console } from "frax-std/FraxTest.sol"; -import { Counter } from "../contracts/Counter.sol"; - -// This is a free function that can be imported and used in tests or other scripts -function deployCounter() returns (address _address) { - Counter _counter = new Counter(); - _address = address(_counter); -} - -// Run this with source .env && forge script --broadcast --rpc-url $MAINNET_URL DeployCounter.s.sol -contract DeployCounter is BaseScript { - function run() public broadcaster { - address _address = deployCounter(); - console.log("Deployed Counter at address: ", _address); - } -} diff --git a/src/test/BaseTest.t.sol b/src/test/BaseTest.t.sol deleted file mode 100644 index 5b3602a..0000000 --- a/src/test/BaseTest.t.sol +++ /dev/null @@ -1,71 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import "frax-std/FraxTest.sol"; -import "../contracts/Counter.sol"; -import "./Helpers.sol"; - -import { Mainnet } from "src/contracts/chain-constants/Mainnet.sol"; -import { FraxtalL1Devnet } from "src/contracts/chain-constants/FraxtalL1Devnet.sol"; -import { FraxtalL2 } from "src/contracts/chain-constants/FraxtalL2.sol"; -import { FraxtalTestnetL1 } from "src/contracts/chain-constants/FraxtalTestnetL1.sol"; -import { FraxtalTestnetL2 } from "src/contracts/chain-constants/FraxtalTestnetL2.sol"; - -contract BaseTest is FraxTest { - Counter public counter; - - address timelock = Mainnet.TIMELOCK_ADDRESS; - // Fraxtal / Fraxtal Testnet L1 & L2 addresses - address public PROXY_ADMIN; - address public COMPTROLLER; - // Fraxtal / Fraxtal Testnet L1 addresses - address public ADDRESS_MANAGER; - address public L1_CROSS_DOMAIN_MESSENGER_PROXY; - address public L1_ERC721_BRIDGE_PROXY; - address public L1_STANDARD_BRIDGE_PROXY; - address public L2_OUTPUT_ORACLE_PROXY; - address public OPTIMISM_MINTABLE_ERC20_FACTORY_PROXY; - address public OPTIMISM_PORTAL_PROXY; - address public SYSTEM_CONFIG_PROXY; - // Fraxtal / Fraxtal Testnet L2 addresses - address public FRAXSWAP_FACTORY; - address public FRAXSWAP_ROUTER; - address public FRAXSWAP_ROUTER_MULTIHOP; - - constructor() { - // Setup fraxtal / fraxtal testnet L1 addresses - if (block.chainid == Mainnet.CHAIN_ID) { - PROXY_ADMIN = Mainnet.PROXY_ADMIN; - COMPTROLLER = Mainnet.COMPTROLLER; - ADDRESS_MANAGER = Mainnet.ADDRESS_MANAGER; - L1_CROSS_DOMAIN_MESSENGER_PROXY = Mainnet.L1_CROSS_DOMAIN_MESSENGER_PROXY; - L1_ERC721_BRIDGE_PROXY = Mainnet.L1_ERC721_BRIDGE_PROXY; - L1_STANDARD_BRIDGE_PROXY = Mainnet.L1_STANDARD_BRIDGE_PROXY; - L2_OUTPUT_ORACLE_PROXY = Mainnet.L2_OUTPUT_ORACLE_PROXY; - OPTIMISM_MINTABLE_ERC20_FACTORY_PROXY = Mainnet.OPTIMISM_MINTABLE_ERC20_FACTORY_PROXY; - OPTIMISM_PORTAL_PROXY = Mainnet.OPTIMISM_PORTAL_PROXY; - SYSTEM_CONFIG_PROXY = Mainnet.SYSTEM_CONFIG_PROXY; - } else if (block.chainid == FraxtalTestnetL1.CHAIN_ID) { - PROXY_ADMIN = FraxtalTestnetL1.PROXY_ADMIN; - COMPTROLLER = FraxtalTestnetL1.COMPTROLLER; - ADDRESS_MANAGER = FraxtalTestnetL1.ADDRESS_MANAGER; - L1_CROSS_DOMAIN_MESSENGER_PROXY = FraxtalTestnetL1.L1_CROSS_DOMAIN_MESSENGER_PROXY; - L1_ERC721_BRIDGE_PROXY = FraxtalTestnetL1.L1_ERC721_BRIDGE_PROXY; - L1_STANDARD_BRIDGE_PROXY = FraxtalTestnetL1.L1_STANDARD_BRIDGE_PROXY; - L2_OUTPUT_ORACLE_PROXY = FraxtalTestnetL1.L2_OUTPUT_ORACLE_PROXY; - OPTIMISM_MINTABLE_ERC20_FACTORY_PROXY = FraxtalTestnetL1.OPTIMISM_MINTABLE_ERC20_FACTORY_PROXY; - OPTIMISM_PORTAL_PROXY = FraxtalTestnetL1.OPTIMISM_PORTAL_PROXY; - SYSTEM_CONFIG_PROXY = FraxtalTestnetL1.SYSTEM_CONFIG_PROXY; - } - // Setup fraxtal / fraxtal testnet L2 addresses - if (block.chainid == FraxtalL2.CHAIN_ID) { - FRAXSWAP_FACTORY = FraxtalL2.FRAXSWAP_FACTORY; - FRAXSWAP_ROUTER = FraxtalL2.FRAXSWAP_ROUTER; - FRAXSWAP_ROUTER_MULTIHOP = FraxtalL2.FRAXSWAP_ROUTER_MULTIHOP; - } else if (block.chainid == FraxtalTestnetL2.CHAIN_ID) { - FRAXSWAP_FACTORY = FraxtalTestnetL2.FRAXSWAP_FACTORY; - FRAXSWAP_ROUTER = FraxtalTestnetL2.FRAXSWAP_ROUTER; - FRAXSWAP_ROUTER_MULTIHOP = FraxtalTestnetL2.FRAXSWAP_ROUTER_MULTIHOP; - } - } -} diff --git a/src/test/Counter/TestIncrement.t.sol b/src/test/Counter/TestIncrement.t.sol deleted file mode 100644 index 18ff5f0..0000000 --- a/src/test/Counter/TestIncrement.t.sol +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import "../BaseTest.t.sol"; - -contract TestIncrement is BaseTest { - function defaultSetup() public returns (uint256 _snapshotId) { - counter = new Counter(); - counter.setNumber(0); - _snapshotId = vm.snapshot(); - } - - function anotherSetup() public returns (uint256 _snapshotId) { - counter = new Counter(); - counter.setNumber(50); - _snapshotId = vm.snapshot(); - } - - function setUp() public { - uint256 _original = vm.snapshot(); - snapShotIds.push(defaultSetup()); - vm.revertTo(_original); - snapShotIds.push(anotherSetup()); - } - - function testIncrement() public useMultipleSetupFunctions { - counter.increment(); - assertEq(counter.number(), 51); - } -} diff --git a/src/test/Counter/TestSetNumber.t.sol b/src/test/Counter/TestSetNumber.t.sol deleted file mode 100644 index e718678..0000000 --- a/src/test/Counter/TestSetNumber.t.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import "../BaseTest.t.sol"; - -contract TestIncrement is BaseTest { - function setUp() public { - setupFunctions.push(defaultSetup); - setupFunctions.push(anotherSetup); - addSetupFunctions(setupFunctions); - } - - function defaultSetup() public { - counter = new Counter(); - counter.setNumber(0); - } - - function anotherSetup() public { - counter = new Counter(); - counter.setNumber(50); - } - - function testSetNumber(uint256 x) public useMultipleSetupFunctions { - counter.setNumber(x); - assertEq(counter.number(), x); - } -}