Description:
Multi-signature wallet contract requiring multiple confirmations for transaction execution.
Blockchain: Ethereum
Source Code: View Code On The Blockchain
Solidity Source Code:
{{
"language": "Solidity",
"sources": {
"src/policies/EmissionManager.sol": {
"content": "// SPDX-License-Identifier: AGPL-3.0
/// forge-lint: disable-start(mixed-case-function, mixed-case-variable, screaming-snake-case-immutable)
pragma solidity >=0.8.15;
// Libraries
import {ERC20} from "@solmate-6.2.0/tokens/ERC20.sol";
import {ERC4626} from "@solmate-6.2.0/mixins/ERC4626.sol";
import {TransferHelper} from "src/libraries/TransferHelper.sol";
import {FullMath} from "src/libraries/FullMath.sol";
// Interfaces
import {IBondSDA} from "src/interfaces/IBondSDA.sol";
import {IgOHM} from "src/interfaces/IgOHM.sol";
import {IEmissionManager} from "src/policies/interfaces/IEmissionManager.sol";
import {IConvertibleDepositAuctioneer} from "src/policies/interfaces/deposits/IConvertibleDepositAuctioneer.sol";
import {IGenericClearinghouse} from "src/policies/interfaces/IGenericClearinghouse.sol";
import {IPeriodicTask} from "src/interfaces/IPeriodicTask.sol";
import {IERC165} from "@openzeppelin-4.8.0/interfaces/IERC165.sol";
import {IEnabler} from "src/periphery/interfaces/IEnabler.sol";
// Bophades
import {Kernel, Keycode, Permissions, Policy, toKeycode} from "src/Kernel.sol";
import {ROLESv1} from "src/modules/ROLES/OlympusRoles.sol";
import {TRSRYv1} from "src/modules/TRSRY/TRSRY.v1.sol";
import {PRICEv1} from "src/modules/PRICE/PRICE.v1.sol";
import {MINTRv1} from "src/modules/MINTR/MINTR.v1.sol";
import {CHREGv1} from "src/modules/CHREG/CHREG.v1.sol";
import {PolicyEnabler} from "src/policies/utils/PolicyEnabler.sol";
// solhint-disable max-states-count
contract EmissionManager is IEmissionManager, IPeriodicTask, Policy, PolicyEnabler {
using FullMath for uint256;
using TransferHelper for ERC20;
// ========== CONSTANTS ========== //
uint256 internal constant ONE_HUNDRED_PERCENT = 1e18;
uint256 internal constant MAX_BOND_MARKET_CAPACITY_SCALAR = 2e18;
/// @notice The role assigned to the Heart contract.
/// This enables the Heart contract to call specific functions on this contract.
bytes32 public constant ROLE_HEART = "heart";
/// @notice The length of the `EnableParams` struct in bytes
uint256 internal constant ENABLE_PARAMS_LENGTH = 224;
// ========== STATE VARIABLES ========== //
/// @notice active base emissions rate change information
/// @dev active until daysLeft is 0
BaseRateChange public rateChange;
// Modules
TRSRYv1 public TRSRY;
PRICEv1 public PRICE;
MINTRv1 public MINTR;
CHREGv1 public CHREG;
// Tokens
// solhint-disable immutable-vars-naming
ERC20 public immutable ohm;
IgOHM public immutable gohm;
ERC20 public immutable reserve;
ERC4626 public immutable sReserve;
// solhint-enable immutable-vars-naming
// External contracts
IBondSDA public bondAuctioneer;
address public teller;
IConvertibleDepositAuctioneer public cdAuctioneer;
// Manager variables
/// @notice The base emission rate, in OHM scale.
/// @dev e.g. 2e5 = 0.02%
uint256 public baseEmissionRate;
/// @notice The minimum premium for bond markets created by the manager, in terms of ONE_HUNDRED_PERCENT.
/// @dev A minimum premium of 1e18 would require the market price to be 100% above the backing price (i.e. double).
uint256 public minimumPremium;
/// @notice The vesting period for bond markets created by the manager, in seconds.
/// @dev Initialized at 0, which means no vesting.
uint48 public vestingPeriod;
/// @notice The backed price of OHM, in reserve scale.
uint256 public backing;
/// @notice Used to track the number of beats that have occurred.
uint8 public beatCounter;
/// @notice The ID of the active bond market (or 0)
uint256 public activeMarketId;
/// @notice The fixed tick size for CD auctions, in OHM scale (9 decimals)
uint256 public tickSize;
/// @notice The multiplier applied to the price, in terms of ONE_HUNDRED_PERCENT
/// @dev The value must be greater than or equal to ONE_HUNDRED_PERCENT (100%)
uint256 public minPriceScalar;
/// @notice The multiplier applied to bond market capacity from auction remainders, in terms of ONE_HUNDRED_PERCENT
/// @dev The value must be between 0 and MAX_BOND_MARKET_CAPACITY_SCALAR (0-200%)
uint256 public bondMarketCapacityScalar;
uint8 internal _oracleDecimals;
// solhint-disable immutable-vars-naming
uint8 internal immutable _ohmDecimals;
uint8 internal immutable _gohmDecimals;
uint8 internal immutable _reserveDecimals;
// solhint-enable immutable-vars-naming
/// @notice timestamp of last shutdown
uint48 public shutdownTimestamp;
/// @notice time in seconds that the manager needs to be restarted after a shutdown, otherwise it must be re-initialized
uint48 public restartTimeframe;
/// @notice In situations where a bond market cannot be created, this variable is used to record the OHM capacity for the bond market that needs to be created
uint256 public bondMarketPendingCapacity;
// ========== SETUP ========== //
constructor(
Kernel kernel_,
address ohm_,
address gohm_,
address reserve_,
address sReserve_,
address bondAuctioneer_,
address cdAuctioneer_,
address teller_
) Policy(kernel_) {
// Set immutable variables
if (ohm_ == address(0)) revert InvalidParam("OHM address cannot be 0");
if (gohm_ == address(0)) revert InvalidParam("gOHM address cannot be 0");
if (reserve_ == address(0)) revert InvalidParam("DAI address cannot be 0");
if (sReserve_ == address(0)) revert InvalidParam("sDAI address cannot be 0");
if (bondAuctioneer_ == address(0))
revert InvalidParam("Bond Auctioneer address cannot be 0");
if (teller_ == address(0)) revert InvalidParam("Bond Teller address cannot be 0");
if (cdAuctioneer_ == address(0)) revert InvalidParam("CD Auctioneer address cannot be 0");
// Validate that the auctioneer implements the IEnabler interface
if (!IERC165(cdAuctioneer_).supportsInterface(type(IEnabler).interfaceId))
revert InvalidParam("CD Auctioneer does not implement IEnabler");
ohm = ERC20(ohm_);
gohm = IgOHM(gohm_);
reserve = ERC20(reserve_);
sReserve = ERC4626(sReserve_);
bondAuctioneer = IBondSDA(bondAuctioneer_);
cdAuctioneer = IConvertibleDepositAuctioneer(cdAuctioneer_);
teller = teller_;
_ohmDecimals = ohm.decimals();
_gohmDecimals = ERC20(gohm_).decimals();
_reserveDecimals = reserve.decimals();
// Max approve sReserve contract for reserve for deposits
reserve.safeApprove(address(sReserve), type(uint256).max);
// Validate that the CDAuctioneer is configured for the reserve asset
if (address(cdAuctioneer.getDepositAsset()) != address(reserve_))
revert InvalidParam("CD Auctioneer not configured for reserve");
// Emit events
emit BondContractsSet(bondAuctioneer_, teller_);
emit ConvertibleDepositAuctioneerSet(cdAuctioneer_);
// PolicyEnabler disables the policy by default
}
/// @inheritdoc Policy
function configureDependencies() external override returns (Keycode[] memory dependencies) {
dependencies = new Keycode[](5);
dependencies[0] = toKeycode("TRSRY");
dependencies[1] = toKeycode("PRICE");
dependencies[2] = toKeycode("MINTR");
dependencies[3] = toKeycode("CHREG");
dependencies[4] = toKeycode("ROLES");
TRSRY = TRSRYv1(getModuleAddress(dependencies[0]));
PRICE = PRICEv1(getModuleAddress(dependencies[1]));
MINTR = MINTRv1(getModuleAddress(dependencies[2]));
CHREG = CHREGv1(getModuleAddress(dependencies[3]));
ROLES = ROLESv1(getModuleAddress(dependencies[4]));
_oracleDecimals = PRICE.decimals();
return dependencies;
}
/// @inheritdoc Policy
function requestPermissions()
external
view
override
returns (Permissions[] memory permissions)
{
Keycode mintrKeycode = toKeycode("MINTR");
permissions = new Permissions[](2);
permissions[0] = Permissions({
keycode: mintrKeycode,
funcSelector: MINTR.increaseMintApproval.selector
});
permissions[1] = Permissions({keycode: mintrKeycode, funcSelector: MINTR.mintOhm.selector});
return permissions;
}
function VERSION() external pure returns (uint8 major, uint8 minor) {
major = 1;
minor = 2;
return (major, minor);
}
// ========== PERIODIC TASK ========== //
/// @inheritdoc IPeriodicTask
/// @dev This function performs the following:
/// - Adjusts the beat counter
/// - Exits if the beat counter is not 0
/// - Sets the parameters for the auction
/// - If the auction tracking period has finished and there is a deficit of OHM sold, attempts to create a bond market
/// - If market creation fails (external dependency), emits BondMarketCreationFailed and continues execution
///
/// Notes:
/// - If the CD auction is not running (e.g. the auctioneer contract is disabled), this function will consider OHM to have been under-sold across the auction tracking period. This will result in a bond market being created at the end of the auction tracking period in an attempt to sell the remaining OHM.
/// - If there are delays in the heartbeat (which calls this function), auction result tracking will be affected.
function execute() external onlyRole(ROLE_HEART) {
// Don't do anything if disabled
if (!isEnabled) return;
beatCounter = ++beatCounter % 3;
if (beatCounter != 0) return;
if (rateChange.daysLeft != 0) {
--rateChange.daysLeft;
if (rateChange.addition) baseEmissionRate += rateChange.changeBy;
else baseEmissionRate -= rateChange.changeBy;
}
// It then calculates the amount to sell for the coming day
(, , uint256 emission) = getNextEmission();
// Calculate tick size for the emission
uint256 calculatedTickSize = getSizeFor(emission);
// If emission is below the tick size threshold, disable the auction by setting target to 0
// This prevents auction rounds that would be too small to be economically viable, or would have a price that increases too rapidly
if (calculatedTickSize == 0) {
emission = 0;
}
// Update the parameters for the convertible deposit auction
// This call can revert, however it is mitigated by:
// - CDAuctioneer will accept a target (emission) of 0 (disables auction)
// - If the tick size is 0, the target will be set to 0 due to the check above
//
// Reverts are expected in the following scenarios:
// - If the target is > 0 (auction active) and minPrice is 0, the call will revert (which we would want)
cdAuctioneer.setAuctionParameters(
emission,
calculatedTickSize,
getMinPriceFor(_getCurrentPrice())
);
// If the tracking period is complete, determine if there was under-selling of OHM
// This only applies if the auctioneer is enabled. Otherwise, it would create a bond market at every third heartbeat (as the auction results index is not incremented while the auctioneer is disabled)
if (
IEnabler(address(cdAuctioneer)).isEnabled() &&
cdAuctioneer.getAuctionResultsNextIndex() == 0
) {
int256[] memory auctionResults = cdAuctioneer.getAuctionResults();
int256 difference;
for (uint256 i = 0; i < auctionResults.length; i++) {
difference += auctionResults[i];
}
// If there was under-selling, create a market to sell the remaining OHM
if (difference < 0) {
// Apply the capacity scalar to the remainder
// Round down in favour of the protocol (fewer emissions)
/// forge-lint: disable-next-line(unsafe-typecast)
uint256 scaledCapacity = uint256(-difference).mulDiv(
bondMarketCapacityScalar,
ONE_HUNDRED_PERCENT
);
// Only set pending capacity if the scaled value is non-zero
if (scaledCapacity > 0) {
bondMarketPendingCapacity = scaledCapacity;
// Attempt to create the bond market
try this.createPendingBondMarket() {
// Do nothing if successful
// createPendingBondMarket() resets the pending capacity, so it does not need to be done here
} catch {
// We don't want the periodic task to fail, so catch the error
// But trigger an event that can be monitored
// Upon failure, the createPendingBondMarket() function can be called again to trigger the bond market
emit BondMarketCreationFailed(scaledCapacity);
}
} else {
// Ensure that we reset the pending capacity
if (bondMarketPendingCapacity > 0) {
bondMarketPendingCapacity = 0;
}
}
}
// Otherwise, make sure we reset the pending capacity
// If there is an issue with creating the bond market, this gives the role-holder the auction tracking period to fix the underlying issue and trigger the creation of the market
else {
if (bondMarketPendingCapacity > 0) {
bondMarketPendingCapacity = 0;
}
}
}
}
// ========== INITIALIZE ========== //
/// @inheritdoc PolicyEnabler
/// @dev This function expects the parameters to be an abi-encoded `EnableParams` struct
function _enable(bytes calldata params_) internal override {
// Cannot initialize if the restart timeframe hasn't passed since the shutdown timestamp
// This is specific to re-initializing after a shutdown
// It will not revert on the first initialization since both values will be zero
if (shutdownTimestamp + restartTimeframe > uint48(block.timestamp))
revert CannotRestartYet(shutdownTimestamp + restartTimeframe);
// Validate that the params are of the correct length
if (params_.length != ENABLE_PARAMS_LENGTH) revert InvalidParam("params length");
// Decode the params
EnableParams memory params = abi.decode(params_, (EnableParams));
// Validate inputs
if (params.baseEmissionsRate == 0) revert InvalidParam("baseEmissionRate");
if (params.minimumPremium == 0) revert InvalidParam("minimumPremium");
if (params.backing == 0) revert InvalidParam("backing");
if (params.restartTimeframe == 0) revert InvalidParam("restartTimeframe");
if (params.tickSize == 0) revert InvalidParam("Tick Size");
if (params.minPriceScalar < ONE_HUNDRED_PERCENT) revert InvalidParam("Min Price Scalar");
if (params.bondMarketCapacityScalar > MAX_BOND_MARKET_CAPACITY_SCALAR)
revert InvalidParam("Bond Market Scalar");
// Assign
baseEmissionRate = params.baseEmissionsRate;
minimumPremium = params.minimumPremium;
backing = params.backing;
restartTimeframe = params.restartTimeframe;
tickSize = params.tickSize;
minPriceScalar = params.minPriceScalar;
bondMarketCapacityScalar = params.bondMarketCapacityScalar;
emit MinimumPremiumChanged(params.minimumPremium);
emit BackingChanged(params.backing);
emit RestartTimeframeChanged(params.restartTimeframe);
emit TickSizeChanged(params.tickSize);
emit MinPriceScalarChanged(params.minPriceScalar);
emit BondMarketCapacityScalarChanged(params.bondMarketCapacityScalar);
}
// ========== BOND CALLBACK ========== //
/// @notice callback function for bond market, only callable by the teller
function callback(uint256 id_, uint256 inputAmount_, uint256 outputAmount_) external {
// Only callable by the bond teller
if (msg.sender != teller) revert OnlyTeller();
// Market ID must match the active market ID stored locally, otherwise revert
if (id_ != activeMarketId) revert InvalidMarket();
// Reserve balance should have increased by atleast the input amount
uint256 reserveBalance = reserve.balanceOf(address(this));
if (reserveBalance < inputAmount_) revert InvalidCallback();
// Update backing value with the new reserves added and supply added
// We do this before depositing the received reserves and minting the output amount of OHM
// so that the getReserves and getSupply values equal the "previous" values
// This also conforms to the CEI pattern
_updateBacking(outputAmount_, inputAmount_);
// Deposit the reserve balance into the sReserve contract with the TRSRY as the recipient
// This will sweep any excess reserves into the TRSRY as well
sReserve.deposit(reserveBalance, address(TRSRY));
// Mint the output amount of OHM to the Teller
MINTR.mintOhm(teller, outputAmount_);
}
// ========== INTERNAL FUNCTIONS ========== //
/// @notice create bond protocol market with given budget
/// @param saleAmount amount of DAI to fund bond market with
function _createMarket(uint256 saleAmount) internal {
// Calculate scaleAdjustment for bond market
// Price decimals are returned from the perspective of the quote token
// so the operations assume payoutPriceDecimal is zero and quotePriceDecimals
// is the priceDecimal value
uint256 minPrice = ((ONE_HUNDRED_PERCENT + minimumPremium) * backing) /
10 ** _reserveDecimals;
int8 priceDecimals = _getPriceDecimals(minPrice);
/// forge-lint: disable-next-line(unsafe-typecast)
int8 scaleAdjustment = int8(_ohmDecimals) - int8(_reserveDecimals) + (priceDecimals / 2);
// Calculate oracle scale and bond scale with scale adjustment and format prices for bond market
/// forge-lint: disable-start(unsafe-typecast)
uint256 oracleScale = 10 ** uint8(int8(_oracleDecimals) - priceDecimals);
uint256 bondScale = 10 **
uint8(
36 + scaleAdjustment + int8(_reserveDecimals) - int8(_ohmDecimals) - priceDecimals
);
/// forge-lint: disable-end(unsafe-typecast)
// Create new bond market to buy the reserve with OHM
activeMarketId = bondAuctioneer.createMarket(
abi.encode(
IBondSDA.MarketParams({
payoutToken: ohm,
quoteToken: reserve,
callbackAddr: address(this),
capacityInQuote: false,
capacity: saleAmount,
formattedInitialPrice: PRICE.getLastPrice().mulDiv(bondScale, oracleScale),
formattedMinimumPrice: minPrice.mulDiv(bondScale, oracleScale),
debtBuffer: 100_000, // 100%
vesting: vestingPeriod,
conclusion: uint48(block.timestamp + 1 days), // 1 day from now
depositInterval: uint32(4 hours), // 4 hours
scaleAdjustment: scaleAdjustment
})
)
);
emit SaleCreated(activeMarketId, saleAmount);
}
/// @notice allow emission manager to update backing price based on new supply and reserves added
/// @param supplyAdded number of new OHM minted
/// @param reservesAdded number of new DAI added
function _updateBacking(uint256 supplyAdded, uint256 reservesAdded) internal {
uint256 previousReserves = getReserves();
uint256 previousSupply = getSupply();
uint256 percentIncreaseReserves = ((previousReserves + reservesAdded) *
10 ** _reserveDecimals) / previousReserves;
uint256 percentIncreaseSupply = ((previousSupply + supplyAdded) * 10 ** _reserveDecimals) /
previousSupply; // scaled to reserve decimals to match
backing =
(backing * percentIncreaseReserves) / // price multiplied by percent increase reserves in reserve scale
percentIncreaseSupply; // divided by percent increase supply in reserve scale
// Emit event to track backing changes and results of sales offchain
emit BackingUpdated(backing, supplyAdded, reservesAdded);
}
/// @notice Helper function to calculate number of price decimals based on the value returned from the price feed.
/// @param price_ The price to calculate the number of decimals for
/// @return The number of decimals
function _getPriceDecimals(uint256 price_) internal view returns (int8) {
int8 decimals;
while (price_ >= 10) {
price_ = price_ / 10;
decimals++;
}
// Subtract the stated decimals from the calculated decimals to get the relative price decimals.
// Required to do it this way vs. normalizing at the beginning since price decimals can be negative.
/// forge-lint: disable-next-line(unsafe-typecast)
return decimals - int8(_oracleDecimals);
}
// ========== ADMIN FUNCTIONS ========== //
/// @inheritdoc PolicyEnabler
/// @dev This function performs the following:
/// - Sets the shutdown timestamp
/// - Closes the active bond market (if it is active)
/// - Disables the convertible deposit auction
function _disable(bytes calldata) internal override {
shutdownTimestamp = uint48(block.timestamp);
// Shutdown the bond market, if it is active
if (bondAuctioneer.isLive(activeMarketId)) {
bondAuctioneer.closeMarket(activeMarketId);
}
// Disable the convertible deposit auction by setting target to 0
cdAuctioneer.setAuctionParameters(0, 0, 0);
}
/// @notice Restart the emission manager
function restart() external onlyAdminRole {
// Restart can be activated only within the specified timeframe since shutdown
// Outside of this span of time, admin must reinitialize
if (uint48(block.timestamp) >= shutdownTimestamp + restartTimeframe)
revert RestartTimeframePassed();
isEnabled = true;
emit Enabled();
}
/// @notice Rescue any ERC20 token sent to this contract and send it to the TRSRY
/// @dev This function is restricted to the ADMIN role
/// @param token_ The address of the ERC20 token to rescue
function rescue(address token_) external onlyAdminRole {
ERC20 token = ERC20(token_);
token.safeTransfer(address(TRSRY), token.balanceOf(address(this)));
}
/// @notice Set the base emissions rate
///
/// @param changeBy_ uint256 added or subtracted from baseEmissionRate
/// @param forNumBeats_ uint256 number of times to change baseEmissionRate by changeBy_
/// @param add bool determining addition or subtraction to baseEmissionRate
function changeBaseRate(
uint256 changeBy_,
uint48 forNumBeats_,
bool add
) external onlyAdminRole {
// Prevent underflow on negative adjustments
if (!add && (changeBy_ * forNumBeats_ > baseEmissionRate))
revert InvalidParam("changeBy * forNumBeats");
// Prevent overflow on positive adjustments
if (add && (type(uint256).max - changeBy_ * forNumBeats_ < baseEmissionRate))
revert InvalidParam("changeBy * forNumBeats");
rateChange = BaseRateChange({changeBy: changeBy_, daysLeft: forNumBeats_, addition: add});
emit BaseRateChanged(changeBy_, forNumBeats_, add);
}
/// @notice Set the minimum premium for emissions
/// @dev This function reverts if:
/// - newMinimumPremium_ is 0
///
/// @param newMinimumPremium_ The new minimum premium, in terms of ONE_HUNDRED_PERCENT
function setMinimumPremium(uint256 newMinimumPremium_) external onlyAdminRole {
if (newMinimumPremium_ == 0) revert InvalidParam("newMinimumPremium");
minimumPremium = newMinimumPremium_;
emit MinimumPremiumChanged(newMinimumPremium_);
}
/// @notice Set the new bond vesting period in seconds
/// @dev This function reverts if:
/// - newVestingPeriod_ is more than 31536000 (1 year in seconds)
///
/// @param newVestingPeriod_ uint48
function setVestingPeriod(uint48 newVestingPeriod_) external onlyAdminRole {
// Verify that the vesting period isn't more than a year
// This check helps ensure a timestamp isn't input instead of a duration
if (newVestingPeriod_ > uint48(31536000)) revert InvalidParam("newVestingPeriod");
vestingPeriod = newVestingPeriod_;
emit VestingPeriodChanged(newVestingPeriod_);
}
/// @notice Allow governance to adjust backing price if deviated from reality
/// @dev This function reverts if:
/// - newBacking is 0
/// - newBacking is less than 90% of current backing (to prevent large sudden drops)
///
/// Note: if adjustment is more than 33% down, contract should be redeployed
///
/// @param newBacking to adjust to
/// TODO maybe put in a timespan arg so it can be smoothed over time if desirable
function setBacking(uint256 newBacking) external onlyAdminRole {
// Backing cannot be reduced by more than 10% at a time
if (newBacking == 0 || newBacking < (backing * 9) / 10) revert InvalidParam("newBacking");
backing = newBacking;
emit BackingChanged(newBacking);
}
/// @notice Allow governance to adjust the timeframe for restart after shutdown
/// @dev This function reverts if:
/// - newTimeframe is 0
///
/// @param newTimeframe to adjust it to
function setRestartTimeframe(uint48 newTimeframe) external onlyAdminRole {
// Restart timeframe must be greater than 0
if (newTimeframe == 0) revert InvalidParam("newRestartTimeframe");
restartTimeframe = newTimeframe;
emit RestartTimeframeChanged(newTimeframe);
}
/// @notice allow governance to set the bond contracts used by the emission manager
/// @dev This function reverts if:
/// - bondAuctioneer_ is the zero address
/// - teller_ is the zero address
///
/// @param bondAuctioneer_ address of the bond auctioneer contract
/// @param teller_ address of the bond teller contract
function setBondContracts(address bondAuctioneer_, address teller_) external onlyAdminRole {
// Bond contracts cannot be set to the zero address
if (bondAuctioneer_ == address(0)) revert InvalidParam("bondAuctioneer");
if (teller_ == address(0)) revert InvalidParam("teller");
bondAuctioneer = IBondSDA(bondAuctioneer_);
teller = teller_;
emit BondContractsSet(bondAuctioneer_, teller_);
}
/// @notice Allow governance to set the CD contract used by the emission manager
/// @dev This function reverts if:
/// - cdAuctioneer_ is the zero address
/// - The deposit asset of the CDAuctioneer is not the same as the reserve asset in this contract
///
/// @param cdAuctioneer_ address of the cd auctioneer contract
function setCDAuctionContract(address cdAuctioneer_) external onlyAdminRole {
// Auction contract cannot be set to the zero address
if (cdAuctioneer_ == address(0)) revert InvalidParam("zero address");
// Validate that the CDAuctioneer is configured for the reserve asset
if (
address(IConvertibleDepositAuctioneer(cdAuctioneer_).getDepositAsset()) !=
address(reserve)
) revert InvalidParam("different asset");
cdAuctioneer = IConvertibleDepositAuctioneer(cdAuctioneer_);
emit ConvertibleDepositAuctioneerSet(cdAuctioneer_);
}
/// @notice Allow governance to set the CD tick size
/// @dev This function reverts if:
/// - newTickSize_ is 0
///
/// @param newTickSize_ as a fixed amount in OHM decimals (9)
function setTickSize(uint256 newTickSize_) external onlyAdminRole {
if (newTickSize_ == 0) revert InvalidParam("Tick Size");
tickSize = newTickSize_;
emit TickSizeChanged(newTickSize_);
}
/// @notice Allow governance to set the CD minimum price scalar
/// @dev This function reverts if:
/// - newScalar is less than ONE_HUNDRED_PERCENT (100% in 18 decimals)
///
/// @param newScalar as a percentage in 18 decimals
function setMinPriceScalar(uint256 newScalar) external onlyAdminRole {
if (newScalar < ONE_HUNDRED_PERCENT) revert InvalidParam("Min Price Scalar");
minPriceScalar = newScalar;
emit MinPriceScalarChanged(newScalar);
}
/// @notice Allow governance to set the bond market capacity scalar
/// @dev This function reverts if:
/// - newScalar is greater than MAX_BOND_MARKET_CAPACITY_SCALAR (200%)
///
/// @param newScalar as a percentage in 18 decimals
function setBondMarketCapacityScalar(uint256 newScalar) external onlyAdminRole {
if (newScalar > MAX_BOND_MARKET_CAPACITY_SCALAR)
revert InvalidParam("Bond Market Capacity Scalar");
bondMarketCapacityScalar = newScalar;
emit BondMarketCapacityScalarChanged(newScalar);
}
// =========- VIEW FUNCTIONS ========== //
/// @notice return reserves, measured as clearinghouse receivables and sReserve balances, in reserve denomination
function getReserves() public view returns (uint256 reserves) {
uint256 chCount = CHREG.registryCount();
for (uint256 i; i < chCount; i++) {
reserves += IGenericClearinghouse(CHREG.registry(i)).principalReceivables();
uint256 bal = sReserve.balanceOf(CHREG.registry(i));
if (bal > 0) reserves += sReserve.previewRedeem(bal);
}
reserves += sReserve.previewRedeem(sReserve.balanceOf(address(TRSRY)));
}
/// @notice return supply, measured as supply of gOHM in OHM denomination
function getSupply() public view returns (uint256 supply) {
return (gohm.totalSupply() * gohm.index()) / 10 ** _gohmDecimals;
}
/// @notice return the current premium as a percentage where 1e18 is 100%
function getPremium() public view returns (uint256) {
uint256 price = PRICE.getLastPrice();
uint256 pbr = (price * 10 ** _reserveDecimals) / backing;
return pbr > ONE_HUNDRED_PERCENT ? pbr - ONE_HUNDRED_PERCENT : 0;
}
/// @notice return the next sale amount, premium, emission rate, and emissions based on the current premium
function getNextEmission()
public
view
returns (uint256 premium, uint256 emissionRate, uint256 emission)
{
// To calculate the sale, it first computes premium (market price / backing price) - 100%
premium = getPremium();
// If the premium is greater than the minimum premium, it computes the emission rate and nominal emissions
if (premium >= minimumPremium) {
emissionRate =
(baseEmissionRate * (ONE_HUNDRED_PERCENT + premium)) /
(ONE_HUNDRED_PERCENT + minimumPremium); // in OHM scale
emission = (getSupply() * emissionRate) / 10 ** _ohmDecimals; // OHM Scale * OHM Scale / OHM Scale = OHM Scale
}
}
/// @notice Get the auction tick size for a given target
/// @dev Returns the standard tick size if the target emission is at least the standard tick size.
/// Otherwise, 0 is returned to indicate that the auction should be disabled.
///
/// @param target size of day's CD auction
/// @return size of tick
function getSizeFor(uint256 target) public view returns (uint256 size) {
if (target < tickSize) return 0;
return tickSize;
}
/// @notice Get CD auction minimum price for a given price input
/// @dev Expects `price` to already be expressed in the reserve asset's decimal scale.
/// This function does not adjust/convert decimal scales.
///
/// @param price Price of OHM in reserve token terms, scaled to the reserve asset's decimals
function getMinPriceFor(uint256 price) public view returns (uint256) {
// Round in favour of the protocol
return price.mulDivUp(minPriceScalar, ONE_HUNDRED_PERCENT);
}
/// @notice Returns the current price from the PRICE module
///
/// @return currentPrice with decimal scale of the reserve asset
function _getCurrentPrice() internal view returns (uint256) {
// Get from PRICE
uint256 currentPrice = PRICE.getCurrentPrice();
// Change the decimal scale to be the reserve asset's
return currentPrice.mulDiv(10 ** _reserveDecimals, 10 ** _oracleDecimals);
}
// ========== BOND MARKET CREATION ========== //
/// @notice Creates a bond market
/// @dev Notes:
/// - If there is no pending capacity, no bond market will be created
///
/// This function will revert if:
/// - The caller is not this contract, or an address with the admin/manager role
/// - The contract is disabled
/// - The bond market cannot be created
function createPendingBondMarket() external onlyEnabled {
// Validate that the caller is this contract or an admin/manager
if (msg.sender != address(this) && !_isManager(msg.sender) && !_isAdmin(msg.sender))
revert NotAuthorised();
// If there is no pending capacity, skip
if (bondMarketPendingCapacity == 0) return;
// Create the market
MINTR.increaseMintApproval(address(this), bondMarketPendingCapacity);
_createMarket(bondMarketPendingCapacity);
// Set the pending capacity to 0
// This prevents the bond market from being created again
bondMarketPendingCapacity = 0;
}
// ========== ERC165 ========== //
function supportsInterface(
bytes4 interfaceId
) public view virtual override(PolicyEnabler, IPeriodicTask) returns (bool) {
return
interfaceId == bytes4(0x01ffc9a7) || // ERC-165
interfaceId == type(IPeriodicTask).interfaceId ||
interfaceId == type(IEmissionManager).interfaceId ||
super.supportsInterface(interfaceId);
}
}
/// forge-lint: disable-end(mixed-case-function, mixed-case-variable, screaming-snake-case-immutable)
"
},
"dependencies/solmate-6.2.0/src/tokens/ERC20.sol": {
"content": "// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity >=0.8.0;
/// @notice Modern and gas efficient ERC20 + EIP-2612 implementation.
/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC20.sol)
/// @author Modified from Uniswap (https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2ERC20.sol)
/// @dev Do not manually set balances without updating totalSupply, as the sum of all user balances must not exceed it.
abstract contract ERC20 {
/*//////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
event Transfer(address indexed from, address indexed to, uint256 amount);
event Approval(address indexed owner, address indexed spender, uint256 amount);
/*//////////////////////////////////////////////////////////////
METADATA STORAGE
//////////////////////////////////////////////////////////////*/
string public name;
string public symbol;
uint8 public immutable decimals;
/*//////////////////////////////////////////////////////////////
ERC20 STORAGE
//////////////////////////////////////////////////////////////*/
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
/*//////////////////////////////////////////////////////////////
EIP-2612 STORAGE
//////////////////////////////////////////////////////////////*/
uint256 internal immutable INITIAL_CHAIN_ID;
bytes32 internal immutable INITIAL_DOMAIN_SEPARATOR;
mapping(address => uint256) public nonces;
/*//////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////*/
constructor(
string memory _name,
string memory _symbol,
uint8 _decimals
) {
name = _name;
symbol = _symbol;
decimals = _decimals;
INITIAL_CHAIN_ID = block.chainid;
INITIAL_DOMAIN_SEPARATOR = computeDomainSeparator();
}
/*//////////////////////////////////////////////////////////////
ERC20 LOGIC
//////////////////////////////////////////////////////////////*/
function approve(address spender, uint256 amount) public virtual returns (bool) {
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function transfer(address to, uint256 amount) public virtual returns (bool) {
balanceOf[msg.sender] -= amount;
// Cannot overflow because the sum of all user
// balances can't exceed the max uint256 value.
unchecked {
balanceOf[to] += amount;
}
emit Transfer(msg.sender, to, amount);
return true;
}
function transferFrom(
address from,
address to,
uint256 amount
) public virtual returns (bool) {
uint256 allowed = allowance[from][msg.sender]; // Saves gas for limited approvals.
if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - amount;
balanceOf[from] -= amount;
// Cannot overflow because the sum of all user
// balances can't exceed the max uint256 value.
unchecked {
balanceOf[to] += amount;
}
emit Transfer(from, to, amount);
return true;
}
/*//////////////////////////////////////////////////////////////
EIP-2612 LOGIC
//////////////////////////////////////////////////////////////*/
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) public virtual {
require(deadline >= block.timestamp, "PERMIT_DEADLINE_EXPIRED");
// Unchecked because the only math done is incrementing
// the owner's nonce which cannot realistically overflow.
unchecked {
address recoveredAddress = ecrecover(
keccak256(
abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR(),
keccak256(
abi.encode(
keccak256(
"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
),
owner,
spender,
value,
nonces[owner]++,
deadline
)
)
)
),
v,
r,
s
);
require(recoveredAddress != address(0) && recoveredAddress == owner, "INVALID_SIGNER");
allowance[recoveredAddress][spender] = value;
}
emit Approval(owner, spender, value);
}
function DOMAIN_SEPARATOR() public view virtual returns (bytes32) {
return block.chainid == INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : computeDomainSeparator();
}
function computeDomainSeparator() internal view virtual returns (bytes32) {
return
keccak256(
abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256(bytes(name)),
keccak256("1"),
block.chainid,
address(this)
)
);
}
/*//////////////////////////////////////////////////////////////
INTERNAL MINT/BURN LOGIC
//////////////////////////////////////////////////////////////*/
function _mint(address to, uint256 amount) internal virtual {
totalSupply += amount;
// Cannot overflow because the sum of all user
// balances can't exceed the max uint256 value.
unchecked {
balanceOf[to] += amount;
}
emit Transfer(address(0), to, amount);
}
function _burn(address from, uint256 amount) internal virtual {
balanceOf[from] -= amount;
// Cannot underflow because a user's balance
// will never be larger than the total supply.
unchecked {
totalSupply -= amount;
}
emit Transfer(from, address(0), amount);
}
}
"
},
"dependencies/solmate-6.2.0/src/mixins/ERC4626.sol": {
"content": "// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity >=0.8.0;
import {ERC20} from "../tokens/ERC20.sol";
import {SafeTransferLib} from "../utils/SafeTransferLib.sol";
import {FixedPointMathLib} from "../utils/FixedPointMathLib.sol";
/// @notice Minimal ERC4626 tokenized Vault implementation.
/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/mixins/ERC4626.sol)
abstract contract ERC4626 is ERC20 {
using SafeTransferLib for ERC20;
using FixedPointMathLib for uint256;
/*//////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
event Deposit(address indexed caller, address indexed owner, uint256 assets, uint256 shares);
event Withdraw(
address indexed caller,
address indexed receiver,
address indexed owner,
uint256 assets,
uint256 shares
);
/*//////////////////////////////////////////////////////////////
IMMUTABLES
//////////////////////////////////////////////////////////////*/
ERC20 public immutable asset;
constructor(
ERC20 _asset,
string memory _name,
string memory _symbol
) ERC20(_name, _symbol, _asset.decimals()) {
asset = _asset;
}
/*//////////////////////////////////////////////////////////////
DEPOSIT/WITHDRAWAL LOGIC
//////////////////////////////////////////////////////////////*/
function deposit(uint256 assets, address receiver) public virtual returns (uint256 shares) {
// Check for rounding error since we round down in previewDeposit.
require((shares = previewDeposit(assets)) != 0, "ZERO_SHARES");
// Need to transfer before minting or ERC777s could reenter.
asset.safeTransferFrom(msg.sender, address(this), assets);
_mint(receiver, shares);
emit Deposit(msg.sender, receiver, assets, shares);
afterDeposit(assets, shares);
}
function mint(uint256 shares, address receiver) public virtual returns (uint256 assets) {
assets = previewMint(shares); // No need to check for rounding error, previewMint rounds up.
// Need to transfer before minting or ERC777s could reenter.
asset.safeTransferFrom(msg.sender, address(this), assets);
_mint(receiver, shares);
emit Deposit(msg.sender, receiver, assets, shares);
afterDeposit(assets, shares);
}
function withdraw(
uint256 assets,
address receiver,
address owner
) public virtual returns (uint256 shares) {
shares = previewWithdraw(assets); // No need to check for rounding error, previewWithdraw rounds up.
if (msg.sender != owner) {
uint256 allowed = allowance[owner][msg.sender]; // Saves gas for limited approvals.
if (allowed != type(uint256).max) allowance[owner][msg.sender] = allowed - shares;
}
beforeWithdraw(assets, shares);
_burn(owner, shares);
emit Withdraw(msg.sender, receiver, owner, assets, shares);
asset.safeTransfer(receiver, assets);
}
function redeem(
uint256 shares,
address receiver,
address owner
) public virtual returns (uint256 assets) {
if (msg.sender != owner) {
uint256 allowed = allowance[owner][msg.sender]; // Saves gas for limited approvals.
if (allowed != type(uint256).max) allowance[owner][msg.sender] = allowed - shares;
}
// Check for rounding error since we round down in previewRedeem.
require((assets = previewRedeem(shares)) != 0, "ZERO_ASSETS");
beforeWithdraw(assets, shares);
_burn(owner, shares);
emit Withdraw(msg.sender, receiver, owner, assets, shares);
asset.safeTransfer(receiver, assets);
}
/*//////////////////////////////////////////////////////////////
ACCOUNTING LOGIC
//////////////////////////////////////////////////////////////*/
function totalAssets() public view virtual returns (uint256);
function convertToShares(uint256 assets) public view virtual returns (uint256) {
uint256 supply = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero.
return supply == 0 ? assets : assets.mulDivDown(supply, totalAssets());
}
function convertToAssets(uint256 shares) public view virtual returns (uint256) {
uint256 supply = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero.
return supply == 0 ? shares : shares.mulDivDown(totalAssets(), supply);
}
function previewDeposit(uint256 assets) public view virtual returns (uint256) {
return convertToShares(assets);
}
function previewMint(uint256 shares) public view virtual returns (uint256) {
uint256 supply = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero.
return supply == 0 ? shares : shares.mulDivUp(totalAssets(), supply);
}
function previewWithdraw(uint256 assets) public view virtual returns (uint256) {
uint256 supply = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero.
return supply == 0 ? assets : assets.mulDivUp(supply, totalAssets());
}
function previewRedeem(uint256 shares) public view virtual returns (uint256) {
return convertToAssets(shares);
}
/*//////////////////////////////////////////////////////////////
DEPOSIT/WITHDRAWAL LIMIT LOGIC
//////////////////////////////////////////////////////////////*/
function maxDeposit(address) public view virtual returns (uint256) {
return type(uint256).max;
}
function maxMint(address) public view virtual returns (uint256) {
return type(uint256).max;
}
function maxWithdraw(address owner) public view virtual returns (uint256) {
return convertToAssets(balanceOf[owner]);
}
function maxRedeem(address owner) public view virtual returns (uint256) {
return balanceOf[owner];
}
/*//////////////////////////////////////////////////////////////
INTERNAL HOOKS LOGIC
//////////////////////////////////////////////////////////////*/
function beforeWithdraw(uint256 assets, uint256 shares) internal virtual {}
function afterDeposit(uint256 assets, uint256 shares) internal virtual {}
}
"
},
"src/libraries/TransferHelper.sol": {
"content": "// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity >=0.8.0;
import {ERC20} from "solmate/tokens/ERC20.sol";
/// @notice Safe ERC20 and ETH transfer library that safely handles missing return values.
/// @author Modified from Uniswap & old Solmate (https://github.com/Uniswap/uniswap-v3-periphery/blob/main/contracts/libraries/TransferHelper.sol)
library TransferHelper {
function safeTransferFrom(ERC20 token, address from, address to, uint256 amount) internal {
(bool success, bytes memory data) = address(token).call(
abi.encodeWithSelector(ERC20.transferFrom.selector, from, to, amount)
);
require(success && (data.length == 0 || abi.decode(data, (bool))), "TRANSFER_FROM_FAILED");
}
function safeTransfer(ERC20 token, address to, uint256 amount) internal {
(bool success, bytes memory data) = address(token).call(
abi.encodeWithSelector(ERC20.transfer.selector, to, amount)
);
require(success && (data.length == 0 || abi.decode(data, (bool))), "TRANSFER_FAILED");
}
function safeApprove(ERC20 token, address to, uint256 amount) internal {
(bool success, bytes memory data) = address(token).call(
abi.encodeWithSelector(ERC20.approve.selector, to, amount)
);
require(success && (data.length == 0 || abi.decode(data, (bool))), "APPROVE_FAILED");
}
}
"
},
"src/libraries/FullMath.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
/// @title Contains 512-bit math functions
/// @notice Facilitates multiplication and division that can have overflow of an intermediate value without any loss of precision
/// @dev Handles "phantom overflow" i.e., allows multiplication and division where an intermediate value overflows 256 bits
library FullMath {
/// @notice Calculates floor(a×b÷denominator) with full precision. Throws if result overflows a uint256 or denominator == 0
/// @param a The multiplicand
/// @param b The multiplier
/// @param denominator The divisor
/// @return result The 256-bit result
/// @dev Credit to Remco Bloemen under MIT license https://xn--2-umb.com/21/muldiv
function mulDiv(
uint256 a,
uint256 b,
uint256 denominator
) internal pure returns (uint256 result) {
unchecked {
// 512-bit multiply [prod1 prod0] = a * b
// Compute the product mod 2**256 and mod 2**256 - 1
// then use the Chinese Remainder Theorem to reconstruct
// the 512 bit result. The result is stored in two 256
// variables such that product = prod1 * 2**256 + prod0
uint256 prod0; // Least significant 256 bits of the product
uint256 prod1; // Most significant 256 bits of the product
assembly {
let mm := mulmod(a, b, not(0))
prod0 := mul(a, b)
prod1 := sub(sub(mm, prod0), lt(mm, prod0))
}
// Handle non-overflow cases, 256 by 256 division
if (prod1 == 0) {
require(denominator > 0);
assembly {
result := div(prod0, denominator)
}
return result;
}
// Make sure the result is less than 2**256.
// Also prevents denominator == 0
require(denominator > prod1);
///////////////////////////////////////////////
// 512 by 256 division.
///////////////////////////////////////////////
// Make division exact by subtracting the remainder from [prod1 prod0]
// Compute remainder using mulmod
uint256 remainder;
assembly {
remainder := mulmod(a, b, denominator)
}
// Subtract 256 bit number from 512 bit number
assembly {
prod1 := sub(prod1, gt(remainder, prod0))
prod0 := sub(prod0, remainder)
}
// Factor powers of two out of denominator
// Compute largest power of two divisor of denominator.
// Always >= 1.
uint256 twos = (type(uint256).max - denominator + 1) & denominator;
// Divide denominator by power of two
assembly {
denominator := div(denominator, twos)
}
// Divide [prod1 prod0] by the factors of two
assembly {
prod0 := div(prod0, twos)
}
// Shift in bits from prod1 into prod0. For this we need
// to flip `twos` such that it is 2**256 / twos.
// If twos is zero, then it becomes one
assembly {
twos := add(div(sub(0, twos), twos), 1)
}
prod0 |= prod1 * twos;
// Invert denominator mod 2**256
// Now that denominator is an odd number, it has an inverse
// modulo 2**256 such that denominator * inv = 1 mod 2**256.
// Compute the inverse by starting with a seed that is correct
// correct for four bits. That is, denominator * inv = 1 mod 2**4
uint256 inv = (3 * denominator) ^ 2;
// Now use Newton-Raphson iteration to improve the precision.
// Thanks to Hensel's lifting lemma, this also works in modular
// arithmetic, doubling the correct bits in each step.
inv *= 2 - denominator * inv; // inverse mod 2**8
inv *= 2 - denominator * inv; // inverse mod 2**16
inv *= 2 - denominator * inv; // inverse mod 2**32
inv *= 2 - denominator * inv; // inverse mod 2**64
inv *= 2 - denominator * inv; // inverse mod 2**128
inv *= 2 - denominator * inv; // inverse mod 2**256
// Because the division is now exact we can divide by multiplying
// with the modular inverse of denominator. This will give us the
// correct result modulo 2**256. Since the precoditions guarantee
// that the outcome is less than 2**256, this is the final result.
// We don't need to compute the high bits of the result and prod1
// is no longer required.
result = prod0 * inv;
return result;
}
}
/// @notice Calculates ceil(a×b÷denominator) with full precision. Throws if result overflows a uint256 or denominator == 0
/// @param a The multiplicand
/// @param b The multiplier
/// @param denominator The divisor
/// @return result The 256-bit result
function mulDivUp(
uint256 a,
uint256 b,
uint256 denominator
) internal pure returns (uint256 result) {
result = mulDiv(a, b, denominator);
unchecked {
if (mulmod(a, b, denominator) > 0) {
require(result < type(uint256).max);
result++;
}
}
}
}
"
},
"src/interfaces/IBondSDA.sol": {
"content": "// SPDX-License-Identifier: AGPL-3.0
pragma solidity >=0.8.0;
import {ERC20} from "solmate/tokens/ERC20.sol";
import {IBondAuctioneer} from "../interfaces/IBondAuctioneer.sol";
interface IBondSDA is IBondAuctioneer {
/// @notice Main information pertaining to bond market
struct BondMarket {
address owner; // market owner. sends payout tokens, receives quote tokens (defaults to creator)
ERC20 payoutToken; // token to pay depositors with
ERC20 quoteToken; // token to accept as payment
address callbackAddr; // address to call for any operations on bond purchase. Must inherit to IBondCallback.
bool capacityInQuote; // capacity limit is in payment token (true) or in payout (false, default)
uint256 capacity; // capacity remaining
uint256 totalDebt; // total payout token debt from market
uint256 minPrice; // minimum price (hard floor for the market)
uint256 maxPayout; // max payout tokens out in one order
uint256 sold; // payout tokens out
uint256 purchased; // quote tokens in
uint256 scale; // scaling factor for the market (see MarketParams struct)
}
/// @notice Information used to control how a bond market changes
struct BondTerms {
uint256 controlVariable; // scaling variable for price
uint256 maxDebt; // max payout token debt accrued
uint48 vesting; // length of time from deposit to expiry if fixed-term, vesting timestamp if fixed-expiry
uint48 conclusion; // timestamp when market no longer offered
}
/// @notice Data needed for tuning bond market
/// @dev Durations are stored in uint32 (not int32) and timestamps are stored in uint48, so is not subject to Y2K38 overflow
struct BondMetadata {
uint48 lastTune; // last timestamp when control variable was tuned
uint48 lastDecay; // last timestamp when market was created and debt was decayed
uint32 length; // time from creation to conclusion.
uint32 depositInterval; // target frequency of deposits
uint32 tuneInterval; // frequency of tuning
uint32 tuneAdjustmentDelay; // time to implement downward tuning adjustments
uint32 debtDecayInterval; // interval over which debt should decay completely
uint256 tuneIntervalCapacity; // capacity expected to be used during a tuning interval
uint256 tuneBelowCapacity; // capacity that the next tuning will occur at
uint256 lastTuneDebt; // target debt calculated at last tuning
}
/// @notice Control variable adjustment data
struct Adjustment {
uint256 change;
uint48 lastAdjustment;
uint48 timeToAdjusted; // how long until adjustment happens
bool active;
}
/// @notice Parameters to create a new bond market
/// @dev Note price should be passed in a specific format:
/// formatted price = (payoutPriceCoefficient / quotePriceCoefficient)
/// * 10**(36 + scaleAdjustment + quoteDecimals - payoutDecimals + payoutPriceDecimals - quotePriceDecimals)
/// where:
/// payoutDecimals - Number of decimals defined for the payoutToken in its ERC20 contract
/// quoteDecimals - Number of decimals defined for the quoteToken in its ERC20 contract
/// payoutPriceCoefficient - The coefficient of the payoutToken price in scientific notation (also known as the significant digits)
/// payoutPriceDecimals - The significand of the payoutToken price in scientific notation (also known as the base ten exponent)
/// quotePriceCoefficient - The coefficient of the quoteToken price in scientific notation (also known as the significant digits)
/// quotePriceDecimals - The significand of the quoteToken price in scientific notation (also known as the base ten exponent)
/// scaleAdjustment - see below
/// * In the above definitions, the "prices" need to have the same unit of account (i.e. both in OHM, $, ETH, etc.)
/// If price is not provided in this format, the market will not behave as intended.
/// @param params_ Encoded bytes array, with the following elements
/// @dev 0. Payout Token (token paid out)
/// @dev 1. Quote Token (token to be received)
/// @dev 2. Callback contract address, should conform to IBondCallback. If 0x00, tokens will be transferred from market.owner
/// @dev 3. Is Capacity in Quote Token?
/// @dev 4. Capacity (amount in quoteDecimals or amount in payoutDecimals)
/// @dev 5. Formatted initial price (see note above)
/// @dev 6. Formatted minimum price (see note above)
/// @dev 7. Debt buffer. Percent with 3 decimals. Percentage over the initial debt to allow the market to accumulate at anyone time.
/// @dev Works as a circuit breaker for the market in case external conditions incentivize massive buying (e.g. stablecoin depeg).
/// @dev Minimum is the greater of 10% or initial max payout as a percentage of capacity.
/// @dev If the value is too small, the market will not be able function normally and close prematurely.
/// @dev If the value is too large, the market will not circuit break when intended. The value must be > 10% but can exceed 100% if desired.
/// @dev A good heuristic to calculate a debtBuffer with is to determine the amount of capacity that you think is reasonable to be expended
/// @dev in a short duration as a percent, e.g. 25%. Then a reasonable debtBuffer would be: 0.25 * 1e3 * decayInterval / marketDuration
/// @dev where decayInterval = max(3 days, 5 * depositInterval) and marketDuration = conclusion - creation time.
/// @dev 8. Is fixed term ? Vesting length (seconds) : Vesting expiry (timestamp).
/// @dev A 'vesting' param longer than 50 years is considered a timestamp for fixed expiry.
/// @dev 9. Conclusion (timestamp)
/// @dev 10. Deposit interval (seconds)
/// @dev 11. Market scaling factor adjustment, ranges from -24 to +24 within the configured market bounds.
/// @dev Should be calculated as: (payoutDecimals - quoteDecimals) - ((payoutPriceDecimals - quotePriceDecimals) / 2)
/// @dev Providing a scaling factor adjustment that doesn't follow this formula could lead to under or overflow errors in the market.
/// @return ID of new bond market
struct MarketParams {
ERC20 payoutToken;
ERC20 quoteToken;
address callbackAddr;
bool capacityInQuote;
uint256 capacity;
uint256 formattedInitialPrice;
uint256 formattedMinimumPrice;
uint32 debtBuffer;
uint48 vesting;
uint48 conclusion;
uint32 depositInterval;
int8 scaleAdjustment;
}
/* ========== VIEW FUNCTIONS ========== */
/// @notice Calculate current market price of payout token in quote tokens
/// @dev Accounts for debt and control variable decay since last deposit (vs _marketPrice())
/// @param id_ ID of market
/// @return Price for market in configured decimals (see Ma
Submitted on: 2025-11-07 16:48:13
Comments
Log in to comment.
No comments yet.