diff --git a/contracts/frxUsd/FrxUSDOFTUpgradeable.sol b/contracts/frxUsd/FrxUSDOFTUpgradeable.sol index 9e391e1a..4fbcb547 100644 --- a/contracts/frxUsd/FrxUSDOFTUpgradeable.sol +++ b/contracts/frxUsd/FrxUSDOFTUpgradeable.sol @@ -8,14 +8,15 @@ import { EIP3009Module } from "contracts/modules/EIP3009Module.sol"; import { PermitModule } from "contracts/modules/PermitModule.sol"; import { SendParam } from "@fraxfinance/layerzero-v2-upgradeable/oapp/contracts/oft/interfaces/IOFT.sol"; import { ERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import { MinterModule } from "contracts/modules/MinterModule.sol"; -contract FrxUSDOFTUpgradeable is OFTUpgradeable, EIP3009Module, PermitModule, FreezeThawModule, PauseModule { +contract FrxUSDOFTUpgradeable is OFTUpgradeable, EIP3009Module, PermitModule, FreezeThawModule, PauseModule, MinterModule { constructor(address _lzEndpoint) OFTUpgradeable(_lzEndpoint) { _disableInitializers(); } function version() public pure returns (string memory) { - return "1.1.0"; + return "1.2.0"; } /// @dev overrides state where previous OFT versions were named the legacy "FRAX" @@ -124,6 +125,35 @@ contract FrxUSDOFTUpgradeable is OFTUpgradeable, EIP3009Module, PermitModule, Fr _unpause(); } + /// @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 + /// @dev Added in v1.2.0 + function minter_mint(address m_address, uint256 m_amount) external onlyMinters { + _minter_mint(m_address, m_amount); + } + + /// @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) external onlyMinters { + _minter_burn_from(b_address, b_amount); + } + + /// @notice Adds a minter + /// @param minter_address Address of minter to add + /// @dev Added in v1.2.0 + function addMinter(address minter_address) external onlyOwner { + _addMinter(minter_address); + } + + /// @notice Removes a non-bridge minter + /// @param minter_address Address of minter to remove + /// @dev Added in v1.2.0 + function removeMinter(address minter_address) external onlyOwner { + _removeMinter(minter_address); + } + function _beforeTokenTransfer( address from, address to, @@ -177,6 +207,19 @@ contract FrxUSDOFTUpgradeable is OFTUpgradeable, EIP3009Module, PermitModule, Fr function _approve(address owner, address spender, uint256 amount) internal override(PermitModule, ERC20Upgradeable) { return ERC20Upgradeable._approve(owner, spender, amount); } + + /// @dev supports minter module + function _mint(address account, uint256 value) internal override(MinterModule, ERC20Upgradeable){ + ERC20Upgradeable._mint(account, value); + } + + function _spendAllowance(address owner, address spender, uint256 amount) internal override(MinterModule, ERC20Upgradeable) { + ERC20Upgradeable._spendAllowance(owner, spender, amount); + } + + function _burn(address account, uint256 amount) internal override(MinterModule, ERC20Upgradeable) { + ERC20Upgradeable._burn(account, amount); + } /* ========== ERRORS ========== */ error ArrayMisMatch(); diff --git a/contracts/modules/MinterModule.sol b/contracts/modules/MinterModule.sol new file mode 100644 index 00000000..1307b9a8 --- /dev/null +++ b/contracts/modules/MinterModule.sol @@ -0,0 +1,153 @@ +pragma solidity ^0.8.0; + +abstract contract MinterModule { + + //============================================================================== + // Storage + //============================================================================== + + struct MinterModuleStorage { + address[] minters_array; + mapping(address => bool) minters; + uint256 totalMinted; + uint256 totalBurned; + } + + // keccak256(abi.encode(uint256(keccak256("frax.storage.MinterModule")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant MinterModuleStorageLocation = 0x7a20b3b4fafc14b62295555dcdd80cd62ae312ce9abdfc5568be6a1913cbf700; + + function _getMinterModuleStorage() private pure returns(MinterModuleStorage storage $) { + assembly { + $.slot := MinterModuleStorageLocation + } + } + + /// @notice A modifier that only allows a minters to call + modifier onlyMinters() { + MinterModuleStorage storage $ = _getMinterModuleStorage(); + if (!$.minters[msg.sender]) revert OnlyMinter(); + _; + } + + /// @notice Adds a minter + /// @param minter_address Address of minter to add + function _addMinter(address minter_address) internal { + MinterModuleStorage storage $ = _getMinterModuleStorage(); + if ($.minters[minter_address]) revert AlreadyExists(); + $.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) internal { + MinterModuleStorage storage $ = _getMinterModuleStorage(); + if (!$.minters[minter_address]) revert AddressNonexistant(); + delete $.minters[minter_address]; + 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 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) internal { + MinterModuleStorage storage $ = _getMinterModuleStorage(); + _mint(m_address, m_amount); + $.totalMinted += m_amount; + emit TokenMinterMinted(msg.sender, m_address, m_amount); + } + + /// @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) internal { + MinterModuleStorage storage $ = _getMinterModuleStorage(); + _burnFrom(b_address, b_amount); + $.totalBurned += b_amount; + emit TokenMinterBurned(b_address, msg.sender, b_amount); + } + + /// @notice Destroys a `value` amount of tokens from `account`, deducting from + /// the caller's allowance. + /// @param account Account to burn tokens from + /// @param value the amount to burn from account caller must have allowance + function _burnFrom(address account, uint256 value) internal { + _spendAllowance(account, msg.sender, value); + _burn(account, value); + } + + + //============================================================================== + // Overridden methods + //============================================================================== + + function _mint(address,uint256) internal virtual; + + function _burn(address,uint256) internal virtual; + + function _spendAllowance(address,address,uint256) internal virtual; + + //============================================================================== + // Views + //============================================================================== + + function minters(address _address) external view returns(bool) { + MinterModuleStorage storage $ = _getMinterModuleStorage(); + return $.minters[_address]; + } + + function minters_array(uint256 _idx) external view returns(address) { + MinterModuleStorage storage $ = _getMinterModuleStorage(); + return $.minters_array[_idx]; + } + + function totalMinted() external view returns(uint256) { + MinterModuleStorage storage $ = _getMinterModuleStorage(); + return $.totalMinted; + } + + function totalBurned() external view returns(uint256) { + MinterModuleStorage storage $ = _getMinterModuleStorage(); + return $.totalBurned; + } + + //============================================================================== + // Events + //============================================================================== + + /// @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 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 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); + + //============================================================================== + // Errors + //============================================================================== + + error OnlyMinter(); + error ZeroAddress(); + error AlreadyExists(); + error AddressNonexistant(); +} \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index a6436b6a..fa8ea66c 100644 --- a/foundry.toml +++ b/foundry.toml @@ -31,3 +31,6 @@ remappings = [ via_ir = false optimizer = true optimizer_runs = 200 + +[lint] +lint_on_build = false \ No newline at end of file diff --git a/test/foundry/contracts/frxUsd/FrxUSDOFTUpgradeableTest.t.sol b/test/foundry/contracts/frxUsd/FrxUSDOFTUpgradeableTest.t.sol index 24950ccf..16811665 100644 --- a/test/foundry/contracts/frxUsd/FrxUSDOFTUpgradeableTest.t.sol +++ b/test/foundry/contracts/frxUsd/FrxUSDOFTUpgradeableTest.t.sol @@ -383,4 +383,93 @@ contract FrxUSDOFTUpgradeableTest is FraxTest { vm.expectRevert(); oft.burnMany(accounts, amounts); } + + + function test_onlyOwner_addMinter() external { + vm.expectRevert(bytes("Ownable: caller is not the owner")); + oft.addMinter(al); + } + + function test_onlyOwner_removeMinter() external { + vm.expectRevert(bytes("Ownable: caller is not the owner")); + oft.removeMinter(al); + } + + function test_canAddMinter() public { + vm.prank(oft.owner()); + oft.addMinter(al); + + assertEq({ + a: oft.minters(al), + b: true + }); + address _m = oft.minters_array(0); + assertEq({ + a: _m, + b: al + }); + } + + function test_minterCanMint() public { + test_canAddMinter(); + + uint tsBefore = oft.totalSupply(); + uint balUserBefore = oft.balanceOf(address(bob)); + vm.prank(al); + oft.minter_mint(address(bob), 100e18); + uint tsAfter = oft.totalSupply(); + uint balUserAfter = oft.balanceOf(address(bob)); + + assertEq({ + a: tsAfter - tsBefore, + b: 100e18 + }); + assertEq({ + a: balUserAfter - balUserBefore, + b: 100e18 + }); + assertEq({ + a: oft.totalMinted(), + b: 100e18 + }); + } + + function test_onlyMinterCanMint() external { + vm.expectRevert(bytes4(keccak256("OnlyMinter()"))); + vm.prank(al); + oft.minter_mint(address(0x39383928), 100e18); + } + + function test_onlyMinterCanBurn() external { + vm.expectRevert(bytes4(keccak256("OnlyMinter()"))); + vm.prank(al); + oft.minter_burn_from(address(0x39383928), 100e18); + } + + function test_minterCanBurn() external { + test_minterCanMint(); + vm.prank(bob); + oft.approve(al, 100e18); + + + uint tsBefore = oft.totalSupply(); + uint balBobBefore = oft.balanceOf(bob); + vm.prank(al); + oft.minter_burn_from(bob, 100e18); + uint tsAfter = oft.totalSupply(); + uint balBobAfter = oft.balanceOf(bob); + + assertEq({ + a: tsBefore - tsAfter, + b: 100e18 + }); + assertEq({ + a: balBobBefore - balBobAfter, + b: 100e18 + }); + assertEq({ + a: oft.totalBurned(), + b: 100e18 + }); + } }