frax.sol/frxusd.sol
311 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);
}
}