diff --git a/src/contracts/ethereum/frxUSD/versioning/FrxUSD2.sol b/src/contracts/ethereum/frxUSD/versioning/FrxUSD2.sol index 9bef61c..641f11b 100644 --- a/src/contracts/ethereum/frxUSD/versioning/FrxUSD2.sol +++ b/src/contracts/ethereum/frxUSD/versioning/FrxUSD2.sol @@ -27,6 +27,10 @@ contract FrxUSD2 is ERC20Permit, ERC20Burnable, Ownable2Step { /// @notice Whether or not the contract is paused bool public isPaused; + mapping(address => bool) public isFreezer; + + uint256[45] private __gap; + /* ========== CONSTRUCTOR ========== */ /// @param _ownerAddress The initial owner /// @param _name ERC20 name @@ -106,6 +110,18 @@ contract FrxUSD2 is ERC20Permit, ERC20Burnable, Ownable2Step { emit MinterRemoved(minter_address); } + function addFreezer(address _freezer) external onlyOwner { + if (isFreezer[_freezer]) revert AlreadyFreezer(); + isFreezer[_freezer] = true; + emit AddFreezer(_freezer); + } + + function removeFreezer(address _freezer) external onlyOwner { + if (!isFreezer[_freezer]) revert NotFreezer(); + isFreezer[_freezer] = false; + emit RemoveFreezer(_freezer); + } + /// @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 { @@ -123,7 +139,8 @@ contract FrxUSD2 is ERC20Permit, ERC20Burnable, Ownable2Step { /// @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 { + function freezeMany(address[] memory _owners) external { + if (!isFreezer[msg.sender] && msg.sender != owner()) revert NotFreezer(); uint256 len = _owners.length; for (uint256 i; i < len; ++i) { _freeze(_owners[i]); @@ -131,8 +148,10 @@ contract FrxUSD2 is ERC20Permit, ERC20Burnable, Ownable2Step { } /// @notice External admin gated function to freeze a given account - /// @param _owner The account to be - function freeze(address _owner) external onlyOwner { + /// @param _owner The account to be frozen + function freeze(address _owner) external { + if (!isFreezer[msg.sender] && msg.sender != owner()) revert NotFreezer(); + _freeze(_owner); } @@ -248,9 +267,19 @@ contract FrxUSD2 is ERC20Permit, ERC20Burnable, Ownable2Step { /// @param account The account being thawed event AccountThawed(address account); + /// @notice Event Emitted when an address is added as a freezer + /// @param account The account being added as a freezer + event AddFreezer(address account); + + /// @notice Event Emitted when an address is removed as a freezer + /// @param account The account being removed as a freezer + event RemoveFreezer(address account); + /* ========== ERRORS ========== */ error ArrayMisMatch(); error IsPaused(); error IsFrozen(); + error NotFreezer(); + error AlreadyFreezer(); error OwnerCannotInitToZeroAddress(); } diff --git a/src/contracts/fraxtal/frxUSD/versioning/FrxUSD2.sol b/src/contracts/fraxtal/frxUSD/versioning/FrxUSD2.sol new file mode 100644 index 0000000..e7e7fd7 --- /dev/null +++ b/src/contracts/fraxtal/frxUSD/versioning/FrxUSD2.sol @@ -0,0 +1,183 @@ +pragma solidity ^0.8.0; + +import { ERC20PermitPermissionedOptiMintable } from "src/contracts/fraxtal/shared/ERC20PermitPermissionedOptiMintable.sol"; + +contract FrxUSD2 is ERC20PermitPermissionedOptiMintable { + /// @notice Mapping indicating which addresses are frozen + mapping(address => bool) public isFrozen; + + /// @notice Whether or not the contract is paused + bool public isPaused; + + /// @notice Mapping indicating which addresses can freeze accounts + mapping(address => bool) public isFreezer; + + uint256[47] private __gap; + + /// @notice Upgrade version of the contract + /// @dev Does not impact EIP712 version, which is automatically set to "1" in constructor + function version() public pure virtual override returns (string memory) { + return "2.0.1"; + } + + /// @param _creator_address The contract creator + /// @param _timelock_address The timelock + /// @param _bridge Address of the L2 standard bridge + /// @param _remoteToken Address of the corresponding L1 token + constructor( + address _creator_address, + address _timelock_address, + address _bridge, + address _remoteToken + ) + ERC20PermitPermissionedOptiMintable( + _creator_address, + _timelock_address, + _bridge, + _remoteToken, + "Frax USD", + "frxUSD" + ) + {} + + function addFreezer(address _freezer) external onlyOwner { + if (isFreezer[_freezer]) revert AlreadyFreezer(); + isFreezer[_freezer] = true; + emit AddFreezer(_freezer); + } + + function removeFreezer(address _freezer) external onlyOwner { + if (!isFreezer[_freezer]) revert NotFreezer(); + isFreezer[_freezer] = false; + emit RemoveFreezer(_freezer); + } + + /// @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 { + if (!isFreezer[msg.sender] && msg.sender != owner) revert NotFreezer(); + 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 frozen + function freeze(address _owner) external { + if (!isFreezer[msg.sender] && msg.sender != owner) revert NotFreezer(); + _freeze(_owner); + } + + /// @notice External admin gated function to batch burn balance from a set of accounts + /// @param _owners Array of accounts whose balances will be burned + /// @param _amounts Array of amounts corresponding to the balances to be burned + /// @dev if `_amount` == 0, entire balance will be burned + function burnMany(address[] memory _owners, uint256[] memory _amounts) external onlyOwner { + uint256 lenOwner = _owners.length; + if (_owners.length != _amounts.length) revert ArrayMisMatch(); + for (uint256 i; i < lenOwner; ++i) { + if (_amounts[i] == 0) _amounts[i] = balanceOf(_owners[i]); + _burn(_owners[i], _amounts[i]); + } + } + + /// @notice External admin gated function to burn balance from a given account + /// @param _owner The account whose balance will be burned + /// @param _amount The amount of balance to burn + /// @dev if `_amount` == 0, entire balance will be burned + function burnFrxUsd(address _owner, uint256 _amount) external onlyOwner { + if (_amount == 0) _amount = balanceOf(_owner); + _burn(_owner, _amount); + } + + /// @notice External admin gated pause function + function pause() external onlyOwner { + isPaused = true; + emit Paused(); + } + + /// @notice External admin gated unpause function + function unpause() external onlyOwner { + isPaused = false; + emit Unpaused(); + } + + /* ========== Internals For Admin Gated ========== */ + + /// @notice Internal helper function to freeze an account + /// @param _owner The account to freeze + function _freeze(address _owner) internal { + isFrozen[_owner] = true; + emit AccountFrozen(_owner); + } + + /// @notice Internal helper function to unfreeze an account + /// @param _owner The account to unfreeze + function _thaw(address _owner) internal { + isFrozen[_owner] = false; + emit AccountThawed(_owner); + } + + /* ========== Overrides ========== */ + + /// @notice override for base internal `_update(address,address,uint256)` + /// implements `paused` and `frozen` transfer logic + /// @param from The address from which balance is originating + /// @param to The address whose balance will be incremented + /// @param value The amount to increment/decrement the balances of + /// @dev Owner can bypass pause and freeze checks + function _update(address from, address to, uint256 value) internal override { + if (msg.sender != owner) { + if (isPaused) revert IsPaused(); + if (isFrozen[to] || isFrozen[from] || isFrozen[msg.sender]) revert IsFrozen(); + } + super._update(from, to, value); + } + + /* ========== EVENTS ========== */ + + /// @notice Event Emitted when the contract is paused + event Paused(); + + /// @notice Event Emitted when the contract is unpaused + event Unpaused(); + + /// @notice Event Emitted when an address is frozen + /// @param account The account being frozen + event AccountFrozen(address account); + + /// @notice Event Emitted when an address is unfrozen + /// @param account The account being thawed + event AccountThawed(address account); + + /// @notice Event Emitted when an address is added as a freezer + /// @param account The account being added as a freezer + event AddFreezer(address account); + + /// @notice Event Emitted when an address is removed as a freezer + /// @param account The account being removed as a freezer + event RemoveFreezer(address account); + + /* ========== ERRORS ========== */ + error ArrayMisMatch(); + error IsPaused(); + error IsFrozen(); + error NotFreezer(); + error AlreadyFreezer(); +}