frax.sol/frxusd.sol

Created Diff never expires
283 removals
Lines
Total
Removed
Words
Total
Removed
To continue using this feature, upgrade to
Diffchecker logo
Diffchecker Pro
311 lines
111 additions
Lines
Total
Added
Words
Total
Added
To continue using this feature, upgrade to
Diffchecker logo
Diffchecker Pro
143 lines
// SPDX-License-Identifier: MIT
//SPDX-License-Identifier: Unlicense
pragma solidity 0.6.11;
pragma solidity ^0.8.0;
pragma experimental ABIEncoderV2;

import "./Context.sol";
import "./IERC20.sol";
import "./ERC20Custom.sol";
import "./ERC20.sol";
import "./SafeMath.sol";
import "./FXS.sol";
import "./FraxPool.sol";
import "./UniswapPairOracle.sol";
import "./ChainlinkETHUSDPriceConsumer.sol";
import "./AccessControl.sol";

contract FRAXStablecoin is ERC20Custom, AccessControl {
using SafeMath for uint256;

/* ========== STATE VARIABLES ========== */
enum PriceChoice { FRAX, FXS }
ChainlinkETHUSDPriceConsumer private eth_usd_pricer;
uint8 private eth_usd_pricer_decimals;
UniswapPairOracle private fraxEthOracle;
UniswapPairOracle private fxsEthOracle;
string public symbol;
string public name;
uint8 public constant decimals = 18;
address public owner_address;
address public creator_address;
address public timelock_address; // Governance timelock address
address public controller_address; // Controller contract to dynamically adjust system parameters automatically
address public fxs_address;
address public frax_eth_oracle_address;
address public fxs_eth_oracle_address;
address public weth_address;
address public eth_usd_consumer_address;
uint256 public constant genesis_supply = 2000000e18; // 2M FRAX (only for testing, genesis supply will be 5k on Mainnet). This is to help with establishing the Uniswap pools, as they need liquidity

// The addresses in this array are added by the oracle and these contracts are able to mint frax
address[] public frax_pools_array;

// Mapping is also used for faster verification
mapping(address => bool) public frax_pools;

// Constants for various precisions
uint256 private constant PRICE_PRECISION = 1e6;
uint256 public global_collateral_ratio; // 6 decimals of precision, e.g. 924102 = 0.924102
uint256 public redemption_fee; // 6 decimals of precision, divide by 1000000 in calculations for fee
uint256 public minting_fee; // 6 decimals of precision, divide by 1000000 in calculations for fee
uint256 public frax_step; // Amount to change the collateralization ratio by upon refreshCollateralRatio()
uint256 public refresh_cooldown; // Seconds to wait before being able to run refreshCollateralRatio() again
uint256 public price_target; // The price of FRAX at which the collateral ratio will respond to; this value is only used for the collateral ratio mechanism and not for minting and redeeming which are hardcoded at $1
uint256 public price_band; // The bound above and below the price target at which the refreshCollateralRatio() will not change the collateral ratio

address public DEFAULT_ADMIN_ADDRESS;
bytes32 public constant COLLATERAL_RATIO_PAUSER = keccak256("COLLATERAL_RATIO_PAUSER");
bool public collateral_ratio_paused = false;

/* ========== MODIFIERS ========== */


modifier onlyCollateralRatioPauser() {
import { ERC20Permit, ERC20 } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
require(hasRole(COLLATERAL_RATIO_PAUSER, msg.sender));
import { ERC20Burnable } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
_;
import { Ownable2Step } from "@openzeppelin/contracts/access/Ownable2Step.sol";
}
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { StorageSlot } from "@openzeppelin/contracts/utils/StorageSlot.sol";


modifier onlyPools() {
/// @title FrxUSD
require(frax_pools[msg.sender] == true, "Only frax pools can call this function");
/**
_;
* @notice Combines Openzeppelin's ERC20Permit, ERC20Burnable and Ownable2Step.
}
* Also includes a list of authorized minters
*/
modifier onlyByOwnerOrGovernance() {
/// @dev FrxUSD adheres to EIP-712/EIP-2612 and can use permits
require(msg.sender == owner_address || msg.sender == timelock_address || msg.sender == controller_address, "You are not the owner, controller, or the governance timelock");
contract FrxUSD is
_;
ERC20Permit,
}
ERC20Burnable,
Ownable2Step
{
/// @notice Array of the non-bridge minters
address[] public minters_array;


modifier onlyByOwnerGovernanceOrPool() {
/// @notice Mapping of the minters
require(
/// @dev Mapping is used for faster verification
msg.sender == owner_address
mapping(address => bool) public minters;
|| msg.sender == timelock_address
|| frax_pools[msg.sender] == true,
"You are not the owner, the governance timelock, or a pool");
_;
}


/* ========== CONSTRUCTOR ========== */
/* ========== CONSTRUCTOR ========== */

/// @param _ownerAddress The initial owner
/// @param _name ERC20 name
/// @param _symbol ERC20 symbol
constructor(
constructor(
address _ownerAddress,
string memory _name,
string memory _name,
string memory _symbol,
string memory _symbol
address _creator_address,
) ERC20(_name, _symbol) ERC20Permit(_name) Ownable(_ownerAddress) {
address _timelock_address
) public {
name = _name;
symbol = _symbol;
creator_address = _creator_address;
timelock_address = _timelock_address;
_setupRole(DEFAULT_ADMIN_ROLE, _msgSender());
DEFAULT_ADMIN_ADDRESS = _msgSender();
owner_address = _creator_address;
_mint(creator_address, genesis_supply);
grantRole(COLLATERAL_RATIO_PAUSER, creator_address);
grantRole(COLLATERAL_RATIO_PAUSER, timelock_address);
frax_step = 2500; // 6 decimals of precision, equal to 0.25%
global_collateral_ratio = 1000000; // Frax system starts off fully collateralized (6 decimals of precision)
refresh_cooldown = 3600; // Refresh cooldown period is set to 1 hour (3600 seconds) at genesis
price_target = 1000000; // Collateral ratio will adjust according to the $1 price target at genesis
price_band = 5000; // Collateral ratio will not adjust if between $0.995 and $1.005 at genesis
}
}


/* ========== VIEWS ========== */
/* ========== INITIALIZER ========== */

/// @dev Used to initialize the contract when it is behind a proxy
// Choice = 'FRAX' or 'FXS' for now
function initialize(
function oracle_price(PriceChoice choice) internal view returns (uint256) {
address _owner,
// Get the ETH / USD price first, and cut it down to 1e6 precision
string memory _name,
uint256 eth_usd_price = uint256(eth_usd_pricer.getLatestPrice()).mul(PRICE_PRECISION).div(uint256(10) ** eth_usd_pricer_decimals);
string memory _symbol
uint256 price_vs_eth;
) public {

require(owner() == address(0), "Already initialized");
if (choice == PriceChoice.FRAX) {
_transferOwnership(_owner);
price_vs_eth = uint256(fraxEthOracle.consult(weth_address, PRICE_PRECISION)); // How much FRAX if you put in PRICE_PRECISION WETH
StorageSlot.getBytesSlot(bytes32(uint256(3))).value = bytes(_name);
}
StorageSlot.getBytesSlot(bytes32(uint256(4))).value = bytes(_symbol);
else if (choice == PriceChoice.FXS) {
price_vs_eth = uint256(fxsEthOracle.consult(weth_address, PRICE_PRECISION)); // How much FXS if you put in PRICE_PRECISION WETH
}
else revert("INVALID PRICE CHOICE. Needs to be either 0 (FRAX) or 1 (FXS)");

// Will be in 1e6 format
return eth_usd_price.mul(PRICE_PRECISION).div(price_vs_eth);
}
}


// Returns X FRAX = 1 USD
/* ========== MODIFIERS ========== */
function frax_price() public view returns (uint256) {
return oracle_price(PriceChoice.FRAX);
}


// Returns X FXS = 1 USD
/// @notice A modifier that only allows a minters to call
function fxs_price() public view returns (uint256) {
modifier onlyMinters() {
return oracle_price(PriceChoice.FXS);
require(minters[msg.sender] == true, "Only minters");
_;
}
}


function eth_usd_price() public view returns (uint256) {
/* ========== RESTRICTED FUNCTIONS [MINTERS] ========== */
return uint256(eth_usd_pricer.getLatestPrice()).mul(PRICE_PRECISION).div(uint256(10) ** eth_usd_pricer_decimals);
}


// This is needed to avoid costly repeat calls to different getter functions
/// @notice Used by minters to burn tokens
// It is cheaper gas-wise to just dump everything and only use some of the info
/// @param b_address Address of the account to burn from
function frax_info() public view returns (uint256, uint256, uint256, uint256, uint256, uint256, uint256, uint256) {
/// @param b_amount Amount of tokens to burn
return (
function minter_burn_from(address b_address, uint256 b_amount) public onlyMinters {
oracle_price(PriceChoice.FRAX), // frax_price()
super.burnFrom(b_address, b_amount);
oracle_price(PriceChoice.FXS), // fxs_price()
emit TokenMinterBurned(b_address, msg.sender, b_amount);
totalSupply(), // totalSupply()
global_collateral_ratio, // global_collateral_ratio()
globalCollateralValue(), // globalCollateralValue
minting_fee, // minting_fee()
redemption_fee, // redemption_fee()
uint256(eth_usd_pricer.getLatestPrice()).mul(PRICE_PRECISION).div(uint256(10) ** eth_usd_pricer_decimals) //eth_usd_price
);
}
}


// Iterate through all frax pools and calculate all value of collateral in all pools globally
/// @notice Used by minters to mint new tokens
function globalCollateralValue() public view returns (uint256) {
/// @param m_address Address of the account to mint to
uint256 total_collateral_value_d18 = 0;
/// @param m_amount Amount of tokens to mint

function minter_mint(address m_address, uint256 m_amount) public onlyMinters {
for (uint i = 0; i < frax_pools_array.length; i++){
super._mint(m_address, m_amount);
// Exclude null addresses
emit TokenMinterMinted(msg.sender, m_address, m_amount);
if (frax_pools_array[i] != address(0)){
total_collateral_value_d18 = total_collateral_value_d18.add(FraxPool(frax_pools_array[i]).collatDollarBalance());
}

}
return total_collateral_value_d18;
}
}


/* ========== PUBLIC FUNCTIONS ========== */
// There needs to be a time interval that this can be called. Otherwise it can be called multiple times per expansion.
uint256 public last_call_time; // Last time the refreshCollateralRatio function was called
function refreshCollateralRatio() public {
require(collateral_ratio_paused == false, "Collateral Ratio has been paused");
uint256 frax_price_cur = frax_price();
require(block.timestamp - last_call_time >= refresh_cooldown, "Must wait for the refresh cooldown since last refresh");

// Step increments are 0.25% (upon genesis, changable by setFraxStep())
if (frax_price_cur > price_target.add(price_band)) { //decrease collateral ratio
if(global_collateral_ratio <= frax_step){ //if within a step of 0, go to 0
global_collateral_ratio = 0;
} else {
global_collateral_ratio = global_collateral_ratio.sub(frax_step);
}
} else if (frax_price_cur < price_target.sub(price_band)) { //increase collateral ratio
if(global_collateral_ratio.add(frax_step) >= 1000000){
global_collateral_ratio = 1000000; // cap collateral ratio at 1.000000
} else {
global_collateral_ratio = global_collateral_ratio.add(frax_step);
}
}

last_call_time = block.timestamp; // Set the time of the last expansion
}


/* ========== RESTRICTED FUNCTIONS ========== */
/* ========== 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");


// Used by pools when user redeems
require(minters[minter_address] == false, "Address already exists");
function pool_burn_from(address b_address, uint256 b_amount) public onlyPools {
minters[minter_address] = true;
super._burnFrom(b_address, b_amount);
minters_array.push(minter_address);
emit FRAXBurned(b_address, msg.sender, b_amount);
}


// This function is what other frax pools will call to mint new FRAX
emit MinterAdded(minter_address);
function pool_mint(address m_address, uint256 m_amount) public onlyPools {
super._mint(m_address, m_amount);
emit FRAXMinted(msg.sender, m_address, m_amount);
}
}


// Adds collateral addresses supported, such as tether and busd, must be ERC20
/// @notice Removes a non-bridge minter
function addPool(address pool_address) public onlyByOwnerOrGovernance {
/// @param minter_address Address of minter to remove
require(frax_pools[pool_address] == false, "address already exists");
function removeMinter(address minter_address) public onlyOwner {
frax_pools[pool_address] = true;
require(minter_address != address(0), "Zero address detected");
frax_pools_array.push(pool_address);
require(minters[minter_address] == true, "Address nonexistant");
}


// Remove a pool
function removePool(address pool_address) public onlyByOwnerOrGovernance {
require(frax_pools[pool_address] == true, "address doesn't exist already");
// Delete from the mapping
// Delete from the mapping
delete frax_pools[pool_address];
delete minters[minter_address];


// 'Delete' from the array by setting the address to 0x0
// 'Delete' from the array by setting the address to 0x0
for (uint i = 0; i < frax_pools_array.length; i++){
for (uint256 i = 0; i < minters_array.length; i++) {
if (frax_pools_array[i] == pool_address) {
if (minters_array[i] == minter_address) {
frax_pools_array[i] = address(0); // This will leave a null in the array and keep the indices the same
minters_array[i] = address(0); // This will leave a null in the array and keep the indices the same
break;
break;
}
}
}
}
}

function setOwner(address _owner_address) external onlyByOwnerOrGovernance {
owner_address = _owner_address;
}

function setRedemptionFee(uint256 red_fee) public onlyByOwnerOrGovernance {
redemption_fee = red_fee;
}

function setMintingFee(uint256 min_fee) public onlyByOwnerOrGovernance {
minting_fee = min_fee;
}

function setFraxStep(uint256 _new_step) public onlyByOwnerOrGovernance {
frax_step = _new_step;
}

function setPriceTarget (uint256 _new_price_target) public onlyByOwnerOrGovernance {
price_target = _new_price_target;
}

function setRefreshCooldown(uint256 _new_cooldown) public onlyByOwnerOrGovernance {
refresh_cooldown = _new_cooldown;
}

function setFXSAddress(address _fxs_address) public onlyByOwnerOrGovernance {
fxs_address = _fxs_address;
}

function setETHUSDOracle(address _eth_usd_consumer_address) public onlyByOwnerOrGovernance {
eth_usd_consumer_address = _eth_usd_consumer_address;
eth_usd_pricer = ChainlinkETHUSDPriceConsumer(eth_usd_consumer_address);
eth_usd_pricer_decimals = eth_usd_pricer.getDecimals();
}

function setTimelock(address new_timelock) external onlyByOwnerOrGovernance {
timelock_address = new_timelock;
}

function setController(address _controller_address) external onlyByOwnerOrGovernance {
controller_address = _controller_address;
}

function setPriceBand(uint256 _price_band) external onlyByOwnerOrGovernance {
price_band = _price_band;
}

// Sets the FRAX_ETH Uniswap oracle address
function setFRAXEthOracle(address _frax_oracle_addr, address _weth_address) public onlyByOwnerOrGovernance {
frax_eth_oracle_address = _frax_oracle_addr;
fraxEthOracle = UniswapPairOracle(_frax_oracle_addr);
weth_address = _weth_address;
}

// Sets the FXS_ETH Uniswap oracle address
function setFXSEthOracle(address _fxs_oracle_addr, address _weth_address) public onlyByOwnerOrGovernance {
fxs_eth_oracle_address = _fxs_oracle_addr;
fxsEthOracle = UniswapPairOracle(_fxs_oracle_addr);
weth_address = _weth_address;
}


function toggleCollateralRatio() public onlyCollateralRatioPauser {
emit MinterRemoved(minter_address);
collateral_ratio_paused = !collateral_ratio_paused;
}
}


/* ========== EVENTS ========== */
/* ========== EVENTS ========== */


// Track FRAX burned
/// @notice Emitted whenever the bridge burns tokens from an account
event FRAXBurned(address indexed from, address indexed to, uint256 amount);
/// @param account Address of the account tokens are being burned from
/// @param amount Amount of tokens burned
event Burn(address indexed account, uint256 amount);


// Track FRAX minted
/// @notice Emitted whenever the bridge mints tokens to an account
event FRAXMinted(address indexed from, address indexed to, uint256 amount);
/// @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);
}
}