From 9d3bacead430238736a270e15f3056d31b3df90c Mon Sep 17 00:00:00 2001 From: Carter Carlson Date: Tue, 9 Sep 2025 04:13:54 -0700 Subject: [PATCH 01/12] chore: rm Counter --- src/contracts/Counter.sol | 20 ----------- src/test/BaseTest.t.sol | 71 --------------------------------------- 2 files changed, 91 deletions(-) delete mode 100644 src/contracts/Counter.sol delete mode 100644 src/test/BaseTest.t.sol 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/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; - } - } -} From a3131234ada7fe3905e60e36d43808e311583db7 Mon Sep 17 00:00:00 2001 From: Carter Carlson Date: Tue, 9 Sep 2025 23:55:24 -0700 Subject: [PATCH 02/12] build: oz 5.3 --- package.json | 1 + pnpm-lock.yaml | 9 + src/contracts/ethereum/frxUSD/FrxUSD.sol | 133 +++++++++ src/contracts/ethereum/frxUSD/FrxUSD2.sol | 256 ++++++++++++++++++ src/contracts/ethereum/interfaces/IFrxUSD.sol | 47 ++++ .../ethereum/interfaces/IFrxUSD2.sol | 27 ++ src/script/.gitkeep | 0 src/script/DeployCounter.s.sol | 20 -- 8 files changed, 473 insertions(+), 20 deletions(-) create mode 100644 src/contracts/ethereum/frxUSD/FrxUSD.sol create mode 100644 src/contracts/ethereum/frxUSD/FrxUSD2.sol create mode 100644 src/contracts/ethereum/interfaces/IFrxUSD.sol create mode 100644 src/contracts/ethereum/interfaces/IFrxUSD2.sol create mode 100644 src/script/.gitkeep delete mode 100644 src/script/DeployCounter.s.sol diff --git a/package.json b/package.json index e057a27..6045a0c 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "typescript": "^5.8.3" }, "dependencies": { + "@openzeppelin/contracts-5.3.0": "npm:@openzeppelin/contracts@5.3.0", "ds-test": "github:dapphub/ds-test", "forge-std": "github:foundry-rs/forge-std#60acb7aaadcce2d68e52986a0a66fe79f07d138f", "frax-standard-solidity": "github:FraxFinance/frax-standard-solidity", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 398b933..61fd220 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,9 @@ settings: importers: .: dependencies: + "@openzeppelin/contracts-5.3.0": + specifier: npm:@openzeppelin/contracts@5.3.0 + version: "@openzeppelin/contracts@5.3.0" ds-test: specifier: github:dapphub/ds-test version: https://codeload.github.com/dapphub/ds-test/tar.gz/e282159d5170298eb2455a6c05280ab5a73a4ef0 @@ -98,6 +101,10 @@ packages: resolution: { integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== } + "@openzeppelin/contracts@5.3.0": + resolution: + { integrity: sha512-zj/KGoW7zxWUE8qOI++rUM18v+VeLTTzKs/DJFkSzHpQFPD/jKKF0TrMxBfGLl3kpdELCNccvB3zmofSzm4nlA== } + "@openzeppelin/contracts@5.4.0": resolution: { integrity: sha512-eCYgWnLg6WO+X52I16TZt8uEjbtdkgLC0SUX/xnAksjjrQI4Xfn4iBRoI5j55dmlOhDv1Y7BoR3cU7e3WWhC6A== } @@ -1187,6 +1194,8 @@ snapshots: "@jridgewell/resolve-uri": 3.1.2 "@jridgewell/sourcemap-codec": 1.5.4 + "@openzeppelin/contracts@5.3.0": {} + "@openzeppelin/contracts@5.4.0": {} "@prettier/sync@0.3.0(prettier@3.6.2)": diff --git a/src/contracts/ethereum/frxUSD/FrxUSD.sol b/src/contracts/ethereum/frxUSD/FrxUSD.sol new file mode 100644 index 0000000..03b9d2f --- /dev/null +++ b/src/contracts/ethereum/frxUSD/FrxUSD.sol @@ -0,0 +1,133 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.0; + +import { ERC20Permit, ERC20 } from "@openzeppelin/contracts-5.3.0/token/ERC20/extensions/ERC20Permit.sol"; +import { ERC20Burnable } from "@openzeppelin/contracts-5.3.0/token/ERC20/extensions/ERC20Burnable.sol"; +import { Ownable2Step } from "@openzeppelin/contracts-5.3.0/access/Ownable2Step.sol"; +import { Ownable } from "@openzeppelin/contracts-5.3.0/access/Ownable.sol"; +import { StorageSlot } from "@openzeppelin/contracts-5.3.0/utils/StorageSlot.sol"; + +/// @title FrxUSD +/** + * @notice Combines Openzeppelin's ERC20Permit, ERC20Burnable and Ownable2Step. + * Also includes a list of authorized minters + */ +/// @dev FrxUSD adheres to EIP-712/EIP-2612 and can use permits +contract FrxUSD is ERC20Permit, ERC20Burnable, Ownable2Step { + /// @notice Array of the non-bridge minters + address[] public minters_array; + + /// @notice Mapping of the minters + /// @dev Mapping is used for faster verification + mapping(address => bool) public minters; + + /* ========== CONSTRUCTOR ========== */ + /// @param _ownerAddress The initial owner + /// @param _name ERC20 name + /// @param _symbol ERC20 symbol + constructor( + address _ownerAddress, + string memory _name, + string memory _symbol + ) ERC20(_name, _symbol) ERC20Permit(_name) Ownable(_ownerAddress) {} + + /* ========== INITIALIZER ========== */ + /// @dev Used to initialize the contract when it is behind a proxy + function initialize(address _owner, string memory _name, string memory _symbol) public { + require(owner() == address(0), "Already initialized"); + _transferOwnership(_owner); + StorageSlot.getBytesSlot(bytes32(uint256(3))).value = bytes(_name); + StorageSlot.getBytesSlot(bytes32(uint256(4))).value = bytes(_symbol); + } + + /* ========== MODIFIERS ========== */ + + /// @notice A modifier that only allows a minters to call + modifier onlyMinters() { + require(minters[msg.sender] == true, "Only minters"); + _; + } + + /* ========== RESTRICTED FUNCTIONS [MINTERS] ========== */ + + /// @notice Used by 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 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); + } + + /* ========== RESTRICTED FUNCTIONS [OWNER] ========== */ + /// @notice Adds a minter + /// @param minter_address Address of minter to add + function addMinter(address minter_address) public onlyOwner { + 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 onlyOwner { + require(minter_address != address(0), "Zero address detected"); + require(minters[minter_address] == true, "Address nonexistant"); + + // 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); + } + + /* ========== 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 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/ethereum/frxUSD/FrxUSD2.sol b/src/contracts/ethereum/frxUSD/FrxUSD2.sol new file mode 100644 index 0000000..9bef61c --- /dev/null +++ b/src/contracts/ethereum/frxUSD/FrxUSD2.sol @@ -0,0 +1,256 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.0; + +import { ERC20Permit, ERC20, EIP712, Nonces } from "@openzeppelin/contracts-5.3.0/token/ERC20/extensions/ERC20Permit.sol"; +import { ERC20Burnable } from "@openzeppelin/contracts-5.3.0/token/ERC20/extensions/ERC20Burnable.sol"; +import { Ownable2Step } from "@openzeppelin/contracts-5.3.0/access/Ownable2Step.sol"; +import { Ownable } from "@openzeppelin/contracts-5.3.0/access/Ownable.sol"; +import { StorageSlot } from "@openzeppelin/contracts-5.3.0/utils/StorageSlot.sol"; + +/// @title FrxUSD +/** + * @notice Combines Openzeppelin's ERC20Permit, ERC20Burnable and Ownable2Step. + * Also includes a list of authorized minters + */ +/// @dev FrxUSD adheres to EIP-712/EIP-2612 and can use permits +contract FrxUSD2 is ERC20Permit, ERC20Burnable, Ownable2Step { + /// @notice Array of the non-bridge minters + address[] public minters_array; + + /// @notice Mapping of the minters + /// @dev Mapping is used for faster verification + mapping(address => bool) public minters; + + /// @notice Mapping indicating which addresses are frozen + mapping(address => bool) public isFrozen; + + /// @notice Whether or not the contract is paused + bool public isPaused; + + /* ========== CONSTRUCTOR ========== */ + /// @param _ownerAddress The initial owner + /// @param _name ERC20 name + /// @param _symbol ERC20 symbol + constructor( + address _ownerAddress, + string memory _name, + string memory _symbol + ) ERC20(_name, _symbol) ERC20Permit(_name) Ownable(_ownerAddress) {} + + /* ========== INITIALIZER ========== */ + /// @dev Used to initialize the contract when it is behind a proxy + function initialize(address _owner, string memory _name, string memory _symbol) public { + require(owner() == address(0), "Already initialized"); + if (_owner == address(0)) revert OwnerCannotInitToZeroAddress(); + _transferOwnership(_owner); + StorageSlot.getBytesSlot(bytes32(uint256(3))).value = bytes(_name); + StorageSlot.getBytesSlot(bytes32(uint256(4))).value = bytes(_symbol); + } + + /* ========== MODIFIERS ========== */ + + /// @notice A modifier that only allows a minters to call + modifier onlyMinters() { + require(minters[msg.sender] == true, "Only minters"); + _; + } + + /* ========== RESTRICTED FUNCTIONS [MINTERS] ========== */ + + /// @notice Used by 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 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); + } + + /* ========== RESTRICTED FUNCTIONS [OWNER] ========== */ + /// @notice Adds a minter + /// @param minter_address Address of minter to add + function addMinter(address minter_address) public onlyOwner { + 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 onlyOwner { + require(minter_address != address(0), "Zero address detected"); + require(minters[minter_address] == true, "Address nonexistant"); + + // 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); + } + + /// @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 burn(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 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 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 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(); + error OwnerCannotInitToZeroAddress(); +} diff --git a/src/contracts/ethereum/interfaces/IFrxUSD.sol b/src/contracts/ethereum/interfaces/IFrxUSD.sol new file mode 100644 index 0000000..ea17ce5 --- /dev/null +++ b/src/contracts/ethereum/interfaces/IFrxUSD.sol @@ -0,0 +1,47 @@ +pragma solidity ^0.8.0; + +import { IERC20 } from "@openzeppelin/contracts-5.3.0/token/ERC20/ERC20.sol"; +import { IERC20Permit } from "@openzeppelin/contracts-5.3.0/token/ERC20/extensions/IERC20Permit.sol"; + +/// @title FrxUSD interface +interface IFrxUSD is IERC20, IERC20Permit { + function minters_array(uint256) external view returns (address); + function minters(address) external view returns (bool); + function initialize(address _owner, string memory _name, string memory _symbol) external; + 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; + + /* ========== 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 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/ethereum/interfaces/IFrxUSD2.sol b/src/contracts/ethereum/interfaces/IFrxUSD2.sol new file mode 100644 index 0000000..93464fe --- /dev/null +++ b/src/contracts/ethereum/interfaces/IFrxUSD2.sol @@ -0,0 +1,27 @@ +pragma solidity ^0.8.0; + +import { IFrxUSD } from "src/contracts/ethereum/interfaces/IFrxUSD.sol"; + +/// @title FrxUSD2 interface +interface IFrxUSD2 is IFrxUSD { + function isFrozen(address account) external view returns (bool); + function isPaused() external view returns (bool); + 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 burn(address _owner, uint256 _amount) external; + function pause() external; + function unpause() external; + + event Paused(); + event Unpaused(); + event AccountFrozen(address account); + event AccountThawed(address account); + + error OwnerCannotInitToZeroAddress(); + error ArrayMisMatch(); + error IsPaused(); + error IsFrozen(); +} diff --git a/src/script/.gitkeep b/src/script/.gitkeep new file mode 100644 index 0000000..e69de29 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); - } -} From 601e55269811c87bc2213536bf39a67abd0ec90f Mon Sep 17 00:00:00 2001 From: Carter Carlson Date: Thu, 11 Sep 2025 00:23:45 -0700 Subject: [PATCH 03/12] build: prb, solmate --- package.json | 5 +++-- pnpm-lock.yaml | 22 +++++++++++++++++++--- remappings.txt | 6 ++++-- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 6045a0c..9288bd5 100644 --- a/package.json +++ b/package.json @@ -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", @@ -32,9 +31,11 @@ }, "dependencies": { "@openzeppelin/contracts-5.3.0": "npm:@openzeppelin/contracts@5.3.0", + "@prb/math": "^4.1.0", "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", + "solmate": "github:transmissions11/solmate#fadb2e2778adbf01c80275bfb99e5c14969d964b" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 61fd220..53e4208 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,6 +10,9 @@ importers: "@openzeppelin/contracts-5.3.0": specifier: npm:@openzeppelin/contracts@5.3.0 version: "@openzeppelin/contracts@5.3.0" + "@prb/math": + specifier: ^4.1.0 + version: 4.1.0 ds-test: specifier: github:dapphub/ds-test version: https://codeload.github.com/dapphub/ds-test/tar.gz/e282159d5170298eb2455a6c05280ab5a73a4ef0 @@ -22,10 +25,10 @@ importers: solidity-bytes-utils: specifier: github:GNSPS/solidity-bytes-utils version: https://codeload.github.com/GNSPS/solidity-bytes-utils/tar.gz/fc502455bb2a7e26a743378df042612dd50d1eb9 + solmate: + specifier: github:transmissions11/solmate#fadb2e2778adbf01c80275bfb99e5c14969d964b + version: https://codeload.github.com/transmissions11/solmate/tar.gz/fadb2e2778adbf01c80275bfb99e5c14969d964b devDependencies: - "@openzeppelin/contracts": - specifier: ^5.4.0 - version: 5.4.0 husky: specifier: ^8.0.3 version: 8.0.3 @@ -109,6 +112,10 @@ packages: resolution: { integrity: sha512-eCYgWnLg6WO+X52I16TZt8uEjbtdkgLC0SUX/xnAksjjrQI4Xfn4iBRoI5j55dmlOhDv1Y7BoR3cU7e3WWhC6A== } + "@prb/math@4.1.0": + resolution: + { integrity: sha512-ef5Xrlh3BeX4xT5/Wi810dpEPq2bYPndRxgFIaKSU1F/Op/s8af03kyom+mfU7gEpvfIZ46xu8W0duiHplbBMg== } + "@prettier/sync@0.3.0": resolution: { integrity: sha512-3dcmCyAxIcxy036h1I7MQU/uEEBq8oLwf1CE3xeze+MPlgkdlb/+w6rGR/1dhp6Hqi17fRS6nvwnOzkESxEkOw== } @@ -992,6 +999,11 @@ packages: } version: 0.8.4 + solmate@https://codeload.github.com/transmissions11/solmate/tar.gz/fadb2e2778adbf01c80275bfb99e5c14969d964b: + resolution: + { tarball: https://codeload.github.com/transmissions11/solmate/tar.gz/fadb2e2778adbf01c80275bfb99e5c14969d964b } + version: 6.2.0 + string-argv@0.3.2: resolution: { integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q== } @@ -1198,6 +1210,8 @@ snapshots: "@openzeppelin/contracts@5.4.0": {} + "@prb/math@4.1.0": {} + "@prettier/sync@0.3.0(prettier@3.6.2)": dependencies: prettier: 3.6.2 @@ -1894,6 +1908,8 @@ snapshots: solidity-bytes-utils@https://codeload.github.com/GNSPS/solidity-bytes-utils/tar.gz/fc502455bb2a7e26a743378df042612dd50d1eb9: {} + solmate@https://codeload.github.com/transmissions11/solmate/tar.gz/fadb2e2778adbf01c80275bfb99e5c14969d964b: {} + string-argv@0.3.2: {} string-width@4.2.3: diff --git a/remappings.txt b/remappings.txt index 92f1540..30e5d7f 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,4 +1,6 @@ frax-std/=node_modules/frax-standard-solidity/src/ -@prb/test/=node_modules/@prb/test/ +@prb/math/=node_modules/@prb/math/ 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/ +solmate=node_modules/solmate/src/ \ No newline at end of file From 9ea469ac02e8985ef8743fe41eff12ea9bc9dafe Mon Sep 17 00:00:00 2001 From: Carter Carlson Date: Thu, 11 Sep 2025 00:24:15 -0700 Subject: [PATCH 04/12] chore: mv frxUSD interface to frxUSD dir --- src/contracts/ethereum/frxUSD/IFrxUSD.sol | 23 +++++++++ .../{interfaces => frxUSD}/IFrxUSD2.sol | 2 +- src/contracts/ethereum/interfaces/IFrxUSD.sol | 47 ------------------- 3 files changed, 24 insertions(+), 48 deletions(-) create mode 100644 src/contracts/ethereum/frxUSD/IFrxUSD.sol rename src/contracts/ethereum/{interfaces => frxUSD}/IFrxUSD2.sol (92%) delete mode 100644 src/contracts/ethereum/interfaces/IFrxUSD.sol diff --git a/src/contracts/ethereum/frxUSD/IFrxUSD.sol b/src/contracts/ethereum/frxUSD/IFrxUSD.sol new file mode 100644 index 0000000..8f52e0c --- /dev/null +++ b/src/contracts/ethereum/frxUSD/IFrxUSD.sol @@ -0,0 +1,23 @@ +pragma solidity ^0.8.0; + +import { IERC20 } from "@openzeppelin/contracts-5.3.0/token/ERC20/ERC20.sol"; +import { IERC20Permit } from "@openzeppelin/contracts-5.3.0/token/ERC20/extensions/IERC20Permit.sol"; +import { IERC20Burnable } from "src/contracts/interfaces/IERC20Burnable.sol"; +import { IMinter } from "src/contracts/interfaces/IMinter.sol"; + +/// @title FrxUSD interface +interface IFrxUSD is IERC20, IERC20Permit, IERC20Burnable, IMinter { + function initialize(address _owner, string memory _name, string memory _symbol) external; + + /* ========== 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); +} diff --git a/src/contracts/ethereum/interfaces/IFrxUSD2.sol b/src/contracts/ethereum/frxUSD/IFrxUSD2.sol similarity index 92% rename from src/contracts/ethereum/interfaces/IFrxUSD2.sol rename to src/contracts/ethereum/frxUSD/IFrxUSD2.sol index 93464fe..cb8cf9c 100644 --- a/src/contracts/ethereum/interfaces/IFrxUSD2.sol +++ b/src/contracts/ethereum/frxUSD/IFrxUSD2.sol @@ -1,6 +1,6 @@ pragma solidity ^0.8.0; -import { IFrxUSD } from "src/contracts/ethereum/interfaces/IFrxUSD.sol"; +import { IFrxUSD } from "src/contracts/ethereum/frxUSD/IFrxUSD.sol"; /// @title FrxUSD2 interface interface IFrxUSD2 is IFrxUSD { diff --git a/src/contracts/ethereum/interfaces/IFrxUSD.sol b/src/contracts/ethereum/interfaces/IFrxUSD.sol deleted file mode 100644 index ea17ce5..0000000 --- a/src/contracts/ethereum/interfaces/IFrxUSD.sol +++ /dev/null @@ -1,47 +0,0 @@ -pragma solidity ^0.8.0; - -import { IERC20 } from "@openzeppelin/contracts-5.3.0/token/ERC20/ERC20.sol"; -import { IERC20Permit } from "@openzeppelin/contracts-5.3.0/token/ERC20/extensions/IERC20Permit.sol"; - -/// @title FrxUSD interface -interface IFrxUSD is IERC20, IERC20Permit { - function minters_array(uint256) external view returns (address); - function minters(address) external view returns (bool); - function initialize(address _owner, string memory _name, string memory _symbol) external; - 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; - - /* ========== 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 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); -} From a721a69b40f1b7dfb1b1c6ed308a3f859692e42e Mon Sep 17 00:00:00 2001 From: Carter Carlson Date: Thu, 11 Sep 2025 00:25:04 -0700 Subject: [PATCH 05/12] feat: sfrxUSD(1-2) --- src/contracts/ethereum/sfrxUSD/ISfrxUSD.sol | 19 + src/contracts/ethereum/sfrxUSD/ISfrxUSD2.sol | 29 ++ src/contracts/ethereum/sfrxUSD/SfrxUSD.sol | 143 +++++++ src/contracts/ethereum/sfrxUSD/SfrxUSD2.sol | 264 ++++++++++++ .../inherited/ILinearRewardsErc4626.sol | 72 ++++ .../inherited/ILinearRewardsErc4626_2.sol | 74 ++++ .../inherited/LinearRewardsErc4626.sol | 277 ++++++++++++ .../inherited/LinearRewardsErc4626_2.sol | 394 ++++++++++++++++++ 8 files changed, 1272 insertions(+) create mode 100644 src/contracts/ethereum/sfrxUSD/ISfrxUSD.sol create mode 100644 src/contracts/ethereum/sfrxUSD/ISfrxUSD2.sol create mode 100644 src/contracts/ethereum/sfrxUSD/SfrxUSD.sol create mode 100644 src/contracts/ethereum/sfrxUSD/SfrxUSD2.sol create mode 100644 src/contracts/ethereum/sfrxUSD/inherited/ILinearRewardsErc4626.sol create mode 100644 src/contracts/ethereum/sfrxUSD/inherited/ILinearRewardsErc4626_2.sol create mode 100644 src/contracts/ethereum/sfrxUSD/inherited/LinearRewardsErc4626.sol create mode 100644 src/contracts/ethereum/sfrxUSD/inherited/LinearRewardsErc4626_2.sol diff --git a/src/contracts/ethereum/sfrxUSD/ISfrxUSD.sol b/src/contracts/ethereum/sfrxUSD/ISfrxUSD.sol new file mode 100644 index 0000000..1d7a14b --- /dev/null +++ b/src/contracts/ethereum/sfrxUSD/ISfrxUSD.sol @@ -0,0 +1,19 @@ +pragma solidity ^0.8.0; + +import { ILinearRewardsErc4626 } from "src/contracts/ethereum/sfrxUSD/inherited/ILinearRewardsErc4626.sol"; +import { ITimelock2Step } from "frax-std/access-control/v2/interfaces/ITimelock2Step.sol"; + +interface ISfrxUSD is ILinearRewardsErc4626, ITimelock2Step { + // state variables + function maxDistributionPerSecondPerAsset() external view returns (uint256); + function version() external view returns (string memory); + function setMaxDistributionPerSecondPerAsset(uint256 _maxDistributionPerSecondPerAsset) external; + function calculateRewardsToDistribute( + RewardsCycleData memory _rewardsCycleData, + uint256 _deltatime + ) external view returns (uint256 _rewardToDistribute); + + event SetMaxDistributionPerSecondPerAsset(uint256 oldMax, uint256 newMax); + + error AlreadyInitialized(); +} diff --git a/src/contracts/ethereum/sfrxUSD/ISfrxUSD2.sol b/src/contracts/ethereum/sfrxUSD/ISfrxUSD2.sol new file mode 100644 index 0000000..7ddb102 --- /dev/null +++ b/src/contracts/ethereum/sfrxUSD/ISfrxUSD2.sol @@ -0,0 +1,29 @@ +pragma solidity ^0.8.0; + +import { ILinearRewardsErc4626_2 } from "src/contracts/ethereum/sfrxUSD/inherited/ILinearRewardsErc4626_2.sol"; +import { ITimelock2Step } from "frax-std/access-control/v2/interfaces/ITimelock2Step.sol"; +import { IMinter } from "src/contracts/interfaces/IMinter.sol"; + +interface ISfrxUSD2 is ILinearRewardsErc4626_2, ITimelock2Step, IMinter { + // views + function version() external view returns (string memory); + + // state changers + function burn(uint256 _amount) external; + function setAllPricingParams( + uint256 _newPricePerShareStored, + uint256 _newPricePerShareIncPerSecond, + uint256 _newLastSync + ) external; + function setPricePerShareIncPerSecond(uint256 _newPricePerShareIncPerSecond) external; + function setPricePerShareStored(uint256 _newPricePerShareStored) external; + + // errors + error MustNotBeInTheFuture(); + + // events + event Burn(address indexed from, uint256 amount); + event SetLastSync(uint256 newLastSync); + event SetPricePerShareIncPerSecond(uint256 newPricePerShareIncPerSecond); + event SetPricePerShareStored(uint256 newPricePerShareStored); +} diff --git a/src/contracts/ethereum/sfrxUSD/SfrxUSD.sol b/src/contracts/ethereum/sfrxUSD/SfrxUSD.sol new file mode 100644 index 0000000..b2fda92 --- /dev/null +++ b/src/contracts/ethereum/sfrxUSD/SfrxUSD.sol @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.21; + +// ==================================================================== +// | ______ _______ | +// | / _____________ __ __ / ____(_____ ____ _____ ________ | +// | / /_ / ___/ __ `| |/_/ / /_ / / __ \/ __ `/ __ \/ ___/ _ \ | +// | / __/ / / / /_/ _> < / __/ / / / / / /_/ / / / / /__/ __/ | +// | /_/ /_/ \__,_/_/|_| /_/ /_/_/ /_/\__,_/_/ /_/\___/\___/ | +// | | +// ==================================================================== +// =========================== StakedFrxUSD =========================== +// ==================================================================== +// Frax Finance: https://github.com/FraxFinance + +import { Timelock2Step } from "frax-std/access-control/v2/Timelock2Step.sol"; +import { IERC20 } from "@openzeppelin/contracts-5.3.0/token/ERC20/ERC20.sol"; +import { SafeCastLib } from "solmate/utils/SafeCastLib.sol"; +import { LinearRewardsErc4626, ERC20 } from "src/contracts/ethereum/sfrxUSD/inherited/LinearRewardsErc4626.sol"; + +/// @title Staked frxUSD +/// @notice A ERC4626 Vault implementation with linear rewards, rewards can be capped +contract SfrxUSD is LinearRewardsErc4626, Timelock2Step { + using SafeCastLib for *; + + /// @notice The maximum amount of rewards that can be distributed per second per 1e18 asset + uint256 public maxDistributionPerSecondPerAsset; + + uint256 private initializeStage = 2; + + string public constant version = "1.0.0"; + + /// @param _underlying The erc20 asset deposited + /// @param _name The name of the vault + /// @param _symbol The symbol of the vault + /// @param _rewardsCycleLength The length of the rewards cycle in seconds + /// @param _maxDistributionPerSecondPerAsset The maximum amount of rewards that can be distributed per second per 1e18 asset + /// @param _timelockAddress The address of the timelock/owner contract + constructor( + IERC20 _underlying, + string memory _name, + string memory _symbol, + uint32 _rewardsCycleLength, + uint256 _maxDistributionPerSecondPerAsset, + address _timelockAddress + ) + LinearRewardsErc4626(ERC20(address(_underlying)), _name, _symbol, _rewardsCycleLength) + Timelock2Step(_timelockAddress) + { + maxDistributionPerSecondPerAsset = _maxDistributionPerSecondPerAsset; + } + + /// @notice The ```SetMaxDistributionPerSecondPerAsset``` event is emitted when the maxDistributionPerSecondPerAsset is set + /// @param oldMax The old maxDistributionPerSecondPerAsset value + /// @param newMax The new maxDistributionPerSecondPerAsset value + event SetMaxDistributionPerSecondPerAsset(uint256 oldMax, uint256 newMax); + + error AlreadyInitialized(); + + function initialize( + string memory _name, + string memory _symbol, + uint256 _maxDistributionPerSecondPerAsset, + address _timelockAddress + ) external { + if (initializeStage != 0) revert AlreadyInitialized(); + initializeStage++; + name = _name; + symbol = _symbol; + maxDistributionPerSecondPerAsset = _maxDistributionPerSecondPerAsset; + timelockAddress = _timelockAddress; + + // initialize rewardsCycleEnd value + // NOTE: normally distribution of rewards should be done prior to _syncRewards but in this case we know there are no users or rewards yet. + _syncRewards(); + + // initialize lastRewardsDistribution value + _distributeRewards(); + } + + /// @notice The ```initializeRewardsCycleData``` function initializes the rewards cycle data + /// @dev This function can only be called once + /// @param _pricePerShare The price per share + /// @param _maxDistributionPerSecondPerAsset The maximum amount of rewards that can be distributed per second per 1e18 asset + /// @param _cycleEnd The end of the rewards cycle + /// @param _lastSync The last sync time + /// @param _rewardCycleAmount The reward cycle amount + function initializeRewardsCycleData( + uint256 _pricePerShare, + uint256 _maxDistributionPerSecondPerAsset, + uint40 _cycleEnd, + uint40 _lastSync, + uint216 _rewardCycleAmount + ) external { + if (initializeStage != 1) revert AlreadyInitialized(); + initializeStage++; + storedTotalAssets = (_pricePerShare * totalSupply) / PRECISION; + maxDistributionPerSecondPerAsset = _maxDistributionPerSecondPerAsset; + rewardsCycleData.cycleEnd = _cycleEnd; + rewardsCycleData.lastSync = _lastSync; + rewardsCycleData.rewardCycleAmount = _rewardCycleAmount; + } + + /// @notice The ```setMaxDistributionPerSecondPerAsset``` function sets the maxDistributionPerSecondPerAsset + /// @dev This function can only be called by the timelock, caps the value to type(uint64).max + /// @param _maxDistributionPerSecondPerAsset The maximum amount of rewards that can be distributed per second per 1e18 asset + function setMaxDistributionPerSecondPerAsset(uint256 _maxDistributionPerSecondPerAsset) external { + _requireSenderIsTimelock(); + syncRewardsAndDistribution(); + + // NOTE: prevents bricking the contract via overflow + if (_maxDistributionPerSecondPerAsset > type(uint64).max) { + _maxDistributionPerSecondPerAsset = type(uint64).max; + } + + emit SetMaxDistributionPerSecondPerAsset({ + oldMax: maxDistributionPerSecondPerAsset, + newMax: _maxDistributionPerSecondPerAsset + }); + + maxDistributionPerSecondPerAsset = _maxDistributionPerSecondPerAsset; + } + + /// @notice The ```calculateRewardsToDistribute``` function calculates the amount of rewards to distribute based on the rewards cycle data and the time passed + /// @param _rewardsCycleData The rewards cycle data + /// @param _deltaTime The time passed since the last rewards distribution + /// @return _rewardToDistribute The amount of rewards to distribute + function calculateRewardsToDistribute( + RewardsCycleData memory _rewardsCycleData, + uint256 _deltaTime + ) public view override returns (uint256 _rewardToDistribute) { + _rewardToDistribute = super.calculateRewardsToDistribute({ + _rewardsCycleData: _rewardsCycleData, + _deltaTime: _deltaTime + }); + + // Cap rewards + uint256 _maxDistribution = (maxDistributionPerSecondPerAsset * _deltaTime * storedTotalAssets) / PRECISION; + if (_rewardToDistribute > _maxDistribution) { + _rewardToDistribute = _maxDistribution; + } + } +} diff --git a/src/contracts/ethereum/sfrxUSD/SfrxUSD2.sol b/src/contracts/ethereum/sfrxUSD/SfrxUSD2.sol new file mode 100644 index 0000000..a08ae93 --- /dev/null +++ b/src/contracts/ethereum/sfrxUSD/SfrxUSD2.sol @@ -0,0 +1,264 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.21; + +// ==================================================================== +// | ______ _______ | +// | / _____________ __ __ / ____(_____ ____ _____ ________ | +// | / /_ / ___/ __ `| |/_/ / /_ / / __ \/ __ `/ __ \/ ___/ _ \ | +// | / __/ / / / /_/ _> < / __/ / / / / / /_/ / / / / /__/ __/ | +// | /_/ /_/ \__,_/_/|_| /_/ /_/_/ /_/\__,_/_/ /_/\___/\___/ | +// | | +// ==================================================================== +// ========================== StakedFrxUSD2 =========================== +// ==================================================================== +// Frax Finance: https://github.com/FraxFinance +// Tested for 18-decimal underlying assets only + +import { Timelock2Step } from "frax-std/access-control/v2/Timelock2Step.sol"; +import { IERC20 } from "@openzeppelin/contracts-5.3.0/token/ERC20/ERC20.sol"; +import { IFrxUSD } from "src/contracts/ethereum/frxUSD/IFrxUSD.sol"; +import { SafeCastLib } from "solmate/utils/SafeCastLib.sol"; +import { LinearRewardsErc4626_2, ERC20 } from "src/contracts/ethereum/sfrxUSD/inherited/LinearRewardsErc4626_2.sol"; + +/// @title Staked frxUSD +/// @notice A ERC4626-like Vault implementation with linear rewards, rewards can be capped +contract SfrxUSD2 is LinearRewardsErc4626_2, Timelock2Step { + using SafeCastLib for *; + + /// @notice Used for initialization + bool public _initialized; + + /// @notice Array of minters + address[] public minters_array; + + /// @notice Mapping of the minters + /// @dev Mapping is used for faster verification + mapping(address => bool) public minters; + + function version() public pure virtual returns (string memory) { + return "2.0.0"; + } + + /// @param _underlying The erc20 asset deposited + /// @param _name The name of the vault + /// @param _symbol The symbol of the vault + /// @param _timelockAddress The address of the timelock/owner contract + constructor( + IERC20 _underlying, + string memory _name, + string memory _symbol, + address _timelockAddress + ) LinearRewardsErc4626_2(ERC20(address(_underlying)), _name, _symbol) Timelock2Step(_timelockAddress) { + _initialized = true; + } + + error AlreadyInitialized(); + + /// @param _name The name of the vault + /// @param _symbol The symbol of the vault + /// @param _timelockAddress The address of the timelock/owner contract + /// @param _ppsInfo [0] Initial PricePerShare [1] PricePerShare increase per sec + function initialize( + string memory _name, + string memory _symbol, + address _timelockAddress, + uint256[2] memory _ppsInfo + ) external { + if (_initialized) revert AlreadyInitialized(); + _initialized = true; + name = _name; + symbol = _symbol; + timelockAddress = _timelockAddress; + + // Burn all the frxUSD currently in this contract + IFrxUSD(address(asset)).burn(asset.balanceOf(address(this))); + + // Set PricePerShare info initially + pricePerShareStored = _ppsInfo[0]; + pricePerShareIncPerSecond = _ppsInfo[1]; + + // Set lastSync to now + lastSync = block.timestamp; + } + + /* ========== MODIFIERS ========== */ + + /// @notice A modifier that only allows a minters to call + modifier onlyMinters() { + if (!minters[msg.sender]) revert OnlyMinters(); + _; + } + + /* ========== UNRESTRICTED FUNCTIONS========== */ + + /// @notice Burn tokens. You do NOT receive any underlying assets when doing so + /// @param _amount Amount of tokens to burn + function burn(uint256 _amount) public { + // Do the burn + super._burn(msg.sender, _amount); + + emit Burn(msg.sender, _amount); + } + + /* ========== RESTRICTED FUNCTIONS [MINTERS] ========== */ + + /// @notice Used by 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._burn(b_address, b_amount); + emit TokenMinterBurned(b_address, msg.sender, b_amount); + } + + /// @notice Used by 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); + } + + /* ========== RESTRICTED FUNCTIONS [OWNER] ========== */ + /// @notice Adds a minter + /// @param minter_address Address of minter to add + function addMinter(address minter_address) public { + _requireSenderIsTimelock(); + 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 { + _requireSenderIsTimelock(); + require(minter_address != address(0), "Zero address detected"); + require(minters[minter_address] == true, "Address nonexistant"); + + // 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); + } + + /// @notice Set pricePerShareStored, pricePerShareIncPerSecond, and lastSync in one call + /// @param _newPricePerShareStored New stored price per share, in E18 asset tokens + /// @param _newPricePerShareIncPerSecond New stored price per share increase per second, in E18 asset tokens + /// @param _newLastSync New lastSync + /// @dev p(t) = p0*e^(r(t-t0)) + function setAllPricingParams( + uint256 _newPricePerShareStored, + uint256 _newPricePerShareIncPerSecond, + uint256 _newLastSync + ) external { + _requireSenderIsTimelock(); + + // Make sure lastSync is not in the future + if (_newLastSync > block.timestamp) revert MustNotBeInTheFuture(); + + // Set the 3 parameters + pricePerShareStored = _newPricePerShareStored; + pricePerShareIncPerSecond = _newPricePerShareIncPerSecond; + lastSync = _newLastSync; + + emit SetPricePerShareStored(_newPricePerShareStored); + emit SetPricePerShareIncPerSecond(_newPricePerShareIncPerSecond); + emit SetLastSync(_newLastSync); + } + + /// @notice Set pricePerShare increase rate, per second (pricePerShareIncPerSecond). Also sets lastSync to now and pricePerShareStored to the current pricePerShare + /// @param _newPricePerShareIncPerSecond New stored price per share increase per second, in E18 asset tokens + function setPricePerShareIncPerSecond(uint256 _newPricePerShareIncPerSecond) external { + _requireSenderIsTimelock(); + + // Sync first + sync(); + + // Set pricePerShareIncPerSecond + pricePerShareIncPerSecond = _newPricePerShareIncPerSecond; + + emit SetPricePerShareIncPerSecond(_newPricePerShareIncPerSecond); + } + + /// @notice Set pricePerShareStored + /// @param _newPricePerShareStored New stored price per share, in E18 asset tokens + function setPricePerShareStored(uint256 _newPricePerShareStored) external { + _requireSenderIsTimelock(); + + // Set lastSync to now + lastSync = block.timestamp; + + // Set pricePerShareStored + pricePerShareStored = _newPricePerShareStored; + + emit SetPricePerShareStored(_newPricePerShareStored); + } + + //============================================================================== + // Errors + //============================================================================== + + /// @notice When lastSync is trying to be set to a future date + error MustNotBeInTheFuture(); + + /// @notice When a non-minter tries to call a restricted function + error OnlyMinters(); + + //============================================================================== + // Events + //============================================================================== + + /// @notice Emitted when a burn happens + /// @param from The address whose tokens were burned + /// @param amount Amount of tokens burned + event Burn(address indexed from, uint256 amount); + + /// @notice Emitted when a mint happens + /// @param to Recipient of the newly-minted tokens + /// @param amount Amount of tokens minted + event Mint(address indexed to, 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 When setLastSync is called + /// @param newLastSync New lastSync + event SetLastSync(uint256 newLastSync); + + /// @notice When setPricePerShareIncPerSecond is called + /// @param newPricePerShareIncPerSecond New stored price per share increase per second, in E18 asset tokens + event SetPricePerShareIncPerSecond(uint256 newPricePerShareIncPerSecond); + + /// @notice When setPricePerShareStored is called + /// @param newPricePerShareStored New stored price per share, in E18 asset tokens + event SetPricePerShareStored(uint256 newPricePerShareStored); + + /// @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/ethereum/sfrxUSD/inherited/ILinearRewardsErc4626.sol b/src/contracts/ethereum/sfrxUSD/inherited/ILinearRewardsErc4626.sol new file mode 100644 index 0000000..1b0337a --- /dev/null +++ b/src/contracts/ethereum/sfrxUSD/inherited/ILinearRewardsErc4626.sol @@ -0,0 +1,72 @@ +pragma solidity ^0.8.0; + +import { ERC20 } from "solmate/tokens/ERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts-5.3.0/token/ERC20/ERC20.sol"; +import { IERC20Metadata } from "@openzeppelin/contracts-5.3.0/token/ERC20/extensions/IERC20Metadata.sol"; + +interface ILinearRewardsErc4626 is IERC20, IERC20Metadata { + // structs + struct RewardsCycleData { + uint40 cycleEnd; + uint40 lastSync; + uint216 rewardCycleAmount; + } + + // state variables + function PRECISION() external view returns (uint256); + function REWARDS_CYCLE_LENGTH() external view returns (uint256); + function rewardsCycleData() external view returns (RewardsCycleData memory); + function lastRewardsDistribution() external view returns (uint256); + function storedTotalAssets() external view returns (uint256); + function UNDERLYING_PRECISION() external view returns (uint256); + function asset() external view returns (ERC20); + + // views + function pricePerShare() external view returns (uint256); + function calculateRewardsToDistribute( + RewardsCycleData memory _rewardsCycleData, + uint256 _deltaTime + ) external view returns (uint256 _rewardToDistribute); + function previewDistributeRewards() external view returns (uint256); + function previewSyncRewards() external view returns (RewardsCycleData memory); + function totalAssets() external view returns (uint256); + function convertToShares(uint256 assets) external view returns (uint256 shares); + function convertToAssets(uint256 shares) external view returns (uint256 assets); + function previewDeposit(uint256 assets) external view returns (uint256 shares); + function previewMint(uint256 shares) external view returns (uint256 assets); + function previewWithdraw(uint256 assets) external view returns (uint256 shares); + function previewRedeem(uint256 shares) external view returns (uint256 assets); + function maxDeposit(address) external view returns (uint256); + function maxMint(address) external view returns (uint256); + function maxWithdraw(address owner) external view returns (uint256); + function maxRedeem(address owner) external view returns (uint256); + + // state changers + function syncRewardsAndDistribution() external; + function deposit(uint256 _assets, address _receiver) external returns (uint256 _shares); + function mint(uint256 _shares, address _receiver) external returns (uint256 _assets); + function withdraw(uint256 _assets, address _receiver, address _owner) external returns (uint256 _shares); + function redeem(uint256 _shares, address _receiver, address _owner) external returns (uint256 _assets); + function depositWithSignature( + uint256 _assets, + address _receiver, + uint256 _deadline, + bool _approveMax, + uint8 _v, + bytes32 _r, + bytes32 _s + ) external returns (uint256 _shares); + + // events + event SyncRewards(uint40 cycleEnd, uint40 lastSync, uint216 rewardCycleAmount); + event DistributeRewards(uint256 rewardsToDistribute); + event Deposit(address indexed caller, address indexed owner, uint256 assets, uint256 shares); + + event Withdraw( + address indexed caller, + address indexed receiver, + address indexed owner, + uint256 assets, + uint256 shares + ); +} diff --git a/src/contracts/ethereum/sfrxUSD/inherited/ILinearRewardsErc4626_2.sol b/src/contracts/ethereum/sfrxUSD/inherited/ILinearRewardsErc4626_2.sol new file mode 100644 index 0000000..242c2c3 --- /dev/null +++ b/src/contracts/ethereum/sfrxUSD/inherited/ILinearRewardsErc4626_2.sol @@ -0,0 +1,74 @@ +pragma solidity ^0.8.0; + +import { ERC20 } from "solmate/tokens/ERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts-5.3.0/token/ERC20/ERC20.sol"; +import { IERC20Metadata } from "@openzeppelin/contracts-5.3.0/token/ERC20/extensions/IERC20Metadata.sol"; +import { UD60x18 } from "@prb/math/src/ud60x18/ValueType.sol"; + +interface ILinearRewardsErc4626_2 is IERC20, IERC20Metadata { + // structs + struct RewardsCycleData { + uint40 cycleEnd; + uint40 lastSync; + uint216 rewardCycleAmount; + } + + // state variables + function PRECISION() external view returns (uint256); + function ONE_YEAR() external view returns (uint256); + function REWARDS_CYCLE_LENGTH() external view returns (uint256); + function ONE_YEAR_UD60X18() external view returns (UD60x18); + function pricePerShareStored() external view returns (uint256); + function pricePerShareIncPerSecond() external view returns (uint256); + function lastSync() external view returns (uint256); + function asset() external view returns (ERC20); + + // views + function calcPPSIPSForGivenAPY(uint256 _apyE18) external view returns (uint256 _newPPSIPS); + function previewTotalAssets() external view returns (uint256 _newTotalAssets); + function storedTotalAssets() external view returns (uint256 _newTotalAssets); + function previewTotalAssetsFuture(uint256 _futureTime) external view returns (uint256 _newTotalAssets); + function previewPricePerShare() external view returns (uint256 _newPricePerShare); + function previewPricePerShareFuture(uint256 _futureTime) external view returns (uint256 _newPricePerShare); + function previewPPSAndTotalAssets() external view returns (uint256 _pricePerShare, uint256 _totalAssets); + function pricePerShare() external view returns (uint256 _pricePerShare); + function totalAssets() external view returns (uint256); + function lastRewardsDistribution() external view returns (uint256); + + // deprecated views + function convertToShares(uint256 assets) external view returns (uint256 shares); + function convertToAssets(uint256 shares) external view returns (uint256 assets); + function previewDeposit(uint256 assets) external view returns (uint256 shares); + function previewMint(uint256 shares) external view returns (uint256 assets); + function previewWithdraw(uint256 assets) external view returns (uint256 shares); + function previewRedeem(uint256 shares) external view returns (uint256 assets); + function maxDeposit(address) external view returns (uint256); + function maxMint(address) external view returns (uint256); + function maxWithdraw(address owner) external view returns (uint256); + function maxRedeem(address owner) external view returns (uint256); + function maxDistributionPerSecondPerAsset() external view returns (uint256); + function rewardsCycleData() external view returns (RewardsCycleData memory); + + // state changers + function sync() external returns (uint256 _pricePerShare); + + // deprecated state changers + function deposit(uint256 _assets, address _receiver) external returns (uint256 _shares); + function mint(uint256 _shares, address _receiver) external returns (uint256 _assets); + function withdraw(uint256 _assets, address _receiver, address _owner) external returns (uint256 _shares); + function redeem(uint256 _shares, address _receiver, address _owner) external returns (uint256 _assets); + function depositWithSignature( + uint256 _assets, + address _receiver, + uint256 _deadline, + bool _approveMax, + uint8 _v, + bytes32 _r, + bytes32 _s + ) external returns (uint256 _shares); + + // errors + error UnderlyingAssetMustBe18Decimals(); + error InvalidAPY(); + error MintRedeemsDisabled(); +} diff --git a/src/contracts/ethereum/sfrxUSD/inherited/LinearRewardsErc4626.sol b/src/contracts/ethereum/sfrxUSD/inherited/LinearRewardsErc4626.sol new file mode 100644 index 0000000..673d104 --- /dev/null +++ b/src/contracts/ethereum/sfrxUSD/inherited/LinearRewardsErc4626.sol @@ -0,0 +1,277 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.21; + +// ==================================================================== +// | ______ _______ | +// | / _____________ __ __ / ____(_____ ____ _____ ________ | +// | / /_ / ___/ __ `| |/_/ / /_ / / __ \/ __ `/ __ \/ ___/ _ \ | +// | / __/ / / / /_/ _> < / __/ / / / / / /_/ / / / / /__/ __/ | +// | /_/ /_/ \__,_/_/|_| /_/ /_/_/ /_/\__,_/_/ /_/\___/\___/ | +// | | +// ==================================================================== +// ======================== LinearRewardsErc4626 ====================== +// ==================================================================== +// Frax Finance: https://github.com/FraxFinance + +import { ERC20, ERC4626 } from "solmate/mixins/ERC4626.sol"; +import { SafeCastLib } from "solmate/utils/SafeCastLib.sol"; + +/// @title LinearRewardsErc4626 +/// @notice An ERC4626 Vault implementation with linear rewards +abstract contract LinearRewardsErc4626 is ERC4626 { + using SafeCastLib for *; + + /// @notice The precision of all integer calculations + uint256 public constant PRECISION = 1e18; + + /// @notice The rewards cycle length in seconds + uint256 public immutable REWARDS_CYCLE_LENGTH; + + /// @notice Information about the current rewards cycle + struct RewardsCycleData { + uint40 cycleEnd; // Timestamp of the end of the current rewards cycle + uint40 lastSync; // Timestamp of the last time the rewards cycle was synced + uint216 rewardCycleAmount; // Amount of rewards to be distributed in the current cycle + } + + /// @notice The rewards cycle data, stored in a single word to save gas + RewardsCycleData public rewardsCycleData; + + /// @notice The timestamp of the last time rewards were distributed + uint256 public lastRewardsDistribution; + + /// @notice The total amount of assets that have been distributed and deposited + uint256 public storedTotalAssets; + + /// @notice The precision of the underlying asset + uint256 public immutable UNDERLYING_PRECISION; + + /// @param _underlying The erc20 asset deposited + /// @param _name The name of the vault + /// @param _symbol The symbol of the vault + /// @param _rewardsCycleLength The length of the rewards cycle in seconds + constructor( + ERC20 _underlying, + string memory _name, + string memory _symbol, + uint256 _rewardsCycleLength + ) ERC4626(_underlying, _name, _symbol) { + REWARDS_CYCLE_LENGTH = _rewardsCycleLength; + UNDERLYING_PRECISION = 10 ** _underlying.decimals(); + + // initialize rewardsCycleEnd value + // NOTE: normally distribution of rewards should be done prior to _syncRewards but in this case we know there are no users or rewards yet. + _syncRewards(); + + // initialize lastRewardsDistribution value + _distributeRewards(); + } + + function pricePerShare() external view returns (uint256 _pricePerShare) { + _pricePerShare = convertToAssets(UNDERLYING_PRECISION); + } + + /// @notice The ```calculateRewardsToDistribute``` function calculates the amount of rewards to distribute based on the rewards cycle data and the time elapsed + /// @param _rewardsCycleData The rewards cycle data + /// @param _deltaTime The time elapsed since the last rewards distribution + /// @return _rewardToDistribute The amount of rewards to distribute + function calculateRewardsToDistribute( + RewardsCycleData memory _rewardsCycleData, + uint256 _deltaTime + ) public view virtual returns (uint256 _rewardToDistribute) { + _rewardToDistribute = + (_rewardsCycleData.rewardCycleAmount * _deltaTime) / + (_rewardsCycleData.cycleEnd - _rewardsCycleData.lastSync); + } + + /// @notice The ```previewDistributeRewards``` function is used to preview the rewards distributed at the top of the block + /// @return _rewardToDistribute The amount of underlying to distribute + function previewDistributeRewards() public view virtual returns (uint256 _rewardToDistribute) { + // Cache state for gas savings + RewardsCycleData memory _rewardsCycleData = rewardsCycleData; + uint256 _lastRewardsDistribution = lastRewardsDistribution; + uint40 _timestamp = block.timestamp.safeCastTo40(); + + // Calculate the delta time, but only include up to the cycle end in case we are passed it + uint256 _deltaTime = _timestamp > _rewardsCycleData.cycleEnd + ? _rewardsCycleData.cycleEnd - _lastRewardsDistribution + : _timestamp - _lastRewardsDistribution; + + // Calculate the rewards to distribute + _rewardToDistribute = calculateRewardsToDistribute({ + _rewardsCycleData: _rewardsCycleData, + _deltaTime: _deltaTime + }); + } + + /// @notice The ```distributeRewards``` function distributes the rewards once per block + /// @return _rewardToDistribute The amount of underlying to distribute + function _distributeRewards() internal virtual returns (uint256 _rewardToDistribute) { + _rewardToDistribute = previewDistributeRewards(); + + // Only write to state/emit if we actually distribute rewards + if (_rewardToDistribute != 0) { + storedTotalAssets += _rewardToDistribute; + emit DistributeRewards({ rewardsToDistribute: _rewardToDistribute }); + } + + lastRewardsDistribution = block.timestamp; + } + + /// @notice The ```previewSyncRewards``` function returns the updated rewards cycle data without updating the state + /// @return _newRewardsCycleData The updated rewards cycle data + function previewSyncRewards() public view virtual returns (RewardsCycleData memory _newRewardsCycleData) { + RewardsCycleData memory _rewardsCycleData = rewardsCycleData; + + uint256 _timestamp = block.timestamp; + + // Only sync if the previous cycle has ended + if (_timestamp <= _rewardsCycleData.cycleEnd) return _rewardsCycleData; + + // Calculate rewards for next cycle + uint256 _newRewards = asset.balanceOf(address(this)) - storedTotalAssets; + + // Calculate the next cycle end, this keeps cycles at the same time regardless of when sync is called + uint40 _cycleEnd = (((_timestamp + REWARDS_CYCLE_LENGTH) / REWARDS_CYCLE_LENGTH) * REWARDS_CYCLE_LENGTH) + .safeCastTo40(); + + // This block prevents big jumps in rewards rate in case the sync happens near the end of the cycle + if (_cycleEnd - _timestamp < REWARDS_CYCLE_LENGTH / 40) { + _cycleEnd += REWARDS_CYCLE_LENGTH.safeCastTo40(); + } + + // Write return values + _rewardsCycleData.rewardCycleAmount = _newRewards.safeCastTo216(); + _rewardsCycleData.lastSync = _timestamp.safeCastTo40(); + _rewardsCycleData.cycleEnd = _cycleEnd; + + return _rewardsCycleData; + } + + /// @notice The ```_syncRewards``` function is used to update the rewards cycle data + function _syncRewards() internal virtual { + RewardsCycleData memory _rewardsCycleData = previewSyncRewards(); + + if ( + // If true, then preview shows a rewards should be processed + // Ensures that we don't write to state twice in the same block + block.timestamp.safeCastTo40() == _rewardsCycleData.lastSync && + rewardsCycleData.lastSync != _rewardsCycleData.lastSync + ) { + rewardsCycleData = _rewardsCycleData; + emit SyncRewards({ + cycleEnd: _rewardsCycleData.cycleEnd, + lastSync: _rewardsCycleData.lastSync, + rewardCycleAmount: _rewardsCycleData.rewardCycleAmount + }); + } + } + + /// @notice The ```syncRewardsAndDistribution``` function is used to update the rewards cycle data and distribute rewards + /// @dev rewards must be distributed before the cycle is synced + function syncRewardsAndDistribution() public virtual { + _distributeRewards(); + _syncRewards(); + } + + /// @notice The ```totalAssets``` function returns the total assets available in the vault + /// @dev This function simulates the rewards that will be distributed at the top of the block + /// @return _totalAssets The total assets available in the vault + function totalAssets() public view virtual override returns (uint256 _totalAssets) { + uint256 _rewardToDistribute = previewDistributeRewards(); + _totalAssets = storedTotalAssets + _rewardToDistribute; + } + + function afterDeposit(uint256 amount, uint256 shares) internal virtual override { + storedTotalAssets += amount; + } + + /// @notice The ```deposit``` function allows a user to mint shares by depositing underlying + /// @param _assets The amount of underlying to deposit + /// @param _receiver The address to send the shares to + /// @return _shares The amount of shares minted + function deposit(uint256 _assets, address _receiver) public override returns (uint256 _shares) { + syncRewardsAndDistribution(); + _shares = super.deposit({ assets: _assets, receiver: _receiver }); + } + + /// @notice The ```mint``` function allows a user to mint a given number of shares + /// @param _shares The amount of shares to mint + /// @param _receiver The address to send the shares to + /// @return _assets The amount of underlying deposited + function mint(uint256 _shares, address _receiver) public override returns (uint256 _assets) { + syncRewardsAndDistribution(); + _assets = super.mint({ shares: _shares, receiver: _receiver }); + } + + function beforeWithdraw(uint256 amount, uint256 shares) internal virtual override { + storedTotalAssets -= amount; + } + + /// @notice The ```withdraw``` function allows a user to withdraw a given amount of underlying + /// @param _assets The amount of underlying to withdraw + /// @param _receiver The address to send the underlying to + /// @param _owner The address of the owner of the shares + /// @return _shares The amount of shares burned + function withdraw(uint256 _assets, address _receiver, address _owner) public override returns (uint256 _shares) { + syncRewardsAndDistribution(); + + _shares = super.withdraw({ assets: _assets, receiver: _receiver, owner: _owner }); + } + + /// @notice The ```redeem``` function allows a user to redeem their shares for underlying + /// @param _shares The amount of shares to redeem + /// @param _receiver The address to send the underlying to + /// @param _owner The address of the owner of the shares + /// @return _assets The amount of underlying redeemed + function redeem(uint256 _shares, address _receiver, address _owner) public override returns (uint256 _assets) { + syncRewardsAndDistribution(); + + _assets = super.redeem({ shares: _shares, receiver: _receiver, owner: _owner }); + } + + /// @notice The ```depositWithSignature``` function allows a user to use signed approvals to deposit + /// @param _assets The amount of underlying to deposit + /// @param _receiver The address to send the shares to + /// @param _deadline The deadline for the signature + /// @param _approveMax Whether or not to approve the maximum amount + /// @param _v The v value of the signature + /// @param _r The r value of the signature + /// @param _s The s value of the signature + /// @return _shares The amount of shares minted + function depositWithSignature( + uint256 _assets, + address _receiver, + uint256 _deadline, + bool _approveMax, + uint8 _v, + bytes32 _r, + bytes32 _s + ) external returns (uint256 _shares) { + uint256 _amount = _approveMax ? type(uint256).max : _assets; + asset.permit({ + owner: msg.sender, + spender: address(this), + value: _amount, + deadline: _deadline, + v: _v, + r: _r, + s: _s + }); + _shares = (deposit({ _assets: _assets, _receiver: _receiver })); + } + + //============================================================================== + // Events + //============================================================================== + + /// @notice The ```SyncRewards``` event is emitted when the rewards cycle is synced + /// @param cycleEnd The timestamp of the end of the current rewards cycle + /// @param lastSync The timestamp of the last time the rewards cycle was synced + /// @param rewardCycleAmount The amount of rewards to be distributed in the current cycle + event SyncRewards(uint40 cycleEnd, uint40 lastSync, uint216 rewardCycleAmount); + + /// @notice The ```DistributeRewards``` event is emitted when rewards are distributed to storedTotalAssets + /// @param rewardsToDistribute The amount of rewards that were distributed + event DistributeRewards(uint256 rewardsToDistribute); +} diff --git a/src/contracts/ethereum/sfrxUSD/inherited/LinearRewardsErc4626_2.sol b/src/contracts/ethereum/sfrxUSD/inherited/LinearRewardsErc4626_2.sol new file mode 100644 index 0000000..99d4097 --- /dev/null +++ b/src/contracts/ethereum/sfrxUSD/inherited/LinearRewardsErc4626_2.sol @@ -0,0 +1,394 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.21; + +// ==================================================================== +// | ______ _______ | +// | / _____________ __ __ / ____(_____ ____ _____ ________ | +// | / /_ / ___/ __ `| |/_/ / /_ / / __ \/ __ `/ __ \/ ___/ _ \ | +// | / __/ / / / /_/ _> < / __/ / / / / / /_/ / / / / /__/ __/ | +// | /_/ /_/ \__,_/_/|_| /_/ /_/_/ /_/\__,_/_/ /_/\___/\___/ | +// | | +// ==================================================================== +// ===================== LinearRewardsErc4626_2 ==================== +// ==================================================================== +// Frax Finance: https://github.com/FraxFinance + +import { ERC20, ERC4626 } from "solmate/mixins/ERC4626.sol"; +import { SafeCastLib } from "solmate/utils/SafeCastLib.sol"; +import { ln, mul, div, pow, exp, wrap } from "@prb/math/src/ud60x18/Math.sol"; +import { convert } from "@prb/math/src/ud60x18/Conversions.sol"; +import { UD60x18 } from "@prb/math/src/ud60x18/ValueType.sol"; +import "forge-std/console2.sol"; + +/// @title LinearRewardsErc4626 +/// @notice An ERC4626 Vault implementation with linear rewards +abstract contract LinearRewardsErc4626_2 is ERC4626 { + using SafeCastLib for *; + + /// @notice The precision of all integer calculations + uint256 public constant PRECISION = 1e18; + + /// @notice One year, in seconds + uint256 public constant ONE_YEAR = 31_536_000; + + /// @notice The rewards cycle length in seconds + uint256 public immutable REWARDS_CYCLE_LENGTH = 604_800; // 7 days + + /// @notice Precomputed year + UD60x18 public immutable ONE_YEAR_UD60X18; + + /// @notice Information about the current rewards cycle + struct RewardsCycleData { + uint40 cycleEnd; // Timestamp of the end of the current rewards cycle + uint40 lastSync; // Timestamp of the last time the rewards cycle was synced + uint216 rewardCycleAmount; // Amount of rewards to be distributed in the current cycle + } + + /// @notice The rewards cycle data, stored in a single word to save gas + RewardsCycleData public DEPRECATED__rewardsCycleData; + + /// @notice The timestamp of the last time rewards were distributed + uint256 public DEPRECATED__lastRewardsDistribution; + + /// @notice The total amount of assets that have been distributed and deposited + uint256 public DEPRECATED__storedTotalAssets; + + /// @notice The precision of the underlying asset + uint256 public immutable UNDERLYING_PRECISION; + + // --------------------------------------------- + // DEPRECATED STORAGE SLOTS (for storage order preservation) + // --------------------------------------------- + /// @notice The pending timelock address + address public DEPRECATED__pendingTimelockAddress; + + /// @notice The current timelock address + address public DEPRECATED__timelockAddress; + + /// @notice The maximum amount of rewards that can be distributed per second per 1e18 asset + uint256 public DEPRECATED__maxDistributionPerSecondPerAsset; + + uint256 private DEPRECATED__initializeStage; + + // --------------------------------------------- + // NEW STATE VARIABLES + // --------------------------------------------- + + /// @notice Last stored pricePerShare. Current rate is stored + (rate * pricePerShareIncPerSecond) + uint256 public pricePerShareStored; + + /// @notice Manually set increase in pricePerShare, per second + uint256 public pricePerShareIncPerSecond; + + /// @notice The last time the contract was synced + uint256 public lastSync; + + // --------------------------------------------- + // CONSTRUCTOR + // --------------------------------------------- + + /// @param _underlying The erc20 asset deposited + /// @param _name The name of the vault + /// @param _symbol The symbol of the vault + constructor(ERC20 _underlying, string memory _name, string memory _symbol) ERC4626(_underlying, _name, _symbol) { + if (_underlying.decimals() != 18) revert UnderlyingAssetMustBe18Decimals(); + UNDERLYING_PRECISION = 10 ** _underlying.decimals(); + ONE_YEAR_UD60X18 = convert(ONE_YEAR); + } + + // --------------------------------------------- + // VIEW FUNCTIONS + // --------------------------------------------- + + /// @notice Calculate pricePerShare increase per second needed for a given APY. + /// @param _apyE18 APY in 1.%%E18 (e.g. 5% APY = input 1.05e18). Must be >= 1e18 + /// @return _newPPSIPS The needed pricePerShare increase, per second, in UNDERLYING_PRECISION + function calcPPSIPSForGivenAPY(uint256 _apyE18) public view returns (uint256 _newPPSIPS) { + if (_apyE18 < 1e18) revert InvalidAPY(); + // Old + // UD60x18 _numerator = mul(ln(convert(_apyE18)), convert(1e18)) - mul(ln(convert(1e18)), convert(1e18)); + // UD60x18 _denominator = convert(ONE_YEAR); + // _newPPSIPS = convert(div(_numerator, _denominator)); + // New + UD60x18 _numerator = ln(wrap(_apyE18)); + UD60x18 _denominator = ONE_YEAR_UD60X18; + _newPPSIPS = (div(_numerator, _denominator)).unwrap(); + } + + /// @notice Calculate the total assets as of a given time. + /// @param _asOfTime The time at which to calculate. Must be now or in the future. + /// @return _newTotalAssets Expected total assets at _asOfTime, in UNDERLYING_PRECISION + function _previewTotalAssets(uint256 _asOfTime) internal view returns (uint256 _newTotalAssets) { + _newTotalAssets = (_previewPricePerShare(_asOfTime) * totalSupply) / 1e18; + } + + /// @notice Calculate current totalAssets as of now, accounting for elapsed time + /// @return _newTotalAssets Total assets as of right now, in UNDERLYING_PRECISION + function previewTotalAssets() public view returns (uint256 _newTotalAssets) { + // Do the calculation + return _previewTotalAssets(block.timestamp); + } + + /// @notice Calculate current totalAssets as of now, accounting for elapsed time + /// @return _newTotalAssets Total assets as of right now, in UNDERLYING_PRECISION + function storedTotalAssets() public view returns (uint256 _newTotalAssets) { + return previewTotalAssets(); + } + + /// @notice Calculate totalAssets at a future time + /// @param _futureTime The future time at which to calculate + /// @return _newTotalAssets Expected total assets at _futureTime, in UNDERLYING_PRECISION + function previewTotalAssetsFuture(uint256 _futureTime) public view returns (uint256 _newTotalAssets) { + // Do the calculation + return _previewTotalAssets(_futureTime); + } + + /// @notice Calculate current pricePerShare as of the given time, accounting for any elapsed time since the last sync. + /// @param _asOfTime The time at which to calculate. Must be now or in the future + /// @return _newPricePerShare Expected pricePerShare at _asOfTime, in UNDERLYING_PRECISION + function _previewPricePerShare(uint256 _asOfTime) internal view returns (uint256 _newPricePerShare) { + // Calculate the elapsed time + uint256 _elapsedTime = _asOfTime - lastSync; + + // Continuously compounding interest. Done here instead of in _previewTotalAssets + // p(t) = p₀ * e^((dr)*t) + // Also might be able to use e^(xy) = (e^x)^y (to avoid overflows) + // --------------------------------------- + // Calculate e^x and convert back to uint256 + + // Get the UD60x18 exponent first and scale down by UNDERLYING_PRECISION + // OLD: UD60x18 _exponentUD60_18 = div( + // convert(pricePerShareIncPerSecond * _elapsedTime), + // convert(UNDERLYING_PRECISION) + // ); + // Get the UD60x18 exponent first and scale down by UNDERLYING_PRECISION + UD60x18 _exponentUD60_18 = wrap(pricePerShareIncPerSecond * _elapsedTime); + // UD60x18 _exponentUD60_18 = div( + // convert(pricePerShareIncPerSecond * _elapsedTime), + // convert(UNDERLYING_PRECISION) + // ); + + // Get the raw e^exponent in UD60x18 + UD60x18 _ePowUD60_18 = exp(_exponentUD60_18); + + // Old + // { + // // Scale the UD60x18 up by UNDERLYING_PRECISION and convert to uint256 + // uint256 _ePowU256 = convert(mul(_ePowUD60_18, convert(UNDERLYING_PRECISION))); + + // // Calculate _newPricePerShare + // _newPricePerShare = (pricePerShareStored * _ePowU256) / UNDERLYING_PRECISION; + // } + + // New + { + _newPricePerShare = mul(wrap(pricePerShareStored), _ePowUD60_18).unwrap(); + } + } + + /// @notice Calculate current pricePerShare as of now, accounting for any elapsed time since the last sync. Same as pricePerShare(). + /// @return _newPricePerShare Current pricePerShare, in UNDERLYING_PRECISION + function previewPricePerShare() public view returns (uint256 _newPricePerShare) { + // Do the calculation + return _previewPricePerShare(block.timestamp); + } + + /// @notice Calculate pricePerShare at a future time + /// @param _futureTime The future time at which to calculate + /// @return _newPricePerShare Expected pricePerShare at _asOfTime, in UNDERLYING_PRECISION + function previewPricePerShareFuture(uint256 _futureTime) public view returns (uint256 _newPricePerShare) { + // Do the calculation + return _previewPricePerShare(_futureTime); + } + + /// @notice Calculate pricePerShare and totalAssets at a given time + /// @param _asOfTime The time at which to calculate. Must be now or in the future. + /// @return _pricePerShare Expected pricePerShare at _asOfTime, in UNDERLYING_PRECISION + /// @return _totalAssets Expected totalAssets at _asOfTime, in UNDERLYING_PRECISION + function _previewPPSAndTotalAssets( + uint256 _asOfTime + ) internal view returns (uint256 _pricePerShare, uint256 _totalAssets) { + _pricePerShare = _previewPricePerShare(_asOfTime); + _totalAssets = _previewTotalAssets(_asOfTime); + } + + /// @notice Calculate pricePerShare and totalAssets as of right now + /// @return _pricePerShare Current pricePerShare, in UNDERLYING_PRECISION + /// @return _totalAssets Current totalAssets, in UNDERLYING_PRECISION + function previewPPSAndTotalAssets() public view returns (uint256 _pricePerShare, uint256 _totalAssets) { + return _previewPPSAndTotalAssets(block.timestamp); + } + + /// @notice The current price per share token, in asset tokens. Same as previewPricePerShare(). + /// @return _pricePerShare Current pricePerShare, in UNDERLYING_PRECISION + function pricePerShare() external view returns (uint256 _pricePerShare) { + return previewPricePerShare(); + } + + /// @notice The current totalAssets, accounting for any elapsed time since the last sync + /// @dev This function simulates the rewards that will be distributed at the top of the block + /// @return _totalAssets The total assets available in the vault + function totalAssets() public view virtual override returns (uint256 _totalAssets) { + _totalAssets = _previewTotalAssets(block.timestamp); + } + + // --------------------------------------------- + // WRITE FUNCTIONS + // --------------------------------------------- + + /// @notice Update pricePerShareStored and storedTotalAssets + /// @return _pricePerShare Current pricePerShare, in UNDERLYING_PRECISION + function sync() public returns (uint256 _pricePerShare) { + // Calculate the current values + _pricePerShare = _previewPricePerShare(block.timestamp); + + // Update the state variables + pricePerShareStored = _pricePerShare; + lastSync = block.timestamp; + } + + /// @notice DEPRECATED: The ```deposit``` function allows a user to mint shares by depositing underlying + /// @param _assets The amount of underlying to deposit + /// @param _receiver The address to send the shares to + /// @return _shares The amount of shares minted + function deposit(uint256 _assets, address _receiver) public override returns (uint256 _shares) { + revert MintRedeemsDisabled(); + } + + /// @notice DEPRECATED: The ```mint``` function allows a user to mint a given number of shares + /// @param _shares The amount of shares to mint + /// @param _receiver The address to send the shares to + /// @return _assets The amount of underlying deposited + function mint(uint256 _shares, address _receiver) public override returns (uint256 _assets) { + revert MintRedeemsDisabled(); + } + + /// @notice DEPRECATED: The ```withdraw``` function allows a user to withdraw a given amount of underlying + /// @param _assets The amount of underlying to withdraw + /// @param _receiver The address to send the underlying to + /// @param _owner The address of the owner of the shares + /// @return _shares The amount of shares burned + function withdraw(uint256 _assets, address _receiver, address _owner) public override returns (uint256 _shares) { + revert MintRedeemsDisabled(); + } + + /// @notice DEPRECATED: The ```redeem``` function allows a user to redeem their shares for underlying + /// @param _shares The amount of shares to redeem + /// @param _receiver The address to send the underlying to + /// @param _owner The address of the owner of the shares + /// @return _assets The amount of underlying redeemed + function redeem(uint256 _shares, address _receiver, address _owner) public override returns (uint256 _assets) { + revert MintRedeemsDisabled(); + } + + /// @notice DEPRECATED: The ```depositWithSignature``` function allows a user to use signed approvals to deposit + /// @param _assets The amount of underlying to deposit + /// @param _receiver The address to send the shares to + /// @param _deadline The deadline for the signature + /// @param _approveMax Whether or not to approve the maximum amount + /// @param _v The v value of the signature + /// @param _r The r value of the signature + /// @param _s The s value of the signature + /// @return _shares The amount of shares minted + function depositWithSignature( + uint256 _assets, + address _receiver, + uint256 _deadline, + bool _approveMax, + uint8 _v, + bytes32 _r, + bytes32 _s + ) external returns (uint256 _shares) { + revert MintRedeemsDisabled(); + } + + /*////////////////////////////////////////////////////////////// + ////// ERC4626 ACCOUNTING LOGIC OVERRIDES + //////////////////////////////////////////////////////////////*/ + + /// @notice DEPRECATED: Will always return 0. + function previewDeposit(uint256 assets) public view override returns (uint256) { + return 0; + } + + /// @notice DEPRECATED: Will always return 0. + function previewMint(uint256 shares) public view override returns (uint256) { + return 0; + } + + /// @notice DEPRECATED: Will always return 0. + function previewWithdraw(uint256 assets) public view override returns (uint256) { + return 0; + } + + /// @notice DEPRECATED: Will always return 0. + function previewRedeem(uint256 shares) public view override returns (uint256) { + return 0; + } + + /*////////////////////////////////////////////////////////////// + ////// ERC4626 DEPOSIT/WITHDRAWAL LIMIT LOGIC OVERRIDES + //////////////////////////////////////////////////////////////*/ + + /// @notice DEPRECATED: Will always return 0. + function maxDeposit(address) public view override returns (uint256) { + return 0; + } + + /// @notice DEPRECATED: Will always return 0. + function maxMint(address) public view override returns (uint256) { + return 0; + } + + /// @notice DEPRECATED: Will always return 0. + function maxWithdraw(address owner) public view override returns (uint256) { + return 0; + } + + /// @notice DEPRECATED: Will always return 0. + function maxRedeem(address owner) public view override returns (uint256) { + return 0; + } + + /*////////////////////////////////////////////////////////////// + ////// Backward compatible yield view functions to match old interface + //////////////////////////////////////////////////////////////*/ + + /// @notice DEPRECATED: use pricePerShareIncPerSecond instead + function maxDistributionPerSecondPerAsset() external view returns (uint256) { + // Return the maximum distribution per second per asset + return pricePerShareIncPerSecond; + } + + /// @notice DEPRECATED: use pricePerShareIncPerSecond instead + function rewardsCycleData() external view returns (RewardsCycleData memory) { + // Return the rewards cycle data as the max possible rate, rate is curbed by maxDistributionPerSecondPerAsset + return + RewardsCycleData({ + cycleEnd: uint40(block.timestamp + REWARDS_CYCLE_LENGTH), + lastSync: uint40(block.timestamp), + rewardCycleAmount: uint216(type(uint216).max / 1e18) // max value + }); + } + + function lastRewardsDistribution() external view returns (uint256) { + return block.timestamp; + } + + //============================================================================== + // Errors + //============================================================================== + + /// @notice If the asset is not 18 decimals + error UnderlyingAssetMustBe18Decimals(); + + /// @notice When the provided APY is invalid + error InvalidAPY(); + + /// @notice When a user attempts to Mint/Redeem + error MintRedeemsDisabled(); + + //============================================================================== + // Events + //============================================================================== +} From 6cada7fe7b2ebd869785ac73e326ecf6812ce37c Mon Sep 17 00:00:00 2001 From: Carter Carlson Date: Thu, 11 Sep 2025 00:25:21 -0700 Subject: [PATCH 06/12] feat: base interfaces dir --- src/contracts/interfaces/IERC20Burnable.sol | 6 ++++++ src/contracts/interfaces/IMinter.sol | 22 +++++++++++++++++++++ src/contracts/interfaces/ITimelock2Step.sol | 0 3 files changed, 28 insertions(+) create mode 100644 src/contracts/interfaces/IERC20Burnable.sol create mode 100644 src/contracts/interfaces/IMinter.sol create mode 100644 src/contracts/interfaces/ITimelock2Step.sol diff --git a/src/contracts/interfaces/IERC20Burnable.sol b/src/contracts/interfaces/IERC20Burnable.sol new file mode 100644 index 0000000..b5a65fa --- /dev/null +++ b/src/contracts/interfaces/IERC20Burnable.sol @@ -0,0 +1,6 @@ +pragma solidity ^0.8.0; + +interface IERC20Burnable { + function burn(uint256 value) external; + function burnFrom(address account, uint256 value) external; +} 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/contracts/interfaces/ITimelock2Step.sol b/src/contracts/interfaces/ITimelock2Step.sol new file mode 100644 index 0000000..e69de29 From a97fba5a77cc597bb9c15fbda50222ae6f749396 Mon Sep 17 00:00:00 2001 From: Carter Carlson Date: Thu, 11 Sep 2025 00:28:33 -0700 Subject: [PATCH 07/12] chore: rm ITimelock2Step --- src/contracts/interfaces/ITimelock2Step.sol | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/contracts/interfaces/ITimelock2Step.sol diff --git a/src/contracts/interfaces/ITimelock2Step.sol b/src/contracts/interfaces/ITimelock2Step.sol deleted file mode 100644 index e69de29..0000000 From cb9e31f0ce8e359d22f811c8241d18c5f0d997e7 Mon Sep 17 00:00:00 2001 From: Carter Carlson Date: Tue, 9 Sep 2025 23:55:24 -0700 Subject: [PATCH 08/12] build: oz 5.3 --- src/contracts/ethereum/interfaces/IFrxUSD.sol | 47 +++++++++++++++++++ .../ethereum/interfaces/IFrxUSD2.sol | 27 +++++++++++ 2 files changed, 74 insertions(+) create mode 100644 src/contracts/ethereum/interfaces/IFrxUSD.sol create mode 100644 src/contracts/ethereum/interfaces/IFrxUSD2.sol diff --git a/src/contracts/ethereum/interfaces/IFrxUSD.sol b/src/contracts/ethereum/interfaces/IFrxUSD.sol new file mode 100644 index 0000000..ea17ce5 --- /dev/null +++ b/src/contracts/ethereum/interfaces/IFrxUSD.sol @@ -0,0 +1,47 @@ +pragma solidity ^0.8.0; + +import { IERC20 } from "@openzeppelin/contracts-5.3.0/token/ERC20/ERC20.sol"; +import { IERC20Permit } from "@openzeppelin/contracts-5.3.0/token/ERC20/extensions/IERC20Permit.sol"; + +/// @title FrxUSD interface +interface IFrxUSD is IERC20, IERC20Permit { + function minters_array(uint256) external view returns (address); + function minters(address) external view returns (bool); + function initialize(address _owner, string memory _name, string memory _symbol) external; + 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; + + /* ========== 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 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/ethereum/interfaces/IFrxUSD2.sol b/src/contracts/ethereum/interfaces/IFrxUSD2.sol new file mode 100644 index 0000000..93464fe --- /dev/null +++ b/src/contracts/ethereum/interfaces/IFrxUSD2.sol @@ -0,0 +1,27 @@ +pragma solidity ^0.8.0; + +import { IFrxUSD } from "src/contracts/ethereum/interfaces/IFrxUSD.sol"; + +/// @title FrxUSD2 interface +interface IFrxUSD2 is IFrxUSD { + function isFrozen(address account) external view returns (bool); + function isPaused() external view returns (bool); + 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 burn(address _owner, uint256 _amount) external; + function pause() external; + function unpause() external; + + event Paused(); + event Unpaused(); + event AccountFrozen(address account); + event AccountThawed(address account); + + error OwnerCannotInitToZeroAddress(); + error ArrayMisMatch(); + error IsPaused(); + error IsFrozen(); +} From 7091ee6c645c3049828351ae6292f0f6c01ff664 Mon Sep 17 00:00:00 2001 From: Carter Carlson Date: Thu, 11 Sep 2025 00:24:15 -0700 Subject: [PATCH 09/12] chore: mv frxUSD interface to frxUSD dir --- src/contracts/ethereum/interfaces/IFrxUSD.sol | 47 ------------------- .../ethereum/interfaces/IFrxUSD2.sol | 27 ----------- 2 files changed, 74 deletions(-) delete mode 100644 src/contracts/ethereum/interfaces/IFrxUSD.sol delete mode 100644 src/contracts/ethereum/interfaces/IFrxUSD2.sol diff --git a/src/contracts/ethereum/interfaces/IFrxUSD.sol b/src/contracts/ethereum/interfaces/IFrxUSD.sol deleted file mode 100644 index ea17ce5..0000000 --- a/src/contracts/ethereum/interfaces/IFrxUSD.sol +++ /dev/null @@ -1,47 +0,0 @@ -pragma solidity ^0.8.0; - -import { IERC20 } from "@openzeppelin/contracts-5.3.0/token/ERC20/ERC20.sol"; -import { IERC20Permit } from "@openzeppelin/contracts-5.3.0/token/ERC20/extensions/IERC20Permit.sol"; - -/// @title FrxUSD interface -interface IFrxUSD is IERC20, IERC20Permit { - function minters_array(uint256) external view returns (address); - function minters(address) external view returns (bool); - function initialize(address _owner, string memory _name, string memory _symbol) external; - 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; - - /* ========== 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 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/ethereum/interfaces/IFrxUSD2.sol b/src/contracts/ethereum/interfaces/IFrxUSD2.sol deleted file mode 100644 index 93464fe..0000000 --- a/src/contracts/ethereum/interfaces/IFrxUSD2.sol +++ /dev/null @@ -1,27 +0,0 @@ -pragma solidity ^0.8.0; - -import { IFrxUSD } from "src/contracts/ethereum/interfaces/IFrxUSD.sol"; - -/// @title FrxUSD2 interface -interface IFrxUSD2 is IFrxUSD { - function isFrozen(address account) external view returns (bool); - function isPaused() external view returns (bool); - 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 burn(address _owner, uint256 _amount) external; - function pause() external; - function unpause() external; - - event Paused(); - event Unpaused(); - event AccountFrozen(address account); - event AccountThawed(address account); - - error OwnerCannotInitToZeroAddress(); - error ArrayMisMatch(); - error IsPaused(); - error IsFrozen(); -} From 30d1a8102c034d58051d71a2e2f6eceb696b4577 Mon Sep 17 00:00:00 2001 From: Carter Carlson Date: Thu, 11 Sep 2025 00:25:21 -0700 Subject: [PATCH 10/12] feat: base interfaces dir --- src/contracts/interfaces/ITimelock2Step.sol | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/contracts/interfaces/ITimelock2Step.sol diff --git a/src/contracts/interfaces/ITimelock2Step.sol b/src/contracts/interfaces/ITimelock2Step.sol new file mode 100644 index 0000000..e69de29 From f57dffe5919cf66d8357965015974505c8042505 Mon Sep 17 00:00:00 2001 From: Carter Carlson Date: Thu, 11 Sep 2025 00:28:33 -0700 Subject: [PATCH 11/12] chore: rm ITimelock2Step --- src/contracts/interfaces/ITimelock2Step.sol | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/contracts/interfaces/ITimelock2Step.sol diff --git a/src/contracts/interfaces/ITimelock2Step.sol b/src/contracts/interfaces/ITimelock2Step.sol deleted file mode 100644 index e69de29..0000000 From 460155b6c551dc5e6ea6f2e619028976dfa6e48e Mon Sep 17 00:00:00 2001 From: Thomas Clement <88335455+tom2o17@users.noreply.github.com> Date: Sat, 20 Sep 2025 19:00:53 -0400 Subject: [PATCH 12/12] FrxUSD Versioning Suggestion (#5) --- src/contracts/ethereum/frxUSD/FrxUSD.sol | 144 +++--------------- src/contracts/ethereum/frxUSD/IFrxUSD.sol | 22 +-- .../ethereum/frxUSD/versioning/FrxUSD1.sol | 133 ++++++++++++++++ .../frxUSD/{ => versioning}/FrxUSD2.sol | 0 .../ethereum/frxUSD/versioning/IFrxUSD1.sol | 23 +++ .../frxUSD/{ => versioning}/IFrxUSD2.sol | 4 +- src/contracts/ethereum/sfrxUSD/ISfrxUSD.sol | 18 +-- src/contracts/ethereum/sfrxUSD/SfrxUSD.sol | 127 +-------------- .../ethereum/sfrxUSD/versioning/ISfrxUSD1.sol | 19 +++ .../sfrxUSD/{ => versioning}/ISfrxUSD2.sol | 0 .../ethereum/sfrxUSD/versioning/SfrxUSD1.sol | 143 +++++++++++++++++ .../sfrxUSD/{ => versioning}/SfrxUSD2.sol | 0 12 files changed, 347 insertions(+), 286 deletions(-) create mode 100644 src/contracts/ethereum/frxUSD/versioning/FrxUSD1.sol rename src/contracts/ethereum/frxUSD/{ => versioning}/FrxUSD2.sol (100%) create mode 100644 src/contracts/ethereum/frxUSD/versioning/IFrxUSD1.sol rename src/contracts/ethereum/frxUSD/{ => versioning}/IFrxUSD2.sol (87%) create mode 100644 src/contracts/ethereum/sfrxUSD/versioning/ISfrxUSD1.sol rename src/contracts/ethereum/sfrxUSD/{ => versioning}/ISfrxUSD2.sol (100%) create mode 100644 src/contracts/ethereum/sfrxUSD/versioning/SfrxUSD1.sol rename src/contracts/ethereum/sfrxUSD/{ => versioning}/SfrxUSD2.sol (100%) diff --git a/src/contracts/ethereum/frxUSD/FrxUSD.sol b/src/contracts/ethereum/frxUSD/FrxUSD.sol index 03b9d2f..07f6e16 100644 --- a/src/contracts/ethereum/frxUSD/FrxUSD.sol +++ b/src/contracts/ethereum/frxUSD/FrxUSD.sol @@ -1,133 +1,25 @@ -//SPDX-License-Identifier: Unlicense +// SPDX-License-Identifier: AGPL-3.0-only pragma solidity ^0.8.0; -import { ERC20Permit, ERC20 } from "@openzeppelin/contracts-5.3.0/token/ERC20/extensions/ERC20Permit.sol"; -import { ERC20Burnable } from "@openzeppelin/contracts-5.3.0/token/ERC20/extensions/ERC20Burnable.sol"; -import { Ownable2Step } from "@openzeppelin/contracts-5.3.0/access/Ownable2Step.sol"; -import { Ownable } from "@openzeppelin/contracts-5.3.0/access/Ownable.sol"; -import { StorageSlot } from "@openzeppelin/contracts-5.3.0/utils/StorageSlot.sol"; - -/// @title FrxUSD -/** - * @notice Combines Openzeppelin's ERC20Permit, ERC20Burnable and Ownable2Step. - * Also includes a list of authorized minters - */ -/// @dev FrxUSD adheres to EIP-712/EIP-2612 and can use permits -contract FrxUSD is ERC20Permit, ERC20Burnable, Ownable2Step { - /// @notice Array of the non-bridge minters - address[] public minters_array; - - /// @notice Mapping of the minters - /// @dev Mapping is used for faster verification - mapping(address => bool) public minters; - - /* ========== CONSTRUCTOR ========== */ - /// @param _ownerAddress The initial owner - /// @param _name ERC20 name - /// @param _symbol ERC20 symbol +// ==================================================================== +// | ______ _______ | +// | / _____________ __ __ / ____(_____ ____ _____ ________ | +// | / /_ / ___/ __ `| |/_/ / /_ / / __ \/ __ `/ __ \/ ___/ _ \ | +// | / __/ / / / /_/ _> < / __/ / / / / / /_/ / / / / /__/ __/ | +// | /_/ /_/ \__,_/_/|_| /_/ /_/_/ /_/\__,_/_/ /_/\___/\___/ | +// | | +// ==================================================================== +// ============================= FrxUSD =============================== +// ==================================================================== +// Frax Finance: https://github.com/FraxFinance +// Tested for 18-decimal underlying assets only + +import { FrxUSD2 } from "src/contracts/ethereum/frxUSD/versioning/FrxUSD2.sol"; + +contract FrxUSD is FrxUSD2 { constructor( address _ownerAddress, string memory _name, string memory _symbol - ) ERC20(_name, _symbol) ERC20Permit(_name) Ownable(_ownerAddress) {} - - /* ========== INITIALIZER ========== */ - /// @dev Used to initialize the contract when it is behind a proxy - function initialize(address _owner, string memory _name, string memory _symbol) public { - require(owner() == address(0), "Already initialized"); - _transferOwnership(_owner); - StorageSlot.getBytesSlot(bytes32(uint256(3))).value = bytes(_name); - StorageSlot.getBytesSlot(bytes32(uint256(4))).value = bytes(_symbol); - } - - /* ========== MODIFIERS ========== */ - - /// @notice A modifier that only allows a minters to call - modifier onlyMinters() { - require(minters[msg.sender] == true, "Only minters"); - _; - } - - /* ========== RESTRICTED FUNCTIONS [MINTERS] ========== */ - - /// @notice Used by 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 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); - } - - /* ========== RESTRICTED FUNCTIONS [OWNER] ========== */ - /// @notice Adds a minter - /// @param minter_address Address of minter to add - function addMinter(address minter_address) public onlyOwner { - 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 onlyOwner { - require(minter_address != address(0), "Zero address detected"); - require(minters[minter_address] == true, "Address nonexistant"); - - // 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); - } - - /* ========== 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 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); + ) FrxUSD2(_ownerAddress, _name, _symbol) {} } diff --git a/src/contracts/ethereum/frxUSD/IFrxUSD.sol b/src/contracts/ethereum/frxUSD/IFrxUSD.sol index 8f52e0c..356a305 100644 --- a/src/contracts/ethereum/frxUSD/IFrxUSD.sol +++ b/src/contracts/ethereum/frxUSD/IFrxUSD.sol @@ -1,23 +1,5 @@ pragma solidity ^0.8.0; -import { IERC20 } from "@openzeppelin/contracts-5.3.0/token/ERC20/ERC20.sol"; -import { IERC20Permit } from "@openzeppelin/contracts-5.3.0/token/ERC20/extensions/IERC20Permit.sol"; -import { IERC20Burnable } from "src/contracts/interfaces/IERC20Burnable.sol"; -import { IMinter } from "src/contracts/interfaces/IMinter.sol"; +import { IFrxUSD2 } from "src/contracts/ethereum/frxUSD/versioning/IFrxUSD2.sol"; -/// @title FrxUSD interface -interface IFrxUSD is IERC20, IERC20Permit, IERC20Burnable, IMinter { - function initialize(address _owner, string memory _name, string memory _symbol) external; - - /* ========== 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); -} +interface IFrxUSD is IFrxUSD2 {} diff --git a/src/contracts/ethereum/frxUSD/versioning/FrxUSD1.sol b/src/contracts/ethereum/frxUSD/versioning/FrxUSD1.sol new file mode 100644 index 0000000..f2a92a7 --- /dev/null +++ b/src/contracts/ethereum/frxUSD/versioning/FrxUSD1.sol @@ -0,0 +1,133 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.0; + +import { ERC20Permit, ERC20 } from "@openzeppelin/contracts-5.3.0/token/ERC20/extensions/ERC20Permit.sol"; +import { ERC20Burnable } from "@openzeppelin/contracts-5.3.0/token/ERC20/extensions/ERC20Burnable.sol"; +import { Ownable2Step } from "@openzeppelin/contracts-5.3.0/access/Ownable2Step.sol"; +import { Ownable } from "@openzeppelin/contracts-5.3.0/access/Ownable.sol"; +import { StorageSlot } from "@openzeppelin/contracts-5.3.0/utils/StorageSlot.sol"; + +/// @title FrxUSD +/** + * @notice Combines Openzeppelin's ERC20Permit, ERC20Burnable and Ownable2Step. + * Also includes a list of authorized minters + */ +/// @dev FrxUSD adheres to EIP-712/EIP-2612 and can use permits +contract FrxUSD1 is ERC20Permit, ERC20Burnable, Ownable2Step { + /// @notice Array of the non-bridge minters + address[] public minters_array; + + /// @notice Mapping of the minters + /// @dev Mapping is used for faster verification + mapping(address => bool) public minters; + + /* ========== CONSTRUCTOR ========== */ + /// @param _ownerAddress The initial owner + /// @param _name ERC20 name + /// @param _symbol ERC20 symbol + constructor( + address _ownerAddress, + string memory _name, + string memory _symbol + ) ERC20(_name, _symbol) ERC20Permit(_name) Ownable(_ownerAddress) {} + + /* ========== INITIALIZER ========== */ + /// @dev Used to initialize the contract when it is behind a proxy + function initialize(address _owner, string memory _name, string memory _symbol) public { + require(owner() == address(0), "Already initialized"); + _transferOwnership(_owner); + StorageSlot.getBytesSlot(bytes32(uint256(3))).value = bytes(_name); + StorageSlot.getBytesSlot(bytes32(uint256(4))).value = bytes(_symbol); + } + + /* ========== MODIFIERS ========== */ + + /// @notice A modifier that only allows a minters to call + modifier onlyMinters() { + require(minters[msg.sender] == true, "Only minters"); + _; + } + + /* ========== RESTRICTED FUNCTIONS [MINTERS] ========== */ + + /// @notice Used by 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 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); + } + + /* ========== RESTRICTED FUNCTIONS [OWNER] ========== */ + /// @notice Adds a minter + /// @param minter_address Address of minter to add + function addMinter(address minter_address) public onlyOwner { + 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 onlyOwner { + require(minter_address != address(0), "Zero address detected"); + require(minters[minter_address] == true, "Address nonexistant"); + + // 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); + } + + /* ========== 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 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/ethereum/frxUSD/FrxUSD2.sol b/src/contracts/ethereum/frxUSD/versioning/FrxUSD2.sol similarity index 100% rename from src/contracts/ethereum/frxUSD/FrxUSD2.sol rename to src/contracts/ethereum/frxUSD/versioning/FrxUSD2.sol diff --git a/src/contracts/ethereum/frxUSD/versioning/IFrxUSD1.sol b/src/contracts/ethereum/frxUSD/versioning/IFrxUSD1.sol new file mode 100644 index 0000000..d3a7f29 --- /dev/null +++ b/src/contracts/ethereum/frxUSD/versioning/IFrxUSD1.sol @@ -0,0 +1,23 @@ +pragma solidity ^0.8.0; + +import { IERC20 } from "@openzeppelin/contracts-5.3.0/token/ERC20/ERC20.sol"; +import { IERC20Permit } from "@openzeppelin/contracts-5.3.0/token/ERC20/extensions/IERC20Permit.sol"; +import { IERC20Burnable } from "src/contracts/interfaces/IERC20Burnable.sol"; +import { IMinter } from "src/contracts/interfaces/IMinter.sol"; + +/// @title FrxUSD interface +interface IFrxUSD1 is IERC20, IERC20Permit, IERC20Burnable, IMinter { + function initialize(address _owner, string memory _name, string memory _symbol) external; + + /* ========== 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); +} diff --git a/src/contracts/ethereum/frxUSD/IFrxUSD2.sol b/src/contracts/ethereum/frxUSD/versioning/IFrxUSD2.sol similarity index 87% rename from src/contracts/ethereum/frxUSD/IFrxUSD2.sol rename to src/contracts/ethereum/frxUSD/versioning/IFrxUSD2.sol index cb8cf9c..a0e3f8f 100644 --- a/src/contracts/ethereum/frxUSD/IFrxUSD2.sol +++ b/src/contracts/ethereum/frxUSD/versioning/IFrxUSD2.sol @@ -1,9 +1,9 @@ pragma solidity ^0.8.0; -import { IFrxUSD } from "src/contracts/ethereum/frxUSD/IFrxUSD.sol"; +import { IFrxUSD1 } from "src/contracts/ethereum/frxUSD/versioning/IFrxUSD1.sol"; /// @title FrxUSD2 interface -interface IFrxUSD2 is IFrxUSD { +interface IFrxUSD2 is IFrxUSD1 { function isFrozen(address account) external view returns (bool); function isPaused() external view returns (bool); function thawMany(address[] memory _owners) external; diff --git a/src/contracts/ethereum/sfrxUSD/ISfrxUSD.sol b/src/contracts/ethereum/sfrxUSD/ISfrxUSD.sol index 1d7a14b..fa03bc3 100644 --- a/src/contracts/ethereum/sfrxUSD/ISfrxUSD.sol +++ b/src/contracts/ethereum/sfrxUSD/ISfrxUSD.sol @@ -1,19 +1,5 @@ pragma solidity ^0.8.0; -import { ILinearRewardsErc4626 } from "src/contracts/ethereum/sfrxUSD/inherited/ILinearRewardsErc4626.sol"; -import { ITimelock2Step } from "frax-std/access-control/v2/interfaces/ITimelock2Step.sol"; +import { ISfrxUSD2 } from "src/contracts/ethereum/sfrxUSD/versioning/ISfrxUSD2.sol"; -interface ISfrxUSD is ILinearRewardsErc4626, ITimelock2Step { - // state variables - function maxDistributionPerSecondPerAsset() external view returns (uint256); - function version() external view returns (string memory); - function setMaxDistributionPerSecondPerAsset(uint256 _maxDistributionPerSecondPerAsset) external; - function calculateRewardsToDistribute( - RewardsCycleData memory _rewardsCycleData, - uint256 _deltatime - ) external view returns (uint256 _rewardToDistribute); - - event SetMaxDistributionPerSecondPerAsset(uint256 oldMax, uint256 newMax); - - error AlreadyInitialized(); -} +interface ISfrxUSD is ISfrxUSD2 {} diff --git a/src/contracts/ethereum/sfrxUSD/SfrxUSD.sol b/src/contracts/ethereum/sfrxUSD/SfrxUSD.sol index b2fda92..8288c81 100644 --- a/src/contracts/ethereum/sfrxUSD/SfrxUSD.sol +++ b/src/contracts/ethereum/sfrxUSD/SfrxUSD.sol @@ -9,135 +9,18 @@ pragma solidity ^0.8.21; // | /_/ /_/ \__,_/_/|_| /_/ /_/_/ /_/\__,_/_/ /_/\___/\___/ | // | | // ==================================================================== -// =========================== StakedFrxUSD =========================== +// ========================== StakedFrxUSD ============================ // ==================================================================== // Frax Finance: https://github.com/FraxFinance +// Tested for 18-decimal underlying assets only -import { Timelock2Step } from "frax-std/access-control/v2/Timelock2Step.sol"; -import { IERC20 } from "@openzeppelin/contracts-5.3.0/token/ERC20/ERC20.sol"; -import { SafeCastLib } from "solmate/utils/SafeCastLib.sol"; -import { LinearRewardsErc4626, ERC20 } from "src/contracts/ethereum/sfrxUSD/inherited/LinearRewardsErc4626.sol"; +import { SfrxUSD2, IERC20 } from "src/contracts/ethereum/sfrxUSD/versioning/SfrxUSD2.sol"; -/// @title Staked frxUSD -/// @notice A ERC4626 Vault implementation with linear rewards, rewards can be capped -contract SfrxUSD is LinearRewardsErc4626, Timelock2Step { - using SafeCastLib for *; - - /// @notice The maximum amount of rewards that can be distributed per second per 1e18 asset - uint256 public maxDistributionPerSecondPerAsset; - - uint256 private initializeStage = 2; - - string public constant version = "1.0.0"; - - /// @param _underlying The erc20 asset deposited - /// @param _name The name of the vault - /// @param _symbol The symbol of the vault - /// @param _rewardsCycleLength The length of the rewards cycle in seconds - /// @param _maxDistributionPerSecondPerAsset The maximum amount of rewards that can be distributed per second per 1e18 asset - /// @param _timelockAddress The address of the timelock/owner contract +contract SfrxUSD is SfrxUSD2 { constructor( IERC20 _underlying, string memory _name, string memory _symbol, - uint32 _rewardsCycleLength, - uint256 _maxDistributionPerSecondPerAsset, - address _timelockAddress - ) - LinearRewardsErc4626(ERC20(address(_underlying)), _name, _symbol, _rewardsCycleLength) - Timelock2Step(_timelockAddress) - { - maxDistributionPerSecondPerAsset = _maxDistributionPerSecondPerAsset; - } - - /// @notice The ```SetMaxDistributionPerSecondPerAsset``` event is emitted when the maxDistributionPerSecondPerAsset is set - /// @param oldMax The old maxDistributionPerSecondPerAsset value - /// @param newMax The new maxDistributionPerSecondPerAsset value - event SetMaxDistributionPerSecondPerAsset(uint256 oldMax, uint256 newMax); - - error AlreadyInitialized(); - - function initialize( - string memory _name, - string memory _symbol, - uint256 _maxDistributionPerSecondPerAsset, address _timelockAddress - ) external { - if (initializeStage != 0) revert AlreadyInitialized(); - initializeStage++; - name = _name; - symbol = _symbol; - maxDistributionPerSecondPerAsset = _maxDistributionPerSecondPerAsset; - timelockAddress = _timelockAddress; - - // initialize rewardsCycleEnd value - // NOTE: normally distribution of rewards should be done prior to _syncRewards but in this case we know there are no users or rewards yet. - _syncRewards(); - - // initialize lastRewardsDistribution value - _distributeRewards(); - } - - /// @notice The ```initializeRewardsCycleData``` function initializes the rewards cycle data - /// @dev This function can only be called once - /// @param _pricePerShare The price per share - /// @param _maxDistributionPerSecondPerAsset The maximum amount of rewards that can be distributed per second per 1e18 asset - /// @param _cycleEnd The end of the rewards cycle - /// @param _lastSync The last sync time - /// @param _rewardCycleAmount The reward cycle amount - function initializeRewardsCycleData( - uint256 _pricePerShare, - uint256 _maxDistributionPerSecondPerAsset, - uint40 _cycleEnd, - uint40 _lastSync, - uint216 _rewardCycleAmount - ) external { - if (initializeStage != 1) revert AlreadyInitialized(); - initializeStage++; - storedTotalAssets = (_pricePerShare * totalSupply) / PRECISION; - maxDistributionPerSecondPerAsset = _maxDistributionPerSecondPerAsset; - rewardsCycleData.cycleEnd = _cycleEnd; - rewardsCycleData.lastSync = _lastSync; - rewardsCycleData.rewardCycleAmount = _rewardCycleAmount; - } - - /// @notice The ```setMaxDistributionPerSecondPerAsset``` function sets the maxDistributionPerSecondPerAsset - /// @dev This function can only be called by the timelock, caps the value to type(uint64).max - /// @param _maxDistributionPerSecondPerAsset The maximum amount of rewards that can be distributed per second per 1e18 asset - function setMaxDistributionPerSecondPerAsset(uint256 _maxDistributionPerSecondPerAsset) external { - _requireSenderIsTimelock(); - syncRewardsAndDistribution(); - - // NOTE: prevents bricking the contract via overflow - if (_maxDistributionPerSecondPerAsset > type(uint64).max) { - _maxDistributionPerSecondPerAsset = type(uint64).max; - } - - emit SetMaxDistributionPerSecondPerAsset({ - oldMax: maxDistributionPerSecondPerAsset, - newMax: _maxDistributionPerSecondPerAsset - }); - - maxDistributionPerSecondPerAsset = _maxDistributionPerSecondPerAsset; - } - - /// @notice The ```calculateRewardsToDistribute``` function calculates the amount of rewards to distribute based on the rewards cycle data and the time passed - /// @param _rewardsCycleData The rewards cycle data - /// @param _deltaTime The time passed since the last rewards distribution - /// @return _rewardToDistribute The amount of rewards to distribute - function calculateRewardsToDistribute( - RewardsCycleData memory _rewardsCycleData, - uint256 _deltaTime - ) public view override returns (uint256 _rewardToDistribute) { - _rewardToDistribute = super.calculateRewardsToDistribute({ - _rewardsCycleData: _rewardsCycleData, - _deltaTime: _deltaTime - }); - - // Cap rewards - uint256 _maxDistribution = (maxDistributionPerSecondPerAsset * _deltaTime * storedTotalAssets) / PRECISION; - if (_rewardToDistribute > _maxDistribution) { - _rewardToDistribute = _maxDistribution; - } - } + ) SfrxUSD2(_underlying, _name, _symbol, _timelockAddress) {} } diff --git a/src/contracts/ethereum/sfrxUSD/versioning/ISfrxUSD1.sol b/src/contracts/ethereum/sfrxUSD/versioning/ISfrxUSD1.sol new file mode 100644 index 0000000..1d7a14b --- /dev/null +++ b/src/contracts/ethereum/sfrxUSD/versioning/ISfrxUSD1.sol @@ -0,0 +1,19 @@ +pragma solidity ^0.8.0; + +import { ILinearRewardsErc4626 } from "src/contracts/ethereum/sfrxUSD/inherited/ILinearRewardsErc4626.sol"; +import { ITimelock2Step } from "frax-std/access-control/v2/interfaces/ITimelock2Step.sol"; + +interface ISfrxUSD is ILinearRewardsErc4626, ITimelock2Step { + // state variables + function maxDistributionPerSecondPerAsset() external view returns (uint256); + function version() external view returns (string memory); + function setMaxDistributionPerSecondPerAsset(uint256 _maxDistributionPerSecondPerAsset) external; + function calculateRewardsToDistribute( + RewardsCycleData memory _rewardsCycleData, + uint256 _deltatime + ) external view returns (uint256 _rewardToDistribute); + + event SetMaxDistributionPerSecondPerAsset(uint256 oldMax, uint256 newMax); + + error AlreadyInitialized(); +} diff --git a/src/contracts/ethereum/sfrxUSD/ISfrxUSD2.sol b/src/contracts/ethereum/sfrxUSD/versioning/ISfrxUSD2.sol similarity index 100% rename from src/contracts/ethereum/sfrxUSD/ISfrxUSD2.sol rename to src/contracts/ethereum/sfrxUSD/versioning/ISfrxUSD2.sol diff --git a/src/contracts/ethereum/sfrxUSD/versioning/SfrxUSD1.sol b/src/contracts/ethereum/sfrxUSD/versioning/SfrxUSD1.sol new file mode 100644 index 0000000..57dad4d --- /dev/null +++ b/src/contracts/ethereum/sfrxUSD/versioning/SfrxUSD1.sol @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.21; + +// ==================================================================== +// | ______ _______ | +// | / _____________ __ __ / ____(_____ ____ _____ ________ | +// | / /_ / ___/ __ `| |/_/ / /_ / / __ \/ __ `/ __ \/ ___/ _ \ | +// | / __/ / / / /_/ _> < / __/ / / / / / /_/ / / / / /__/ __/ | +// | /_/ /_/ \__,_/_/|_| /_/ /_/_/ /_/\__,_/_/ /_/\___/\___/ | +// | | +// ==================================================================== +// =========================== StakedFrxUSD =========================== +// ==================================================================== +// Frax Finance: https://github.com/FraxFinance + +import { Timelock2Step } from "frax-std/access-control/v2/Timelock2Step.sol"; +import { IERC20 } from "@openzeppelin/contracts-5.3.0/token/ERC20/ERC20.sol"; +import { SafeCastLib } from "solmate/utils/SafeCastLib.sol"; +import { LinearRewardsErc4626, ERC20 } from "src/contracts/ethereum/sfrxUSD/inherited/LinearRewardsErc4626.sol"; + +/// @title Staked frxUSD +/// @notice A ERC4626 Vault implementation with linear rewards, rewards can be capped +contract SfrxUSD1 is LinearRewardsErc4626, Timelock2Step { + using SafeCastLib for *; + + /// @notice The maximum amount of rewards that can be distributed per second per 1e18 asset + uint256 public maxDistributionPerSecondPerAsset; + + uint256 private initializeStage = 2; + + string public constant version = "1.0.0"; + + /// @param _underlying The erc20 asset deposited + /// @param _name The name of the vault + /// @param _symbol The symbol of the vault + /// @param _rewardsCycleLength The length of the rewards cycle in seconds + /// @param _maxDistributionPerSecondPerAsset The maximum amount of rewards that can be distributed per second per 1e18 asset + /// @param _timelockAddress The address of the timelock/owner contract + constructor( + IERC20 _underlying, + string memory _name, + string memory _symbol, + uint32 _rewardsCycleLength, + uint256 _maxDistributionPerSecondPerAsset, + address _timelockAddress + ) + LinearRewardsErc4626(ERC20(address(_underlying)), _name, _symbol, _rewardsCycleLength) + Timelock2Step(_timelockAddress) + { + maxDistributionPerSecondPerAsset = _maxDistributionPerSecondPerAsset; + } + + /// @notice The ```SetMaxDistributionPerSecondPerAsset``` event is emitted when the maxDistributionPerSecondPerAsset is set + /// @param oldMax The old maxDistributionPerSecondPerAsset value + /// @param newMax The new maxDistributionPerSecondPerAsset value + event SetMaxDistributionPerSecondPerAsset(uint256 oldMax, uint256 newMax); + + error AlreadyInitialized(); + + function initialize( + string memory _name, + string memory _symbol, + uint256 _maxDistributionPerSecondPerAsset, + address _timelockAddress + ) external { + if (initializeStage != 0) revert AlreadyInitialized(); + initializeStage++; + name = _name; + symbol = _symbol; + maxDistributionPerSecondPerAsset = _maxDistributionPerSecondPerAsset; + timelockAddress = _timelockAddress; + + // initialize rewardsCycleEnd value + // NOTE: normally distribution of rewards should be done prior to _syncRewards but in this case we know there are no users or rewards yet. + _syncRewards(); + + // initialize lastRewardsDistribution value + _distributeRewards(); + } + + /// @notice The ```initializeRewardsCycleData``` function initializes the rewards cycle data + /// @dev This function can only be called once + /// @param _pricePerShare The price per share + /// @param _maxDistributionPerSecondPerAsset The maximum amount of rewards that can be distributed per second per 1e18 asset + /// @param _cycleEnd The end of the rewards cycle + /// @param _lastSync The last sync time + /// @param _rewardCycleAmount The reward cycle amount + function initializeRewardsCycleData( + uint256 _pricePerShare, + uint256 _maxDistributionPerSecondPerAsset, + uint40 _cycleEnd, + uint40 _lastSync, + uint216 _rewardCycleAmount + ) external { + if (initializeStage != 1) revert AlreadyInitialized(); + initializeStage++; + storedTotalAssets = (_pricePerShare * totalSupply) / PRECISION; + maxDistributionPerSecondPerAsset = _maxDistributionPerSecondPerAsset; + rewardsCycleData.cycleEnd = _cycleEnd; + rewardsCycleData.lastSync = _lastSync; + rewardsCycleData.rewardCycleAmount = _rewardCycleAmount; + } + + /// @notice The ```setMaxDistributionPerSecondPerAsset``` function sets the maxDistributionPerSecondPerAsset + /// @dev This function can only be called by the timelock, caps the value to type(uint64).max + /// @param _maxDistributionPerSecondPerAsset The maximum amount of rewards that can be distributed per second per 1e18 asset + function setMaxDistributionPerSecondPerAsset(uint256 _maxDistributionPerSecondPerAsset) external { + _requireSenderIsTimelock(); + syncRewardsAndDistribution(); + + // NOTE: prevents bricking the contract via overflow + if (_maxDistributionPerSecondPerAsset > type(uint64).max) { + _maxDistributionPerSecondPerAsset = type(uint64).max; + } + + emit SetMaxDistributionPerSecondPerAsset({ + oldMax: maxDistributionPerSecondPerAsset, + newMax: _maxDistributionPerSecondPerAsset + }); + + maxDistributionPerSecondPerAsset = _maxDistributionPerSecondPerAsset; + } + + /// @notice The ```calculateRewardsToDistribute``` function calculates the amount of rewards to distribute based on the rewards cycle data and the time passed + /// @param _rewardsCycleData The rewards cycle data + /// @param _deltaTime The time passed since the last rewards distribution + /// @return _rewardToDistribute The amount of rewards to distribute + function calculateRewardsToDistribute( + RewardsCycleData memory _rewardsCycleData, + uint256 _deltaTime + ) public view override returns (uint256 _rewardToDistribute) { + _rewardToDistribute = super.calculateRewardsToDistribute({ + _rewardsCycleData: _rewardsCycleData, + _deltaTime: _deltaTime + }); + + // Cap rewards + uint256 _maxDistribution = (maxDistributionPerSecondPerAsset * _deltaTime * storedTotalAssets) / PRECISION; + if (_rewardToDistribute > _maxDistribution) { + _rewardToDistribute = _maxDistribution; + } + } +} diff --git a/src/contracts/ethereum/sfrxUSD/SfrxUSD2.sol b/src/contracts/ethereum/sfrxUSD/versioning/SfrxUSD2.sol similarity index 100% rename from src/contracts/ethereum/sfrxUSD/SfrxUSD2.sol rename to src/contracts/ethereum/sfrxUSD/versioning/SfrxUSD2.sol