Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 45 additions & 2 deletions contracts/frxUsd/FrxUSDOFTUpgradeable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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();
Expand Down
153 changes: 153 additions & 0 deletions contracts/modules/MinterModule.sol
Original file line number Diff line number Diff line change
@@ -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();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: add zero-address sanity checks similar to Fraxtal's frxUSD.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$.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();
}
3 changes: 3 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,6 @@ remappings = [
via_ir = false
optimizer = true
optimizer_runs = 200

[lint]
lint_on_build = false
89 changes: 89 additions & 0 deletions test/foundry/contracts/frxUsd/FrxUSDOFTUpgradeableTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
}
}