From 053cb3237f98822f41720b0ff57335737c6f6c1d Mon Sep 17 00:00:00 2001
From: ohmzeus <93612359+chefomi@users.noreply.github.com>
Date: Fri, 29 Nov 2024 08:10:29 -0500
Subject: [PATCH 01/64] Create CDsEmissions.sol
---
src/policies/CDsEmissions.sol | 469 ++++++++++++++++++++++++++++++++++
1 file changed, 469 insertions(+)
create mode 100644 src/policies/CDsEmissions.sol
diff --git a/src/policies/CDsEmissions.sol b/src/policies/CDsEmissions.sol
new file mode 100644
index 00000000..d8d1718a
--- /dev/null
+++ b/src/policies/CDsEmissions.sol
@@ -0,0 +1,469 @@
+// SPDX-License-Identifier: AGPL-3.0
+pragma solidity 0.8.15;
+
+import "src/Kernel.sol";
+
+import {ReentrancyGuard} from "solmate/utils/ReentrancyGuard.sol";
+import {ERC20} from "solmate/tokens/ERC20.sol";
+import {ERC4626} from "solmate/mixins/ERC4626.sol";
+
+import {FullMath} from "libraries/FullMath.sol";
+
+import {IBondSDA} from "interfaces/IBondSDA.sol";
+import {IgOHM} from "interfaces/IgOHM.sol";
+
+import {RolesConsumer, ROLESv1} from "modules/ROLES/OlympusRoles.sol";
+import {TRSRYv1} from "modules/TRSRY/TRSRY.v1.sol";
+import {PRICEv1} from "modules/PRICE/PRICE.v1.sol";
+import {MINTRv1} from "modules/MINTR/MINTR.v1.sol";
+import {CHREGv1} from "modules/CHREG/CHREG.v1.sol";
+
+interface BurnableERC20 {
+ function burn(uint256 amount) external;
+}
+
+interface Clearinghouse {
+ function principalReceivables() external view returns (uint256);
+}
+
+contract EmissionManager is Policy, RolesConsumer {
+ using FullMath for uint256;
+
+ // ========== ERRORS ========== //
+
+ error OnlyTeller();
+ error InvalidMarket();
+ error InvalidCallback();
+ error InvalidParam(string parameter);
+ error CannotRestartYet(uint48 availableAt);
+ error RestartTimeframePassed();
+ error NotActive();
+ error AlreadyActive();
+
+ // ========== EVENTS ========== //
+
+ event SaleCreated(uint256 marketID, uint256 saleAmount);
+ event BackingUpdated(uint256 newBacking, uint256 supplyAdded, uint256 reservesAdded);
+
+ // ========== DATA STRUCTURES ========== //
+
+ struct BaseRateChange {
+ uint256 changeBy;
+ uint48 beatsLeft;
+ bool addition;
+ }
+
+ // ========== STATE VARIABLES ========== //
+
+ /// @notice active base emissions rate change information
+ /// @dev active until beatsLeft is 0
+ BaseRateChange public rateChange;
+
+ // Modules
+ TRSRYv1 public TRSRY;
+ PRICEv1 public PRICE;
+ MINTRv1 public MINTR;
+ CHREGv1 public CHREG;
+
+ // Tokens
+ // solhint-disable const-name-snakecase
+ ERC20 public immutable ohm;
+ IgOHM public immutable gohm;
+ ERC20 public immutable reserve;
+ ERC4626 public immutable sReserve;
+
+ // External contracts
+ IBondSDA public auctioneer;
+ address public teller;
+
+ // Manager variables
+ uint256 public baseEmissionRate;
+ uint256 public minimumPremium;
+ uint48 public vestingPeriod; // initialized at 0
+ uint256 public backing;
+ uint8 public beatCounter;
+ bool public locallyActive;
+ uint256 public activeMarketId;
+
+ uint8 internal _oracleDecimals;
+ uint8 internal immutable _ohmDecimals;
+ uint8 internal immutable _reserveDecimals;
+
+ /// @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;
+
+ uint256 internal constant ONE_HUNDRED_PERCENT = 1e18;
+
+ // ========== SETUP ========== //
+
+ constructor(
+ Kernel kernel_,
+ address ohm_,
+ address gohm_,
+ address reserve_,
+ address sReserve_,
+ address auctioneer_,
+ address teller_
+ ) Policy(kernel_) {
+ // Set immutable variables
+ if (ohm_ == address(0)) revert("OHM address cannot be 0");
+ if (gohm_ == address(0)) revert("gOHM address cannot be 0");
+ if (reserve_ == address(0)) revert("DAI address cannot be 0");
+ if (sReserve_ == address(0)) revert("sDAI address cannot be 0");
+ if (auctioneer_ == address(0)) revert("Auctioneer address cannot be 0");
+
+ ohm = ERC20(ohm_);
+ gohm = IgOHM(gohm_);
+ reserve = ERC20(reserve_);
+ sReserve = ERC4626(sReserve_);
+ auctioneer = IBondSDA(auctioneer_);
+ teller = teller_;
+
+ _ohmDecimals = ohm.decimals();
+ _reserveDecimals = reserve.decimals();
+
+ // Max approve sReserve contract for reserve for deposits
+ reserve.approve(address(sReserve), type(uint256).max);
+ }
+
+ 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();
+ }
+
+ function requestPermissions()
+ external
+ view
+ override
+ returns (Permissions[] memory permissions)
+ {
+ Keycode mintrKeycode = toKeycode("MINTR");
+
+ permissions = new Permissions[](2);
+ permissions[0] = Permissions(mintrKeycode, MINTR.increaseMintApproval.selector);
+ permissions[1] = Permissions(mintrKeycode, MINTR.mintOhm.selector);
+ }
+
+ // ========== HEARTBEAT ========== //
+
+ /// @notice calculate and execute sale, if applicable, once per day (every 3 beats)
+ function execute() external onlyRole("heart") {
+ if (!locallyActive) return;
+
+ beatCounter = ++beatCounter % 3;
+ if (beatCounter != 0) return;
+
+ if (rateChange.beatsLeft != 0) {
+ --rateChange.beatsLeft;
+ if (rateChange.addition) baseEmissionRate += rateChange.changeBy;
+ else baseEmissionRate -= rateChange.changeBy;
+ }
+
+ // It then calculates the amount to sell for the coming day
+ (, , uint256 sell) = getNextSale();
+
+ // And then opens a market if applicable
+ if (sell != 0) {
+ MINTR.increaseMintApproval(address(this), sell);
+ _createMarket(sell);
+ }
+ }
+
+ // ========== INITIALIZE ========== //
+
+ /// @notice allow governance to initialize the emission manager
+ /// @param baseEmissionsRate_ percent of OHM supply to issue per day at the minimum premium, in OHM scale, i.e. 1e9 = 100%
+ /// @param minimumPremium_ minimum premium at which to issue OHM, a percentage where 1e18 is 100%
+ /// @param backing_ backing price of OHM in reserve token, in reserve scale
+ /// @param restartTimeframe_ time in seconds that the manager needs to be restarted after a shutdown, otherwise it must be re-initialized
+ function initialize(
+ uint256 baseEmissionsRate_,
+ uint256 minimumPremium_,
+ uint256 backing_,
+ uint48 restartTimeframe_
+ ) external onlyRole("emissions_admin") {
+ // Cannot initialize if currently active
+ if (locallyActive) revert AlreadyActive();
+
+ // 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 inputs
+ if (baseEmissionsRate_ == 0) revert InvalidParam("baseEmissionRate");
+ if (minimumPremium_ == 0) revert InvalidParam("minimumPremium");
+ if (backing_ == 0) revert InvalidParam("backing");
+ if (restartTimeframe_ == 0) revert InvalidParam("restartTimeframe");
+
+ // Assign
+ baseEmissionRate = baseEmissionsRate_;
+ minimumPremium = minimumPremium_;
+ backing = backing_;
+ restartTimeframe = restartTimeframe_;
+
+ // Activate
+ locallyActive = true;
+ }
+
+ // ========== 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);
+ int8 scaleAdjustment = int8(_ohmDecimals) - int8(_reserveDecimals) + (priceDecimals / 2);
+
+ // Calculate oracle scale and bond scale with scale adjustment and format prices for bond market
+ uint256 oracleScale = 10 ** uint8(int8(_oracleDecimals) - priceDecimals);
+ uint256 bondScale = 10 **
+ uint8(
+ 36 + scaleAdjustment + int8(_reserveDecimals) - int8(_ohmDecimals) - priceDecimals
+ );
+
+ // Create new bond market to buy the reserve with OHM
+ activeMarketId = auctioneer.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.
+ return decimals - int8(_oracleDecimals);
+ }
+
+ // ========== ADMIN FUNCTIONS ========== //
+
+ /// @notice shutdown the emission manager locally, burn OHM, and return any reserves to TRSRY
+ function shutdown() external onlyRole("emergency_shutdown") {
+ locallyActive = false;
+ shutdownTimestamp = uint48(block.timestamp);
+
+ uint256 ohmBalance = ohm.balanceOf(address(this));
+ if (ohmBalance > 0) BurnableERC20(address(ohm)).burn(ohmBalance);
+
+ uint256 reserveBalance = reserve.balanceOf(address(this));
+ if (reserveBalance > 0) sReserve.deposit(reserveBalance, address(TRSRY));
+ }
+
+ /// @notice restart the emission manager locally
+ function restart() external onlyRole("emergency_restart") {
+ // Restart can be activated only within the specified timeframe since shutdown
+ // Outside of this span of time, emissions_admin must reinitialize
+ if (uint48(block.timestamp) >= shutdownTimestamp + restartTimeframe)
+ revert RestartTimeframePassed();
+
+ locallyActive = true;
+ }
+
+ /// @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 onlyRole("emissions_admin") {
+ // 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_, forNumBeats_, add);
+ }
+
+ /// @notice set the minimum premium for emissions
+ /// @param newMinimumPremium_ uint256
+ function setMinimumPremium(uint256 newMinimumPremium_) external onlyRole("emissions_admin") {
+ if (newMinimumPremium_ == 0) revert InvalidParam("newMinimumPremium");
+
+ minimumPremium = newMinimumPremium_;
+ }
+
+ /// @notice set the new vesting period in seconds
+ /// @param newVestingPeriod_ uint48
+ function setVestingPeriod(uint48 newVestingPeriod_) external onlyRole("emissions_admin") {
+ // 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_;
+ }
+
+ /// @notice allow governance to adjust backing price if deviated from reality
+ /// @dev 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 onlyRole("emissions_admin") {
+ // Backing cannot be reduced by more than 10% at a time
+ if (newBacking < (backing * 9) / 10) revert InvalidParam("newBacking");
+ backing = newBacking;
+ }
+
+ /// @notice allow governance to adjust the timeframe for restart after shutdown
+ /// @param newTimeframe to adjust it to
+ function setRestartTimeframe(uint48 newTimeframe) external onlyRole("emissions_admin") {
+ // Restart timeframe must be greater than 0
+ if (newTimeframe == 0) revert InvalidParam("newRestartTimeframe");
+
+ restartTimeframe = newTimeframe;
+ }
+
+ /// @notice allow governance to set the bond contracts used by the emission manager
+ /// @param auctioneer_ address of the bond auctioneer contract
+ /// @param teller_ address of the bond teller contract
+ function setBondContracts(
+ address auctioneer_,
+ address teller_
+ ) external onlyRole("emissions_admin") {
+ // Bond contracts cannot be set to the zero address
+ if (auctioneer_ == address(0)) revert InvalidParam("auctioneer");
+ if (teller_ == address(0)) revert InvalidParam("teller");
+
+ auctioneer = IBondSDA(auctioneer_);
+ teller = teller_;
+ }
+
+ // =========- 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 += Clearinghouse(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 ** _ohmDecimals;
+ }
+
+ /// @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 getNextSale()
+ 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
+ }
+ }
+}
From 56b8f3b678b9425078a329d1788b61d44162d0fd Mon Sep 17 00:00:00 2001
From: ohmzeus <93612359+chefomi@users.noreply.github.com>
Date: Fri, 29 Nov 2024 08:16:11 -0500
Subject: [PATCH 02/64] Update CDsEmissions.sol
---
src/policies/CDsEmissions.sol | 84 +++++++++++++++++++++++++----------
1 file changed, 61 insertions(+), 23 deletions(-)
diff --git a/src/policies/CDsEmissions.sol b/src/policies/CDsEmissions.sol
index d8d1718a..1ecdf97a 100644
--- a/src/policies/CDsEmissions.sol
+++ b/src/policies/CDsEmissions.sol
@@ -26,7 +26,7 @@ interface Clearinghouse {
function principalReceivables() external view returns (uint256);
}
-contract EmissionManager is Policy, RolesConsumer {
+contract CDEmissionManager is Policy, RolesConsumer {
using FullMath for uint256;
// ========== ERRORS ========== //
@@ -42,7 +42,7 @@ contract EmissionManager is Policy, RolesConsumer {
// ========== EVENTS ========== //
- event SaleCreated(uint256 marketID, uint256 saleAmount);
+ event SaleCreated(uint256 marketID, uint256 saleAmount, bool isCD);
event BackingUpdated(uint256 newBacking, uint256 supplyAdded, uint256 reservesAdded);
// ========== DATA STRUCTURES ========== //
@@ -75,6 +75,7 @@ contract EmissionManager is Policy, RolesConsumer {
// External contracts
IBondSDA public auctioneer;
address public teller;
+ address public cdEscrow; // Holds CD funds prior to conversion or reversion
// Manager variables
uint256 public baseEmissionRate;
@@ -83,7 +84,9 @@ contract EmissionManager is Policy, RolesConsumer {
uint256 public backing;
uint8 public beatCounter;
bool public locallyActive;
- uint256 public activeMarketId;
+ uint256 public activeBondMarketId;
+ uint256 public activeCDMarketId;
+ uint256 public minimumSpread; // Min % above current price for CD market
uint8 internal _oracleDecimals;
uint8 internal immutable _ohmDecimals;
@@ -173,13 +176,18 @@ contract EmissionManager is Policy, RolesConsumer {
else baseEmissionRate -= rateChange.changeBy;
}
- // It then calculates the amount to sell for the coming day
- (, , uint256 sell) = getNextSale();
-
+ uint256 bond = getUnsold();
// And then opens a market if applicable
- if (sell != 0) {
- MINTR.increaseMintApproval(address(this), sell);
- _createMarket(sell);
+ if (bond != 0) {
+ // Already has mint approval from prior day
+ _createMarket(bond);
+ }
+
+ // It then calculates the amount to sell for the coming day
+ (, , uint256 cd) = getNextSale();
+ if (cd != 0) {
+ MINTR.increaseMintApproval(address(this), cd);
+ _createCDMarket(cd);
}
}
@@ -223,43 +231,69 @@ contract EmissionManager is Policy, RolesConsumer {
// ========== BOND CALLBACK ========== //
+ /// @dev Bond logic remains the same, CD proceeds go to CD escrow
/// @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();
+ if (id_ != activeBondMarketId || id_ != activeCDMarketId) 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_);
+ address reserveTo;
+ address outputTo;
+ if (id_ == activeBondMarketId) {
+ reserveTo = address(TRSRY);
+ outputTo = teller;
+
+ // 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_);
+ } else {
+ reserveTo = cdEscrow;
+ outputTo = cdEscrow;
+ }
// 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));
+ sReserve.deposit(reserveBalance, reserveTo);
// Mint the output amount of OHM to the Teller
- MINTR.mintOhm(teller, outputAmount_);
+ MINTR.mintOhm(outputTo, outputAmount_);
}
// ========== INTERNAL FUNCTIONS ========== //
+ function _createBondMarket(uint256 saleAmount) internal {
+ uint256 minPrice = ((ONE_HUNDRED_PERCENT + minimumPremium) * backing) /
+ 10 ** _reserveDecimals;
+
+ activeBondMarketId = _createMarket(saleAmount, minPrice);
+ }
+
+ function _createCDMarket(uint256 saleAmount) internal {
+ uint256 minPrice = (PRICE.getLastPrice * minimumSpread) / 10 ** _reserveDecimals;
+
+ activeCDMarketId = _createMarket(saleAmount, minPrice);
+ }
+
/// @notice create bond protocol market with given budget
/// @param saleAmount amount of DAI to fund bond market with
- function _createMarket(uint256 saleAmount) internal {
+ function _createMarket(
+ uint256 saleAmount,
+ uint256 minPrice,
+ bool isCD
+ ) internal returns (uint256 id) {
// 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);
int8 scaleAdjustment = int8(_ohmDecimals) - int8(_reserveDecimals) + (priceDecimals / 2);
@@ -271,10 +305,10 @@ contract EmissionManager is Policy, RolesConsumer {
);
// Create new bond market to buy the reserve with OHM
- activeMarketId = auctioneer.createMarket(
+ id = auctioneer.createMarket(
abi.encode(
IBondSDA.MarketParams({
- payoutToken: ohm,
+ payoutToken: isCD ? ohm : cdToken, // idt this works
quoteToken: reserve,
callbackAddr: address(this),
capacityInQuote: false,
@@ -290,7 +324,7 @@ contract EmissionManager is Policy, RolesConsumer {
)
);
- emit SaleCreated(activeMarketId, saleAmount);
+ emit SaleCreated(activeBondMarketId, saleAmount, isCD);
}
/// @notice allow emission manager to update backing price based on new supply and reserves added
@@ -466,4 +500,8 @@ contract EmissionManager is Policy, RolesConsumer {
emission = (getSupply() * emissionRate) / 10 ** _ohmDecimals; // OHM Scale * OHM Scale / OHM Scale = OHM Scale
}
}
+
+ function getUnsold() public view returns (uint256 tokens) {
+ // if CD sale undersubscribed, return amount of ohm that was not sold
+ }
}
From 49ecd6d07c9b53fe754c472878868cec402828f7 Mon Sep 17 00:00:00 2001
From: ohmzeus <93612359+chefomi@users.noreply.github.com>
Date: Tue, 3 Dec 2024 23:05:41 -0500
Subject: [PATCH 03/64] Auctioneer + Facility
---
src/policies/CDAuctioneer.sol | 179 ++++++++++++
src/policies/CDFacility.sol | 210 ++++++++++++++
src/policies/CDsEmissions.sol | 507 ----------------------------------
3 files changed, 389 insertions(+), 507 deletions(-)
create mode 100644 src/policies/CDAuctioneer.sol
create mode 100644 src/policies/CDFacility.sol
delete mode 100644 src/policies/CDsEmissions.sol
diff --git a/src/policies/CDAuctioneer.sol b/src/policies/CDAuctioneer.sol
new file mode 100644
index 00000000..6f53966c
--- /dev/null
+++ b/src/policies/CDAuctioneer.sol
@@ -0,0 +1,179 @@
+// SPDX-License-Identifier: AGPL-3.0
+pragma solidity 0.8.15;
+
+import "src/Kernel.sol";
+
+import {ReentrancyGuard} from "solmate/utils/ReentrancyGuard.sol";
+import {ERC20} from "solmate/tokens/ERC20.sol";
+
+import {RolesConsumer, ROLESv1} from "modules/ROLES/OlympusRoles.sol";
+
+import {FullMath} from "libraries/FullMath.sol";
+
+import {CDFacility} from "./CDFacility.sol";
+
+interface CDRC20 {
+ function mint(address to, uint256 amount) external;
+ function burn(address from, uint256 amount) external;
+ function convertFor(uint256 amount) external view returns (uint256);
+ function expiry() external view returns (uint256);
+}
+
+contract CDAuctioneer is Policy, RolesConsumer {
+ using FullMath for uint256;
+
+ // ========== DATA STRUCTURES ========== //
+
+ struct Tick {
+ uint256 price;
+ uint256 capacity;
+ }
+
+ // ========== EVENTS ========== //
+
+ // ========== STATE VARIABLES ========== //
+
+ Tick public currentTick;
+ uint256 public target; // number of ohm per day
+ uint256 public tickSize; // number of ohm in a tick
+ uint256 public tickStep; // percentage increase (decrease) per tick
+ uint256 public lastUpdate; // timestamp of last update to current tick
+ uint256 public currentExpiry; // current CD token expiry
+ uint256 public timeBetweenExpiries; // time between CD token expiries
+ uint256 public minPrice; // minimum tick price
+
+ uint256 public decimals;
+
+ CDFacility public cdFacility;
+
+ mapping(uint256 => mapping(uint256 => address)) public cdTokens; // mapping(expiry => price => token)
+
+ // ========== SETUP ========== //
+
+ constructor(Kernel kernel_) Policy(kernel_) {}
+
+ function configureDependencies() external override returns (Keycode[] memory dependencies) {
+ dependencies = new Keycode[](1);
+ dependencies[2] = toKeycode("ROLES");
+
+ ROLES = ROLESv1(getModuleAddress(dependencies[0]));
+ }
+
+ function requestPermissions()
+ external
+ view
+ override
+ returns (Permissions[] memory permissions)
+ {}
+
+ // ========== AUCTION ========== //
+
+ /// @notice use a deposit to bid for CDs
+ /// @param deposit amount of reserve tokens
+ /// @return tokens CD tokens minted to user
+ /// @return amounts amounts of CD tokens minted to user
+ function bid(
+ uint256 deposit
+ ) external returns (CDRC20[] memory tokens, uint256[] memory amounts) {
+ // update state
+ currentTick = getCurrentTick();
+ currentExpiry = getCurrentExpiry();
+ lastUpdate = block.timestamp;
+
+ uint256 i;
+
+ // iterate until user has no more reserves to bid
+ while (deposit > 0) {
+ // get CD token for tick price
+ CDRC20 token = CDRC20(tokenFor(currentTick.price));
+
+ // handle spent/capacity for tick
+ uint256 amount = currentTick.capacity < token.convertFor(deposit) ? tickSize : deposit;
+ if (amount != tickSize) currentTick.capacity -= amount;
+
+ // mint amount of CD token
+ cdFacility.addNewCD(msg.sender, amount, token);
+
+ // decrement bid and increment tick price
+ deposit -= amount;
+ currentTick.price *= tickStep / decimals;
+
+ // add to return arrays
+ tokens[i] = token;
+ amounts[i] = token.convertFor(amount);
+ ++i;
+ }
+ }
+
+ // ========== INTERNAL FUNCTIONS ========== //
+
+ /// @notice create, or return address for existing, CD token
+ /// @param price tick price of CD token
+ /// @return token address of CD token
+ function tokenFor(uint256 price) internal returns (address token) {
+ token = cdTokens[currentExpiry][price];
+ if (token == address(0)) {
+ // new token
+ }
+ }
+
+ // ========== VIEW FUNCTIONS ========== //
+
+ /// @notice get current tick info
+ /// @dev time passing changes tick info
+ /// @return tick info in Tick struct
+ function getCurrentTick() public view returns (Tick memory tick) {
+ // find amount of time passed and new capacity to add
+ uint256 timePassed = block.timestamp - lastUpdate;
+ uint256 newCapacity = (target * timePassed) / 1 days;
+
+ tick = currentTick;
+
+ // decrement price while ticks are full
+ while (tick.capacity + newCapacity > tickSize) {
+ newCapacity -= tickSize;
+ tick.price *= decimals / tickStep;
+
+ // tick price does not go below the minimum
+ // tick capacity is full if the min price is exceeded
+ if (tick.price < minPrice) {
+ tick.price = minPrice;
+ newCapacity = tickSize;
+ break;
+ }
+ }
+
+ // decrement capacity by remainder
+ tick.capacity = newCapacity;
+ }
+
+ /// @notice get current new CD expiry
+ /// @return expiry timestamp of expiration
+ function getCurrentExpiry() public view returns (uint256 expiry) {
+ uint256 nextExpiry = currentExpiry + timeBetweenExpiries;
+ expiry = nextExpiry > block.timestamp ? currentExpiry : nextExpiry;
+ }
+
+ // ========== ADMIN FUNCTIONS ========== //
+
+ /// @notice update auction parameters
+ /// @dev only callable by the auction admin
+ /// @param newTarget new target sale per day
+ /// @param newSize new size per tick
+ /// @param newMinPrice new minimum tick price
+ function beat(
+ uint256 newTarget,
+ uint256 newSize,
+ uint256 newMinPrice
+ ) external onlyRole("CD_Auction_Admin") {
+ target = newTarget;
+ tickSize = newSize;
+ minPrice = newMinPrice;
+ }
+
+ /// @notice update time between new CD expiries
+ /// @param newTime number of seconds between expiries
+ function setTimeBetweenExpiries(uint256 newTime) external onlyRole("CD_Auction_Admin") {
+ timeBetweenExpiries = newTime;
+ }
+}
diff --git a/src/policies/CDFacility.sol b/src/policies/CDFacility.sol
new file mode 100644
index 00000000..bccbfc0c
--- /dev/null
+++ b/src/policies/CDFacility.sol
@@ -0,0 +1,210 @@
+// SPDX-License-Identifier: AGPL-3.0
+pragma solidity 0.8.15;
+
+import "src/Kernel.sol";
+
+import {ReentrancyGuard} from "solmate/utils/ReentrancyGuard.sol";
+import {ERC20} from "solmate/tokens/ERC20.sol";
+import {ERC4626} from "solmate/mixins/ERC4626.sol";
+
+import {RolesConsumer, ROLESv1} from "modules/ROLES/OlympusRoles.sol";
+import {MINTRv1} from "modules/MINTR/MINTR.v1.sol";
+import {TRSRYv1} from "modules/TRSRY/TRSRY.v1.sol";
+
+import {FullMath} from "libraries/FullMath.sol";
+
+interface CDRC20 {
+ function mint(address to, uint256 amount) external;
+ function convertFor(uint256 amount) external view returns (uint256);
+}
+
+contract CDFacility is Policy, RolesConsumer {
+ using FullMath for uint256;
+
+ // ========== EVENTS ========== //
+
+ event CreatedCD(address user, uint48 expiry, uint256 deposit, uint256 convert);
+ event ConvertedCD(address user, uint256 deposit, uint256 convert);
+ event ReclaimedCD(address user, uint256 deposit);
+ event SweptYield(address receiver, uint256 amount);
+
+ // ========== DATA STRUCTURES ========== //
+
+ struct ConvertibleDebt {
+ uint256 deposit;
+ uint256 convert;
+ uint256 expiry;
+ }
+
+ // ========== STATE VARIABLES ========== //
+
+ // Modules
+ TRSRYv1 public TRSRY;
+ MINTRv1 public MINTR;
+
+ // Tokens
+ ERC20 public reserve;
+ ERC4626 public sReserve;
+
+ // State variables
+ mapping(address => ConvertibleDebt[]) public cdsFor;
+ uint256 public totalDeposits;
+ uint256 public totalShares;
+
+ // ========== SETUP ========== //
+
+ constructor(Kernel kernel_, address reserve_, address sReserve_) Policy(kernel_) {
+ reserve = ERC20(reserve_);
+ sReserve = ERC4626(sReserve_);
+ }
+
+ function configureDependencies() external override returns (Keycode[] memory dependencies) {
+ dependencies = new Keycode[](3);
+ dependencies[0] = toKeycode("TRSRY");
+ dependencies[1] = toKeycode("MINTR");
+ dependencies[2] = toKeycode("ROLES");
+
+ TRSRY = TRSRYv1(getModuleAddress(dependencies[0]));
+ MINTR = MINTRv1(getModuleAddress(dependencies[1]));
+ ROLES = ROLESv1(getModuleAddress(dependencies[2]));
+ }
+
+ function requestPermissions()
+ external
+ view
+ override
+ returns (Permissions[] memory permissions)
+ {
+ Keycode mintrKeycode = toKeycode("MINTR");
+
+ permissions = new Permissions[](2);
+ permissions[0] = Permissions(mintrKeycode, MINTR.increaseMintApproval.selector);
+ permissions[1] = Permissions(mintrKeycode, MINTR.mintOhm.selector);
+ }
+
+ // ========== EMISSIONS MANAGER ========== //
+
+ /// @notice allow emissions manager to create new convertible debt
+ /// @param user owner of the convertible debt
+ /// @param amount amount of reserve tokens deposited
+ /// @param token CD Token with terms for deposit
+ function addNewCD(
+ address user,
+ uint256 amount,
+ CDRC20 token
+ ) external onlyRole("Emissions_Manager") {
+ // transfer in debt token
+ reserve.transferFrom(user, address(this), amount);
+
+ // deploy debt token into vault
+ totalShares += sReserve.deposit(amount, address(this));
+
+ // add mint approval for conversion
+ MINTR.increaseMintApproval(address(this), token.convertFor(amount));
+
+ // mint CD token
+ token.mint(user, amount);
+ }
+
+ // ========== CD Position Owner ========== //
+
+ /// @notice allow user to convert their convertible debt before expiration
+ /// @param ids IDs of convertible debts to convert
+ /// @return totalDeposit total reserve tokens relinquished
+ /// @return totalConvert total convert tokens sent
+ function convertCD(
+ CDRC20[] memory tokens,
+ uint256[] memory amounts
+ ) external returns (uint256 totalDeposit, uint256 totalConvert) {
+ // iterate through list of ids, add to totals, and delete cd entries
+ for (uint256 i; i < ids.length; ++i) {
+ ConvertibleDebt memory cd = cdsFor[msg.sender][i];
+ if (cd.convert > 0 && cd.expiry <= block.timestamp) {
+ totalDeposit += cd.deposit;
+ totalConvert += cd.convert;
+ delete cdsFor[msg.sender][i];
+ }
+ }
+
+ // compute shares to send
+ uint256 shares = sReserve.previewWithdraw(totalDeposit);
+ totalShares -= shares;
+
+ // mint convert token, and send wrapped debt token to treasury
+ MINTR.mintOhm(msg.sender, totalConvert);
+ sReserve.transfer(address(TRSRY), shares);
+
+ emit ConvertedCD(msg.sender, totalDeposit, totalConvert);
+ }
+
+ /// @notice allow user to reclaim their convertible debt deposits after expiration
+ /// @param ids IDs of convertible debts to reclaim
+ /// @return totalDeposit total reserve tokens relinquished
+ function reclaimDeposit(uint256[] memory ids) external returns (uint256 totalDeposit) {
+ // iterate through list of ids, add to total, and delete cd entries
+ for (uint256 i; i < ids.length; ++i) {
+ ConvertibleDebt memory cd = cdsFor[msg.sender][i];
+ if (cd.expiry > block.timestamp) {
+ // reduce mint approval
+ MINTR.decreaseMintApproval(address(this), cd.convert);
+
+ totalDeposit += cd.deposit;
+ delete cdsFor[msg.sender][i];
+ }
+ }
+
+ // compute shares to redeem
+ uint256 shares = sReserve.previewWithdraw(totalDeposit);
+ totalShares -= shares;
+
+ // undeploy and return debt token to user
+ sReserve.redeem(shares, msg.sender, address(this));
+
+ emit ReclaimedCD(msg.sender, totalDeposit);
+ }
+
+ // ========== YIELD MANAGER ========== //
+
+ /// @notice allow yield manager to sweep yield accrued on reserves
+ /// @return yield yield in reserve token terms
+ /// @return shares yield in sReserve token terms
+ function sweepYield()
+ external
+ onlyRole("CD_Yield_Manager")
+ returns (uint256 yield, uint256 shares)
+ {
+ yield = sReserve.previewRedeem(totalShares) - totalDeposits;
+ shares = sReserve.previewWithdraw(yield);
+ sReserve.transfer(msg.sender, shares);
+
+ emit SweptYield(msg.sender, yield);
+ }
+
+ // ========== VIEW FUNCTIONS ========== //
+
+ /// @notice get yield accrued on deposited reserve tokens
+ /// @return yield in reserve token terms
+ function yieldAccrued() external view returns (uint256) {
+ return sReserve.previewRedeem(totalShares) - totalDeposits;
+ }
+
+ /// @notice return all existing CD IDs for user
+ /// @param user to search for
+ /// @return ids for user
+ function idsForUser(address user) external view returns (uint256[] memory ids) {
+ uint256 j;
+ for (uint256 i; i < cdsFor[user].length; ++i) {
+ ConvertibleDebt memory cd = cdsFor[user][i];
+ if (cd.deposit > 0) ids[j] = i;
+ ++j;
+ }
+ }
+
+ /// @notice query whether a given CD ID is expired
+ /// @param user who holds the CD
+ /// @param id of the CD to query
+ /// @return status whether the CD is expired
+ function idExpired(address user, uint256 id) external view returns (bool status) {
+ status = cdsFor[user][id].expiry > block.timestamp;
+ }
+}
diff --git a/src/policies/CDsEmissions.sol b/src/policies/CDsEmissions.sol
deleted file mode 100644
index 1ecdf97a..00000000
--- a/src/policies/CDsEmissions.sol
+++ /dev/null
@@ -1,507 +0,0 @@
-// SPDX-License-Identifier: AGPL-3.0
-pragma solidity 0.8.15;
-
-import "src/Kernel.sol";
-
-import {ReentrancyGuard} from "solmate/utils/ReentrancyGuard.sol";
-import {ERC20} from "solmate/tokens/ERC20.sol";
-import {ERC4626} from "solmate/mixins/ERC4626.sol";
-
-import {FullMath} from "libraries/FullMath.sol";
-
-import {IBondSDA} from "interfaces/IBondSDA.sol";
-import {IgOHM} from "interfaces/IgOHM.sol";
-
-import {RolesConsumer, ROLESv1} from "modules/ROLES/OlympusRoles.sol";
-import {TRSRYv1} from "modules/TRSRY/TRSRY.v1.sol";
-import {PRICEv1} from "modules/PRICE/PRICE.v1.sol";
-import {MINTRv1} from "modules/MINTR/MINTR.v1.sol";
-import {CHREGv1} from "modules/CHREG/CHREG.v1.sol";
-
-interface BurnableERC20 {
- function burn(uint256 amount) external;
-}
-
-interface Clearinghouse {
- function principalReceivables() external view returns (uint256);
-}
-
-contract CDEmissionManager is Policy, RolesConsumer {
- using FullMath for uint256;
-
- // ========== ERRORS ========== //
-
- error OnlyTeller();
- error InvalidMarket();
- error InvalidCallback();
- error InvalidParam(string parameter);
- error CannotRestartYet(uint48 availableAt);
- error RestartTimeframePassed();
- error NotActive();
- error AlreadyActive();
-
- // ========== EVENTS ========== //
-
- event SaleCreated(uint256 marketID, uint256 saleAmount, bool isCD);
- event BackingUpdated(uint256 newBacking, uint256 supplyAdded, uint256 reservesAdded);
-
- // ========== DATA STRUCTURES ========== //
-
- struct BaseRateChange {
- uint256 changeBy;
- uint48 beatsLeft;
- bool addition;
- }
-
- // ========== STATE VARIABLES ========== //
-
- /// @notice active base emissions rate change information
- /// @dev active until beatsLeft is 0
- BaseRateChange public rateChange;
-
- // Modules
- TRSRYv1 public TRSRY;
- PRICEv1 public PRICE;
- MINTRv1 public MINTR;
- CHREGv1 public CHREG;
-
- // Tokens
- // solhint-disable const-name-snakecase
- ERC20 public immutable ohm;
- IgOHM public immutable gohm;
- ERC20 public immutable reserve;
- ERC4626 public immutable sReserve;
-
- // External contracts
- IBondSDA public auctioneer;
- address public teller;
- address public cdEscrow; // Holds CD funds prior to conversion or reversion
-
- // Manager variables
- uint256 public baseEmissionRate;
- uint256 public minimumPremium;
- uint48 public vestingPeriod; // initialized at 0
- uint256 public backing;
- uint8 public beatCounter;
- bool public locallyActive;
- uint256 public activeBondMarketId;
- uint256 public activeCDMarketId;
- uint256 public minimumSpread; // Min % above current price for CD market
-
- uint8 internal _oracleDecimals;
- uint8 internal immutable _ohmDecimals;
- uint8 internal immutable _reserveDecimals;
-
- /// @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;
-
- uint256 internal constant ONE_HUNDRED_PERCENT = 1e18;
-
- // ========== SETUP ========== //
-
- constructor(
- Kernel kernel_,
- address ohm_,
- address gohm_,
- address reserve_,
- address sReserve_,
- address auctioneer_,
- address teller_
- ) Policy(kernel_) {
- // Set immutable variables
- if (ohm_ == address(0)) revert("OHM address cannot be 0");
- if (gohm_ == address(0)) revert("gOHM address cannot be 0");
- if (reserve_ == address(0)) revert("DAI address cannot be 0");
- if (sReserve_ == address(0)) revert("sDAI address cannot be 0");
- if (auctioneer_ == address(0)) revert("Auctioneer address cannot be 0");
-
- ohm = ERC20(ohm_);
- gohm = IgOHM(gohm_);
- reserve = ERC20(reserve_);
- sReserve = ERC4626(sReserve_);
- auctioneer = IBondSDA(auctioneer_);
- teller = teller_;
-
- _ohmDecimals = ohm.decimals();
- _reserveDecimals = reserve.decimals();
-
- // Max approve sReserve contract for reserve for deposits
- reserve.approve(address(sReserve), type(uint256).max);
- }
-
- 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();
- }
-
- function requestPermissions()
- external
- view
- override
- returns (Permissions[] memory permissions)
- {
- Keycode mintrKeycode = toKeycode("MINTR");
-
- permissions = new Permissions[](2);
- permissions[0] = Permissions(mintrKeycode, MINTR.increaseMintApproval.selector);
- permissions[1] = Permissions(mintrKeycode, MINTR.mintOhm.selector);
- }
-
- // ========== HEARTBEAT ========== //
-
- /// @notice calculate and execute sale, if applicable, once per day (every 3 beats)
- function execute() external onlyRole("heart") {
- if (!locallyActive) return;
-
- beatCounter = ++beatCounter % 3;
- if (beatCounter != 0) return;
-
- if (rateChange.beatsLeft != 0) {
- --rateChange.beatsLeft;
- if (rateChange.addition) baseEmissionRate += rateChange.changeBy;
- else baseEmissionRate -= rateChange.changeBy;
- }
-
- uint256 bond = getUnsold();
- // And then opens a market if applicable
- if (bond != 0) {
- // Already has mint approval from prior day
- _createMarket(bond);
- }
-
- // It then calculates the amount to sell for the coming day
- (, , uint256 cd) = getNextSale();
- if (cd != 0) {
- MINTR.increaseMintApproval(address(this), cd);
- _createCDMarket(cd);
- }
- }
-
- // ========== INITIALIZE ========== //
-
- /// @notice allow governance to initialize the emission manager
- /// @param baseEmissionsRate_ percent of OHM supply to issue per day at the minimum premium, in OHM scale, i.e. 1e9 = 100%
- /// @param minimumPremium_ minimum premium at which to issue OHM, a percentage where 1e18 is 100%
- /// @param backing_ backing price of OHM in reserve token, in reserve scale
- /// @param restartTimeframe_ time in seconds that the manager needs to be restarted after a shutdown, otherwise it must be re-initialized
- function initialize(
- uint256 baseEmissionsRate_,
- uint256 minimumPremium_,
- uint256 backing_,
- uint48 restartTimeframe_
- ) external onlyRole("emissions_admin") {
- // Cannot initialize if currently active
- if (locallyActive) revert AlreadyActive();
-
- // 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 inputs
- if (baseEmissionsRate_ == 0) revert InvalidParam("baseEmissionRate");
- if (minimumPremium_ == 0) revert InvalidParam("minimumPremium");
- if (backing_ == 0) revert InvalidParam("backing");
- if (restartTimeframe_ == 0) revert InvalidParam("restartTimeframe");
-
- // Assign
- baseEmissionRate = baseEmissionsRate_;
- minimumPremium = minimumPremium_;
- backing = backing_;
- restartTimeframe = restartTimeframe_;
-
- // Activate
- locallyActive = true;
- }
-
- // ========== BOND CALLBACK ========== //
-
- /// @dev Bond logic remains the same, CD proceeds go to CD escrow
- /// @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_ != activeBondMarketId || id_ != activeCDMarketId) revert InvalidMarket();
-
- // Reserve balance should have increased by atleast the input amount
- uint256 reserveBalance = reserve.balanceOf(address(this));
- if (reserveBalance < inputAmount_) revert InvalidCallback();
-
- address reserveTo;
- address outputTo;
- if (id_ == activeBondMarketId) {
- reserveTo = address(TRSRY);
- outputTo = teller;
-
- // 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_);
- } else {
- reserveTo = cdEscrow;
- outputTo = cdEscrow;
- }
-
- // 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, reserveTo);
-
- // Mint the output amount of OHM to the Teller
- MINTR.mintOhm(outputTo, outputAmount_);
- }
-
- // ========== INTERNAL FUNCTIONS ========== //
-
- function _createBondMarket(uint256 saleAmount) internal {
- uint256 minPrice = ((ONE_HUNDRED_PERCENT + minimumPremium) * backing) /
- 10 ** _reserveDecimals;
-
- activeBondMarketId = _createMarket(saleAmount, minPrice);
- }
-
- function _createCDMarket(uint256 saleAmount) internal {
- uint256 minPrice = (PRICE.getLastPrice * minimumSpread) / 10 ** _reserveDecimals;
-
- activeCDMarketId = _createMarket(saleAmount, minPrice);
- }
-
- /// @notice create bond protocol market with given budget
- /// @param saleAmount amount of DAI to fund bond market with
- function _createMarket(
- uint256 saleAmount,
- uint256 minPrice,
- bool isCD
- ) internal returns (uint256 id) {
- // 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
- int8 priceDecimals = _getPriceDecimals(minPrice);
- int8 scaleAdjustment = int8(_ohmDecimals) - int8(_reserveDecimals) + (priceDecimals / 2);
-
- // Calculate oracle scale and bond scale with scale adjustment and format prices for bond market
- uint256 oracleScale = 10 ** uint8(int8(_oracleDecimals) - priceDecimals);
- uint256 bondScale = 10 **
- uint8(
- 36 + scaleAdjustment + int8(_reserveDecimals) - int8(_ohmDecimals) - priceDecimals
- );
-
- // Create new bond market to buy the reserve with OHM
- id = auctioneer.createMarket(
- abi.encode(
- IBondSDA.MarketParams({
- payoutToken: isCD ? ohm : cdToken, // idt this works
- 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(activeBondMarketId, saleAmount, isCD);
- }
-
- /// @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.
- return decimals - int8(_oracleDecimals);
- }
-
- // ========== ADMIN FUNCTIONS ========== //
-
- /// @notice shutdown the emission manager locally, burn OHM, and return any reserves to TRSRY
- function shutdown() external onlyRole("emergency_shutdown") {
- locallyActive = false;
- shutdownTimestamp = uint48(block.timestamp);
-
- uint256 ohmBalance = ohm.balanceOf(address(this));
- if (ohmBalance > 0) BurnableERC20(address(ohm)).burn(ohmBalance);
-
- uint256 reserveBalance = reserve.balanceOf(address(this));
- if (reserveBalance > 0) sReserve.deposit(reserveBalance, address(TRSRY));
- }
-
- /// @notice restart the emission manager locally
- function restart() external onlyRole("emergency_restart") {
- // Restart can be activated only within the specified timeframe since shutdown
- // Outside of this span of time, emissions_admin must reinitialize
- if (uint48(block.timestamp) >= shutdownTimestamp + restartTimeframe)
- revert RestartTimeframePassed();
-
- locallyActive = true;
- }
-
- /// @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 onlyRole("emissions_admin") {
- // 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_, forNumBeats_, add);
- }
-
- /// @notice set the minimum premium for emissions
- /// @param newMinimumPremium_ uint256
- function setMinimumPremium(uint256 newMinimumPremium_) external onlyRole("emissions_admin") {
- if (newMinimumPremium_ == 0) revert InvalidParam("newMinimumPremium");
-
- minimumPremium = newMinimumPremium_;
- }
-
- /// @notice set the new vesting period in seconds
- /// @param newVestingPeriod_ uint48
- function setVestingPeriod(uint48 newVestingPeriod_) external onlyRole("emissions_admin") {
- // 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_;
- }
-
- /// @notice allow governance to adjust backing price if deviated from reality
- /// @dev 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 onlyRole("emissions_admin") {
- // Backing cannot be reduced by more than 10% at a time
- if (newBacking < (backing * 9) / 10) revert InvalidParam("newBacking");
- backing = newBacking;
- }
-
- /// @notice allow governance to adjust the timeframe for restart after shutdown
- /// @param newTimeframe to adjust it to
- function setRestartTimeframe(uint48 newTimeframe) external onlyRole("emissions_admin") {
- // Restart timeframe must be greater than 0
- if (newTimeframe == 0) revert InvalidParam("newRestartTimeframe");
-
- restartTimeframe = newTimeframe;
- }
-
- /// @notice allow governance to set the bond contracts used by the emission manager
- /// @param auctioneer_ address of the bond auctioneer contract
- /// @param teller_ address of the bond teller contract
- function setBondContracts(
- address auctioneer_,
- address teller_
- ) external onlyRole("emissions_admin") {
- // Bond contracts cannot be set to the zero address
- if (auctioneer_ == address(0)) revert InvalidParam("auctioneer");
- if (teller_ == address(0)) revert InvalidParam("teller");
-
- auctioneer = IBondSDA(auctioneer_);
- teller = teller_;
- }
-
- // =========- 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 += Clearinghouse(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 ** _ohmDecimals;
- }
-
- /// @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 getNextSale()
- 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
- }
- }
-
- function getUnsold() public view returns (uint256 tokens) {
- // if CD sale undersubscribed, return amount of ohm that was not sold
- }
-}
From 356af037023fba70ab80ef47db20be1cc638c36c Mon Sep 17 00:00:00 2001
From: ohmzeus <93612359+chefomi@users.noreply.github.com>
Date: Thu, 5 Dec 2024 23:08:46 -0500
Subject: [PATCH 04/64] updates
---
src/policies/CDAuctioneer.sol | 120 ++++++++++------------
src/policies/CDFacility.sol | 188 ++++++++++++++++++++++------------
2 files changed, 174 insertions(+), 134 deletions(-)
diff --git a/src/policies/CDAuctioneer.sol b/src/policies/CDAuctioneer.sol
index 6f53966c..a8bee333 100644
--- a/src/policies/CDAuctioneer.sol
+++ b/src/policies/CDAuctioneer.sol
@@ -24,6 +24,15 @@ contract CDAuctioneer is Policy, RolesConsumer {
// ========== DATA STRUCTURES ========== //
+ struct State {
+ uint256 target; // number of ohm per day
+ uint256 tickSize; // number of ohm in a tick
+ uint256 tickStep; // percentage increase (decrease) per tick
+ uint256 minPrice; // minimum tick price
+ uint256 timeToExpiry; // time between creation and expiry of deposit
+ uint256 lastUpdate; // timestamp of last update to current tick
+ }
+
struct Tick {
uint256 price;
uint256 capacity;
@@ -34,13 +43,7 @@ contract CDAuctioneer is Policy, RolesConsumer {
// ========== STATE VARIABLES ========== //
Tick public currentTick;
- uint256 public target; // number of ohm per day
- uint256 public tickSize; // number of ohm in a tick
- uint256 public tickStep; // percentage increase (decrease) per tick
- uint256 public lastUpdate; // timestamp of last update to current tick
- uint256 public currentExpiry; // current CD token expiry
- uint256 public timeBetweenExpiries; // time between CD token expiries
- uint256 public minPrice; // minimum tick price
+ State public state;
uint256 public decimals;
@@ -69,76 +72,53 @@ contract CDAuctioneer is Policy, RolesConsumer {
// ========== AUCTION ========== //
/// @notice use a deposit to bid for CDs
- /// @param deposit amount of reserve tokens
- /// @return tokens CD tokens minted to user
- /// @return amounts amounts of CD tokens minted to user
- function bid(
- uint256 deposit
- ) external returns (CDRC20[] memory tokens, uint256[] memory amounts) {
+ /// @param deposit amount of reserve tokens
+ /// @return convertable amount of convertable tokens
+ function bid(uint256 deposit) external returns (uint256 convertable) {
// update state
currentTick = getCurrentTick();
- currentExpiry = getCurrentExpiry();
- lastUpdate = block.timestamp;
-
- uint256 i;
+ state.lastUpdate = block.timestamp;
// iterate until user has no more reserves to bid
while (deposit > 0) {
- // get CD token for tick price
- CDRC20 token = CDRC20(tokenFor(currentTick.price));
-
// handle spent/capacity for tick
- uint256 amount = currentTick.capacity < token.convertFor(deposit) ? tickSize : deposit;
- if (amount != tickSize) currentTick.capacity -= amount;
-
- // mint amount of CD token
- cdFacility.addNewCD(msg.sender, amount, token);
+ uint256 amount = currentTick.capacity < convertFor(deposit, currentTick.price)
+ ? state.tickSize
+ : deposit;
+ if (amount != state.tickSize) currentTick.capacity -= amount;
// decrement bid and increment tick price
deposit -= amount;
- currentTick.price *= tickStep / decimals;
-
- // add to return arrays
- tokens[i] = token;
- amounts[i] = token.convertFor(amount);
- ++i;
+ currentTick.price *= state.tickStep / decimals;
+ convertable += convertFor(amount, currentTick.price);
}
- }
- // ========== INTERNAL FUNCTIONS ========== //
-
- /// @notice create, or return address for existing, CD token
- /// @param price tick price of CD token
- /// @return token address of CD token
- function tokenFor(uint256 price) internal returns (address token) {
- token = cdTokens[currentExpiry][price];
- if (token == address(0)) {
- // new token
- }
+ // mint amount of CD token
+ cdFacility.addNewCD(msg.sender, deposit, convertable, block.timestamp + state.timeToExpiry);
}
// ========== VIEW FUNCTIONS ========== //
/// @notice get current tick info
- /// @dev time passing changes tick info
+ /// @dev time passing changes tick info
/// @return tick info in Tick struct
function getCurrentTick() public view returns (Tick memory tick) {
// find amount of time passed and new capacity to add
- uint256 timePassed = block.timestamp - lastUpdate;
- uint256 newCapacity = (target * timePassed) / 1 days;
+ uint256 timePassed = block.timestamp - state.lastUpdate;
+ uint256 newCapacity = (state.target * timePassed) / 1 days;
tick = currentTick;
// decrement price while ticks are full
- while (tick.capacity + newCapacity > tickSize) {
- newCapacity -= tickSize;
- tick.price *= decimals / tickStep;
+ while (tick.capacity + newCapacity > state.tickSize) {
+ newCapacity -= state.tickSize;
+ tick.price *= decimals / state.tickStep;
// tick price does not go below the minimum
// tick capacity is full if the min price is exceeded
- if (tick.price < minPrice) {
- tick.price = minPrice;
- newCapacity = tickSize;
+ if (tick.price < state.minPrice) {
+ tick.price = state.minPrice;
+ newCapacity = state.tickSize;
break;
}
}
@@ -147,33 +127,39 @@ contract CDAuctioneer is Policy, RolesConsumer {
tick.capacity = newCapacity;
}
- /// @notice get current new CD expiry
- /// @return expiry timestamp of expiration
- function getCurrentExpiry() public view returns (uint256 expiry) {
- uint256 nextExpiry = currentExpiry + timeBetweenExpiries;
- expiry = nextExpiry > block.timestamp ? currentExpiry : nextExpiry;
+ /// @notice get amount of cdOHM for a deposit at a tick price
+ /// @return amount convertable
+ function convertFor(uint256 deposit, uint256 price) public view returns (uint256) {
+ return (deposit * decimals) / price;
}
// ========== ADMIN FUNCTIONS ========== //
/// @notice update auction parameters
- /// @dev only callable by the auction admin
- /// @param newTarget new target sale per day
- /// @param newSize new size per tick
- /// @param newMinPrice new minimum tick price
+ /// @dev only callable by the auction admin
+ /// @param newTarget new target sale per day
+ /// @param newSize new size per tick
+ /// @param newStep new percentage change per tick
+ /// @param newMinPrice new minimum tick price
function beat(
uint256 newTarget,
uint256 newSize,
+ uint256 newStep,
uint256 newMinPrice
) external onlyRole("CD_Auction_Admin") {
- target = newTarget;
- tickSize = newSize;
- minPrice = newMinPrice;
+ state = State(
+ newTarget,
+ newSize,
+ newStep,
+ newMinPrice,
+ state.timeToExpiry,
+ state.lastUpdate
+ );
}
- /// @notice update time between new CD expiries
- /// @param newTime number of seconds between expiries
- function setTimeBetweenExpiries(uint256 newTime) external onlyRole("CD_Auction_Admin") {
- timeBetweenExpiries = newTime;
+ /// @notice update time between creation and expiry of deposit
+ /// @param newTime number of seconds
+ function setTimeToExpiry(uint256 newTime) external onlyRole("CD_Admin") {
+ state.timeToExpiry = newTime;
}
}
diff --git a/src/policies/CDFacility.sol b/src/policies/CDFacility.sol
index bccbfc0c..dc0c3a53 100644
--- a/src/policies/CDFacility.sol
+++ b/src/policies/CDFacility.sol
@@ -15,12 +15,22 @@ import {FullMath} from "libraries/FullMath.sol";
interface CDRC20 {
function mint(address to, uint256 amount) external;
+ function burn(address from, uint256 amount) external;
function convertFor(uint256 amount) external view returns (uint256);
+ function expiry() external view returns (uint256);
}
contract CDFacility is Policy, RolesConsumer {
using FullMath for uint256;
+ error Misconfigured();
+
+ struct CD {
+ uint256 deposit;
+ uint256 convertable;
+ uint256 expiry;
+ }
+
// ========== EVENTS ========== //
event CreatedCD(address user, uint48 expiry, uint256 deposit, uint256 convert);
@@ -28,16 +38,11 @@ contract CDFacility is Policy, RolesConsumer {
event ReclaimedCD(address user, uint256 deposit);
event SweptYield(address receiver, uint256 amount);
- // ========== DATA STRUCTURES ========== //
-
- struct ConvertibleDebt {
- uint256 deposit;
- uint256 convert;
- uint256 expiry;
- }
-
// ========== STATE VARIABLES ========== //
+ // Constants
+ uint256 public constant DECIMALS = 1e18;
+
// Modules
TRSRYv1 public TRSRY;
MINTRv1 public MINTR;
@@ -45,11 +50,13 @@ contract CDFacility is Policy, RolesConsumer {
// Tokens
ERC20 public reserve;
ERC4626 public sReserve;
+ CDRC20 public cdUSDS;
// State variables
- mapping(address => ConvertibleDebt[]) public cdsFor;
uint256 public totalDeposits;
uint256 public totalShares;
+ mapping(address => CD[]) public cdInfo;
+ uint256 public redeemRate;
// ========== SETUP ========== //
@@ -85,14 +92,16 @@ contract CDFacility is Policy, RolesConsumer {
// ========== EMISSIONS MANAGER ========== //
/// @notice allow emissions manager to create new convertible debt
- /// @param user owner of the convertible debt
- /// @param amount amount of reserve tokens deposited
- /// @param token CD Token with terms for deposit
+ /// @param user owner of the convertible debt
+ /// @param amount amount of reserve tokens deposited
+ /// @param convertable amount of OHM that can be converted into
+ /// @param expiry timestamp when conversion expires
function addNewCD(
address user,
uint256 amount,
- CDRC20 token
- ) external onlyRole("Emissions_Manager") {
+ uint256 convertable,
+ uint256 expiry
+ ) external onlyRole("CD_Auctioneer") {
// transfer in debt token
reserve.transferFrom(user, address(this), amount);
@@ -100,67 +109,116 @@ contract CDFacility is Policy, RolesConsumer {
totalShares += sReserve.deposit(amount, address(this));
// add mint approval for conversion
- MINTR.increaseMintApproval(address(this), token.convertFor(amount));
+ MINTR.increaseMintApproval(address(this), convertable);
- // mint CD token
- token.mint(user, amount);
+ // store convertable deposit info and mint cdUSDS
+ cdInfo[user].push(CD(amount, convertable, expiry));
+ cdUSDS.mint(user, amount);
}
// ========== CD Position Owner ========== //
/// @notice allow user to convert their convertible debt before expiration
- /// @param ids IDs of convertible debts to convert
- /// @return totalDeposit total reserve tokens relinquished
- /// @return totalConvert total convert tokens sent
+ /// @param cds CD indexes to convert
+ /// @param amounts CD token amounts to convert
function convertCD(
- CDRC20[] memory tokens,
+ uint256[] memory cds,
uint256[] memory amounts
- ) external returns (uint256 totalDeposit, uint256 totalConvert) {
- // iterate through list of ids, add to totals, and delete cd entries
- for (uint256 i; i < ids.length; ++i) {
- ConvertibleDebt memory cd = cdsFor[msg.sender][i];
- if (cd.convert > 0 && cd.expiry <= block.timestamp) {
- totalDeposit += cd.deposit;
- totalConvert += cd.convert;
- delete cdsFor[msg.sender][i];
- }
+ ) external returns (uint256 converted) {
+ if (cds.length != amounts.length) revert Misconfigured();
+
+ uint256 totalDeposit;
+
+ // iterate through and burn CD tokens, adding deposit and conversion amounts to running totals
+ for (uint256 i; i < cds.length; ++i) {
+ CD storage cd = cdInfo[msg.sender][i];
+
+ uint256 amount = amounts[i];
+ uint256 converting = ((cd.convertable * amount) / cd.deposit);
+
+ // increment running totals
+ totalDeposit += amount;
+ converted += converting;
+
+ // decrement deposit info
+ cd.convertable -= converting; // reverts on overflow
+ cd.deposit -= amount;
}
- // compute shares to send
+ // compute and account for shares to send to treasury
uint256 shares = sReserve.previewWithdraw(totalDeposit);
totalShares -= shares;
- // mint convert token, and send wrapped debt token to treasury
- MINTR.mintOhm(msg.sender, totalConvert);
+ // burn cdUSDS
+ cdUSDS.burn(msg.sender, totalDeposit);
+
+ // mint ohm and send underlying debt token to treasury
+ MINTR.mintOhm(msg.sender, converted);
sReserve.transfer(address(TRSRY), shares);
- emit ConvertedCD(msg.sender, totalDeposit, totalConvert);
+ emit ConvertedCD(msg.sender, totalDeposit, converted);
}
/// @notice allow user to reclaim their convertible debt deposits after expiration
- /// @param ids IDs of convertible debts to reclaim
- /// @return totalDeposit total reserve tokens relinquished
- function reclaimDeposit(uint256[] memory ids) external returns (uint256 totalDeposit) {
- // iterate through list of ids, add to total, and delete cd entries
- for (uint256 i; i < ids.length; ++i) {
- ConvertibleDebt memory cd = cdsFor[msg.sender][i];
- if (cd.expiry > block.timestamp) {
- // reduce mint approval
- MINTR.decreaseMintApproval(address(this), cd.convert);
-
- totalDeposit += cd.deposit;
- delete cdsFor[msg.sender][i];
+ /// @param cds CD indexes to return
+ /// @param amounts amounts of CD tokens to burn
+ /// @return returned total reserve tokens returned
+ function returnDeposit(
+ uint256[] memory cds,
+ uint256[] memory amounts
+ ) external returns (uint256 returned) {
+ if (cds.length != amounts.length) revert Misconfigured();
+
+ uint256 unconverted;
+
+ // iterate through and burn CD tokens, adding deposit and conversion amounts to running totals
+ for (uint256 i; i < cds.length; ++i) {
+ CD memory cd = cdInfo[msg.sender][cds[i]];
+ uint256 amount = amounts[i];
+ uint256 convertable = ((cd.convertable * amount) / cd.deposit);
+
+ if (cd.expiry < block.timestamp) {
+ returned += amount;
+ unconverted += convertable;
+
+ // decrement deposit info
+ cd.convertable -= convertable; // reverts on overflow
+ cd.deposit -= amount;
}
}
+ // burn cdUSDS
+ cdUSDS.burn(msg.sender, returned);
+
// compute shares to redeem
- uint256 shares = sReserve.previewWithdraw(totalDeposit);
+ uint256 shares = sReserve.previewWithdraw(returned);
totalShares -= shares;
- // undeploy and return debt token to user
+ // return debt token to user
sReserve.redeem(shares, msg.sender, address(this));
- emit ReclaimedCD(msg.sender, totalDeposit);
+ // decrease mint approval to reflect tokens that will not convert
+ MINTR.decreaseMintApproval(address(this), unconverted);
+
+ emit ReclaimedCD(msg.sender, returned);
+ }
+
+ // ========== cdUSDS ========== //
+
+ /// @notice allow non cd holder to sell cdUSDS for USDS
+ /// @notice the amount of USDS per cdUSDS is not 1:1
+ /// @notice convertible depositors should use returnDeposit() for 1:1
+ function redeem(uint256 amount) external returns (uint256 tokensOut) {
+ // burn cdUSDS
+ cdUSDS.burn(msg.sender, amount);
+
+ // compute shares to redeem
+ tokensOut = redeemOutput(amount);
+ uint256 shares = sReserve.previewWithdraw(tokensOut);
+ totalShares -= shares;
+
+ // return debt token to user
+ sReserve.redeem(shares, msg.sender, address(this));
}
// ========== YIELD MANAGER ========== //
@@ -180,6 +238,15 @@ contract CDFacility is Policy, RolesConsumer {
emit SweptYield(msg.sender, yield);
}
+ // ========== GOVERNOR ========== //
+
+ /// @notice allow admin to change redeem rate
+ /// @dev redeem rate must be lower than or equal to 1:1
+ function setRedeemRate(uint256 newRate) external onlyRole("CD_Admin") {
+ if (newRate > DECIMALS) revert Misconfigured();
+ redeemRate = newRate;
+ }
+
// ========== VIEW FUNCTIONS ========== //
/// @notice get yield accrued on deposited reserve tokens
@@ -188,23 +255,10 @@ contract CDFacility is Policy, RolesConsumer {
return sReserve.previewRedeem(totalShares) - totalDeposits;
}
- /// @notice return all existing CD IDs for user
- /// @param user to search for
- /// @return ids for user
- function idsForUser(address user) external view returns (uint256[] memory ids) {
- uint256 j;
- for (uint256 i; i < cdsFor[user].length; ++i) {
- ConvertibleDebt memory cd = cdsFor[user][i];
- if (cd.deposit > 0) ids[j] = i;
- ++j;
- }
- }
-
- /// @notice query whether a given CD ID is expired
- /// @param user who holds the CD
- /// @param id of the CD to query
- /// @return status whether the CD is expired
- function idExpired(address user, uint256 id) external view returns (bool status) {
- status = cdsFor[user][id].expiry > block.timestamp;
+ /// @notice amount of deposit tokens out for amount of cdUSDS redeemed
+ /// @param amount of cdUSDS in
+ /// @return output amount of USDS out
+ function redeemOutput(uint256 amount) public view returns (uint256) {
+ return (amount * redeemRate) / DECIMALS;
}
}
From 4d55f12b70fbaa596492d5425490981c0bac1aac Mon Sep 17 00:00:00 2001
From: ohmzeus <93612359+chefomi@users.noreply.github.com>
Date: Sat, 7 Dec 2024 00:38:52 -0500
Subject: [PATCH 05/64] integrate with emissions manager
# Conflicts:
# src/policies/EmissionManager.sol
---
src/policies/CDAuctioneer.sol | 28 +++++++---
src/policies/CDFacility.sol | 32 +++++++++---
src/policies/EmissionManager.sol | 88 +++++++++++++++++++++++++++-----
3 files changed, 120 insertions(+), 28 deletions(-)
diff --git a/src/policies/CDAuctioneer.sol b/src/policies/CDAuctioneer.sol
index a8bee333..4eab7700 100644
--- a/src/policies/CDAuctioneer.sol
+++ b/src/policies/CDAuctioneer.sol
@@ -27,12 +27,17 @@ contract CDAuctioneer is Policy, RolesConsumer {
struct State {
uint256 target; // number of ohm per day
uint256 tickSize; // number of ohm in a tick
- uint256 tickStep; // percentage increase (decrease) per tick
uint256 minPrice; // minimum tick price
+ uint256 tickStep; // percentage increase (decrease) per tick
uint256 timeToExpiry; // time between creation and expiry of deposit
uint256 lastUpdate; // timestamp of last update to current tick
}
+ struct Day {
+ uint256 deposits; // total deposited for day
+ uint256 convertable; // total convertable for day
+ }
+
struct Tick {
uint256 price;
uint256 capacity;
@@ -51,6 +56,8 @@ contract CDAuctioneer is Policy, RolesConsumer {
mapping(uint256 => mapping(uint256 => address)) public cdTokens; // mapping(expiry => price => token)
+ Day public today;
+
// ========== SETUP ========== //
constructor(Kernel kernel_) Policy(kernel_) {}
@@ -86,13 +93,16 @@ contract CDAuctioneer is Policy, RolesConsumer {
? state.tickSize
: deposit;
if (amount != state.tickSize) currentTick.capacity -= amount;
+ else currentTick.price *= state.tickStep / decimals;
// decrement bid and increment tick price
deposit -= amount;
- currentTick.price *= state.tickStep / decimals;
convertable += convertFor(amount, currentTick.price);
}
+ today.deposits += deposit;
+ today.convertable += convertable;
+
// mint amount of CD token
cdFacility.addNewCD(msg.sender, deposit, convertable, block.timestamp + state.timeToExpiry);
}
@@ -139,19 +149,19 @@ contract CDAuctioneer is Policy, RolesConsumer {
/// @dev only callable by the auction admin
/// @param newTarget new target sale per day
/// @param newSize new size per tick
- /// @param newStep new percentage change per tick
/// @param newMinPrice new minimum tick price
function beat(
uint256 newTarget,
uint256 newSize,
- uint256 newStep,
uint256 newMinPrice
- ) external onlyRole("CD_Auction_Admin") {
+ ) external onlyRole("CD_Auction_Admin") returns (uint256 remainder) {
+ remainder = (state.target > today.convertable) ? state.target - today.convertable : 0;
+
state = State(
newTarget,
newSize,
- newStep,
newMinPrice,
+ state.tickStep,
state.timeToExpiry,
state.lastUpdate
);
@@ -162,4 +172,10 @@ contract CDAuctioneer is Policy, RolesConsumer {
function setTimeToExpiry(uint256 newTime) external onlyRole("CD_Admin") {
state.timeToExpiry = newTime;
}
+
+ /// @notice update change between ticks
+ /// @param newStep percentage in decimal terms
+ function setTickStep(uint256 newStep) external onlyRole("CD_Admin") {
+ state.tickStep = newStep;
+ }
}
diff --git a/src/policies/CDFacility.sol b/src/policies/CDFacility.sol
index dc0c3a53..46121ffc 100644
--- a/src/policies/CDFacility.sol
+++ b/src/policies/CDFacility.sol
@@ -18,6 +18,7 @@ interface CDRC20 {
function burn(address from, uint256 amount) external;
function convertFor(uint256 amount) external view returns (uint256);
function expiry() external view returns (uint256);
+ function totalSupply() external view returns (uint256);
}
contract CDFacility is Policy, RolesConsumer {
@@ -132,6 +133,7 @@ contract CDFacility is Policy, RolesConsumer {
// iterate through and burn CD tokens, adding deposit and conversion amounts to running totals
for (uint256 i; i < cds.length; ++i) {
CD storage cd = cdInfo[msg.sender][i];
+ if (cd.expiry < block.timestamp) continue;
uint256 amount = amounts[i];
uint256 converting = ((cd.convertable * amount) / cd.deposit);
@@ -174,17 +176,17 @@ contract CDFacility is Policy, RolesConsumer {
// iterate through and burn CD tokens, adding deposit and conversion amounts to running totals
for (uint256 i; i < cds.length; ++i) {
CD memory cd = cdInfo[msg.sender][cds[i]];
+ if (cd.expiry >= block.timestamp) continue;
+
uint256 amount = amounts[i];
uint256 convertable = ((cd.convertable * amount) / cd.deposit);
- if (cd.expiry < block.timestamp) {
- returned += amount;
- unconverted += convertable;
+ returned += amount;
+ unconverted += convertable;
- // decrement deposit info
- cd.convertable -= convertable; // reverts on overflow
- cd.deposit -= amount;
- }
+ // decrement deposit info
+ cd.deposit -= amount;
+ cd.convertable -= convertable; // reverts on overflow
}
// burn cdUSDS
@@ -205,6 +207,19 @@ contract CDFacility is Policy, RolesConsumer {
// ========== cdUSDS ========== //
+ /// @notice allow user to mint cdUSDS
+ /// @notice redeeming without a CD may be at a discount
+ /// @param amount of reserve token
+ /// @return tokensOut cdUSDS out (1:1 with USDS in)
+ function mint(uint256 amount) external returns (uint256 tokensOut) {
+ tokensOut = amount;
+
+ reserve.transferFrom(msg.sender, address(this), amount);
+ totalShares += sReserve.deposit(amount, msg.sender);
+
+ cdUSDS.mint(msg.sender, amount);
+ }
+
/// @notice allow non cd holder to sell cdUSDS for USDS
/// @notice the amount of USDS per cdUSDS is not 1:1
/// @notice convertible depositors should use returnDeposit() for 1:1
@@ -231,8 +246,9 @@ contract CDFacility is Policy, RolesConsumer {
onlyRole("CD_Yield_Manager")
returns (uint256 yield, uint256 shares)
{
- yield = sReserve.previewRedeem(totalShares) - totalDeposits;
+ yield = sReserve.previewRedeem(totalShares) - cdUSDS.totalSupply();
shares = sReserve.previewWithdraw(yield);
+ totalShares -= shares;
sReserve.transfer(msg.sender, shares);
emit SweptYield(msg.sender, yield);
diff --git a/src/policies/EmissionManager.sol b/src/policies/EmissionManager.sol
index 2afa9493..a8a962af 100644
--- a/src/policies/EmissionManager.sol
+++ b/src/policies/EmissionManager.sol
@@ -19,6 +19,7 @@ import {MINTRv1} from "modules/MINTR/MINTR.v1.sol";
import {CHREGv1} from "modules/CHREG/CHREG.v1.sol";
import {IEmissionManager} from "policies/interfaces/IEmissionManager.sol";
+import {CDAuctioneer} from "./CDAuctioneer.sol";
interface BurnableERC20 {
function burn(uint256 amount) external;
@@ -53,8 +54,9 @@ contract EmissionManager is IEmissionManager, Policy, RolesConsumer {
ERC4626 public immutable sReserve;
// External contracts
- IBondSDA public auctioneer;
+ IBondSDA public bondAuctioneer;
address public teller;
+ CDAuctioneer public cdAuctioneer;
// Manager variables
uint256 public baseEmissionRate;
@@ -64,6 +66,8 @@ contract EmissionManager is IEmissionManager, Policy, RolesConsumer {
uint8 public beatCounter;
bool public locallyActive;
uint256 public activeMarketId;
+ uint256 public tickSizeScalar;
+ uint256 public minPriceScalar;
uint8 internal _oracleDecimals;
uint8 internal immutable _ohmDecimals;
@@ -85,7 +89,8 @@ contract EmissionManager is IEmissionManager, Policy, RolesConsumer {
address gohm_,
address reserve_,
address sReserve_,
- address auctioneer_,
+ address bondAuctioneer_,
+ address cdAuctioneer_,
address teller_
) Policy(kernel_) {
// Set immutable variables
@@ -93,13 +98,15 @@ contract EmissionManager is IEmissionManager, Policy, RolesConsumer {
if (gohm_ == address(0)) revert("gOHM address cannot be 0");
if (reserve_ == address(0)) revert("DAI address cannot be 0");
if (sReserve_ == address(0)) revert("sDAI address cannot be 0");
- if (auctioneer_ == address(0)) revert("Auctioneer address cannot be 0");
+ if (bondAuctioneer_ == address(0)) revert("Bond Auctioneer address cannot be 0");
+ if (cdAuctioneer_ == address(0)) revert("CD Auctioneer address cannot be 0");
ohm = ERC20(ohm_);
gohm = IgOHM(gohm_);
reserve = ERC20(reserve_);
sReserve = ERC4626(sReserve_);
- auctioneer = IBondSDA(auctioneer_);
+ bondAuctioneer = IBondSDA(bondAuctioneer_);
+ cdAuctioneer = CDAuctioneer(cdAuctioneer_);
teller = teller_;
_ohmDecimals = ohm.decimals();
@@ -156,12 +163,18 @@ contract EmissionManager is IEmissionManager, Policy, RolesConsumer {
}
// It then calculates the amount to sell for the coming day
- (, , uint256 sell) = getNextSale();
+ (, , uint256 emission) = getNextEmission();
+
+ uint256 remainder = cdAuctioneer.beat(
+ emission,
+ getSizeFor(emission),
+ getMinPriceFor(PRICE.getCurrentPrice())
+ );
// And then opens a market if applicable
- if (sell != 0) {
- MINTR.increaseMintApproval(address(this), sell);
- _createMarket(sell);
+ if (remainder != 0) {
+ MINTR.increaseMintApproval(address(this), remainder);
+ _createMarket(remainder);
}
}
@@ -176,6 +189,8 @@ contract EmissionManager is IEmissionManager, Policy, RolesConsumer {
uint256 baseEmissionsRate_,
uint256 minimumPremium_,
uint256 backing_,
+ uint256 tickScalar,
+ uint256 priceScalar,
uint48 restartTimeframe_
) external onlyRole("emissions_admin") {
// Cannot initialize if currently active
@@ -192,12 +207,18 @@ contract EmissionManager is IEmissionManager, Policy, RolesConsumer {
if (minimumPremium_ == 0) revert InvalidParam("minimumPremium");
if (backing_ == 0) revert InvalidParam("backing");
if (restartTimeframe_ == 0) revert InvalidParam("restartTimeframe");
+ if (tickScalar == 0 || tickScalar > ONE_HUNDRED_PERCENT)
+ revert InvalidParam("Tick Size Scalar");
+ if (priceScalar == 0 || priceScalar > ONE_HUNDRED_PERCENT)
+ revert InvalidParam("Tick Size Scalar");
// Assign
baseEmissionRate = baseEmissionsRate_;
minimumPremium = minimumPremium_;
backing = backing_;
restartTimeframe = restartTimeframe_;
+ tickSizeScalar = tickScalar;
+ minPriceScalar = priceScalar;
// Activate
locallyActive = true;
@@ -258,7 +279,7 @@ contract EmissionManager is IEmissionManager, Policy, RolesConsumer {
);
// Create new bond market to buy the reserve with OHM
- activeMarketId = auctioneer.createMarket(
+ activeMarketId = bondAuctioneer.createMarket(
abi.encode(
IBondSDA.MarketParams({
payoutToken: ohm,
@@ -417,22 +438,47 @@ contract EmissionManager is IEmissionManager, Policy, RolesConsumer {
}
/// @notice allow governance to set the bond contracts used by the emission manager
- /// @param auctioneer_ address of the bond auctioneer contract
+ /// @param bondAuctioneer_ address of the bond auctioneer contract
/// @param teller_ address of the bond teller contract
function setBondContracts(
- address auctioneer_,
+ address bondAuctioneer_,
address teller_
) external onlyRole("emissions_admin") {
// Bond contracts cannot be set to the zero address
- if (auctioneer_ == address(0)) revert InvalidParam("auctioneer");
+ if (bondAuctioneer_ == address(0)) revert InvalidParam("bondAuctioneer");
if (teller_ == address(0)) revert InvalidParam("teller");
- auctioneer = IBondSDA(auctioneer_);
+ bondAuctioneer = IBondSDA(bondAuctioneer_);
teller = teller_;
emit BondContractsSet(auctioneer_, teller_);
}
+ /// @notice allow governance to set the CD contract used by the emission manager
+ /// @param cdAuctioneer_ address of the cd auctioneer contract
+ function setCDAuctionContract(address cdAuctioneer_) external onlyRole("emissions_admin") {
+ // Auction contract cannot be set to the zero address
+ if (cdAuctioneer_ == address(0)) revert InvalidParam("cdAuctioneer");
+
+ cdAuctioneer = CDAuctioneer(cdAuctioneer_);
+ }
+
+ /// @notice allow governance to set the CD tick size scalar
+ /// @param newScalar as a percentage in 18 decimals
+ function setTickSizeScalar(uint256 newScalar) external onlyRole("emissions_admin") {
+ if (newScalar == 0 || newScalar > ONE_HUNDRED_PERCENT)
+ revert InvalidParam("Tick Size Scalar");
+ tickSizeScalar = newScalar;
+ }
+
+ /// @notice allow governance to set the CD minimum price scalar
+ /// @param newScalar as a percentage in 18 decimals
+ function setMinPriceScalar(uint256 newScalar) external onlyRole("emissions_admin") {
+ if (newScalar == 0 || newScalar > ONE_HUNDRED_PERCENT)
+ revert InvalidParam("Min Price Scalar");
+ minPriceScalar = newScalar;
+ }
+
// =========- VIEW FUNCTIONS ========== //
/// @notice return reserves, measured as clearinghouse receivables and sReserve balances, in reserve denomination
@@ -460,7 +506,7 @@ contract EmissionManager is IEmissionManager, Policy, RolesConsumer {
}
/// @notice return the next sale amount, premium, emission rate, and emissions based on the current premium
- function getNextSale()
+ function getNextEmission()
public
view
returns (uint256 premium, uint256 emissionRate, uint256 emission)
@@ -476,4 +522,18 @@ contract EmissionManager is IEmissionManager, Policy, RolesConsumer {
emission = (getSupply() * emissionRate) / 10 ** _ohmDecimals; // OHM Scale * OHM Scale / OHM Scale = OHM Scale
}
}
+
+ /// @notice get CD auction tick size for a given target
+ /// @param target size of day's CD auction
+ /// @return size of tick
+ function getSizeFor(uint256 target) public view returns (uint256) {
+ return (target * tickSizeScalar) / ONE_HUNDRED_PERCENT;
+ }
+
+ /// @notice get CD auction minimum price for given current price
+ /// @param price of OHM on market according to PRICE module
+ /// @return minPrice for CD auction
+ function getMinPriceFor(uint256 price) public view returns (uint256) {
+ return (price * minPriceScalar) / ONE_HUNDRED_PERCENT;
+ }
}
From 61a73777a1bd2b333ca973520a611d3614c5329d Mon Sep 17 00:00:00 2001
From: Jem <0x0xjem@gmail.com>
Date: Mon, 9 Dec 2024 12:15:31 +0400
Subject: [PATCH 06/64] Fix compilation errors
---
src/policies/EmissionManager.sol | 6 +++---
src/scripts/deploy/DeployV2.sol | 23 ++++++++++++++++++++++-
2 files changed, 25 insertions(+), 4 deletions(-)
diff --git a/src/policies/EmissionManager.sol b/src/policies/EmissionManager.sol
index a8a962af..a1a94a77 100644
--- a/src/policies/EmissionManager.sol
+++ b/src/policies/EmissionManager.sol
@@ -344,8 +344,8 @@ contract EmissionManager is IEmissionManager, Policy, RolesConsumer {
shutdownTimestamp = uint48(block.timestamp);
// Shutdown the bond market, if it is active
- if (auctioneer.isLive(activeMarketId)) {
- auctioneer.closeMarket(activeMarketId);
+ if (bondAuctioneer.isLive(activeMarketId)) {
+ bondAuctioneer.closeMarket(activeMarketId);
}
emit Deactivated();
@@ -451,7 +451,7 @@ contract EmissionManager is IEmissionManager, Policy, RolesConsumer {
bondAuctioneer = IBondSDA(bondAuctioneer_);
teller = teller_;
- emit BondContractsSet(auctioneer_, teller_);
+ emit BondContractsSet(bondAuctioneer_, teller_);
}
/// @notice allow governance to set the CD contract used by the emission manager
diff --git a/src/scripts/deploy/DeployV2.sol b/src/scripts/deploy/DeployV2.sol
index 5184fdfe..13283730 100644
--- a/src/scripts/deploy/DeployV2.sol
+++ b/src/scripts/deploy/DeployV2.sol
@@ -63,6 +63,7 @@ import {Clearinghouse} from "policies/Clearinghouse.sol";
import {YieldRepurchaseFacility} from "policies/YieldRepurchaseFacility.sol";
import {ReserveMigrator} from "policies/ReserveMigrator.sol";
import {EmissionManager} from "policies/EmissionManager.sol";
+import {CDAuctioneer} from "policies/CDAuctioneer.sol";
import {MockPriceFeed} from "src/test/mocks/MockPriceFeed.sol";
import {MockAuraBooster, MockAuraRewardPool, MockAuraMiningLib, MockAuraVirtualRewardPool, MockAuraStashToken} from "src/test/mocks/AuraMocks.sol";
@@ -111,6 +112,7 @@ contract OlympusDeploy is Script {
YieldRepurchaseFacility public yieldRepo;
ReserveMigrator public reserveMigrator;
EmissionManager public emissionManager;
+ CDAuctioneer public cdAuctioneer;
/// Other Olympus contracts
OlympusAuthority public burnerReplacementAuthority;
@@ -223,6 +225,7 @@ contract OlympusDeploy is Script {
selectorMap["YieldRepurchaseFacility"] = this._deployYieldRepurchaseFacility.selector;
selectorMap["ReserveMigrator"] = this._deployReserveMigrator.selector;
selectorMap["EmissionManager"] = this._deployEmissionManager.selector;
+ selectorMap["ConvertibleDebtAuctioneer"] = this._deployConvertibleDebtAuctioneer.selector;
// Governance
selectorMap["Timelock"] = this._deployTimelock.selector;
@@ -312,6 +315,7 @@ contract OlympusDeploy is Script {
yieldRepo = YieldRepurchaseFacility(envAddress("olympus.policies.YieldRepurchaseFacility"));
reserveMigrator = ReserveMigrator(envAddress("olympus.policies.ReserveMigrator"));
emissionManager = EmissionManager(envAddress("olympus.policies.EmissionManager"));
+ cdAuctioneer = CDAuctioneer(envAddress("olympus.policies.ConvertibleDebtAuctioneer"));
// Governance
timelock = Timelock(payable(envAddress("olympus.governance.Timelock")));
@@ -1203,7 +1207,8 @@ contract OlympusDeploy is Script {
console2.log(" gohm", address(gohm));
console2.log(" reserve", address(reserve));
console2.log(" sReserve", address(sReserve));
- console2.log(" auctioneer", address(bondAuctioneer));
+ console2.log(" bondAuctioneer", address(bondAuctioneer));
+ console2.log(" cdAuctioneer", address(cdAuctioneer));
console2.log(" teller", address(bondFixedTermTeller));
// Deploy EmissionManager
@@ -1215,6 +1220,7 @@ contract OlympusDeploy is Script {
address(reserve),
address(sReserve),
address(bondAuctioneer),
+ address(cdAuctioneer),
address(bondFixedTermTeller)
);
@@ -1223,6 +1229,21 @@ contract OlympusDeploy is Script {
return address(emissionManager);
}
+ function _deployConvertibleDebtAuctioneer(bytes calldata) public returns (address) {
+ // No additional arguments for ConvertibleDebtAuctioneer
+
+ // Log dependencies
+ console2.log("ConvertibleDebtAuctioneer parameters:");
+ console2.log(" kernel", address(kernel));
+
+ // Deploy ConvertibleDebtAuctioneer
+ vm.broadcast();
+ cdAuctioneer = new CDAuctioneer(kernel);
+ console2.log("ConvertibleDebtAuctioneer deployed at:", address(cdAuctioneer));
+
+ return address(cdAuctioneer);
+ }
+
// ========== VERIFICATION ========== //
/// @dev Verifies that the environment variable addresses were set correctly following deployment
From 9068ba08f3b0f8d6a530aa44d9f65ff49892d7fb Mon Sep 17 00:00:00 2001
From: Jem <0x0xjem@gmail.com>
Date: Mon, 9 Dec 2024 12:15:37 +0400
Subject: [PATCH 07/64] chore: linting
---
src/policies/CDAuctioneer.sol | 3 +++
src/policies/CDFacility.sol | 4 ++++
2 files changed, 7 insertions(+)
diff --git a/src/policies/CDAuctioneer.sol b/src/policies/CDAuctioneer.sol
index 4eab7700..d5dc14c8 100644
--- a/src/policies/CDAuctioneer.sol
+++ b/src/policies/CDAuctioneer.sol
@@ -14,8 +14,11 @@ import {CDFacility} from "./CDFacility.sol";
interface CDRC20 {
function mint(address to, uint256 amount) external;
+
function burn(address from, uint256 amount) external;
+
function convertFor(uint256 amount) external view returns (uint256);
+
function expiry() external view returns (uint256);
}
diff --git a/src/policies/CDFacility.sol b/src/policies/CDFacility.sol
index 46121ffc..1c329702 100644
--- a/src/policies/CDFacility.sol
+++ b/src/policies/CDFacility.sol
@@ -15,9 +15,13 @@ import {FullMath} from "libraries/FullMath.sol";
interface CDRC20 {
function mint(address to, uint256 amount) external;
+
function burn(address from, uint256 amount) external;
+
function convertFor(uint256 amount) external view returns (uint256);
+
function expiry() external view returns (uint256);
+
function totalSupply() external view returns (uint256);
}
From e41276b5dc6ce0331a6dc92b304e08dbd1aa4c90 Mon Sep 17 00:00:00 2001
From: Jem <0x0xjem@gmail.com>
Date: Mon, 9 Dec 2024 12:36:39 +0400
Subject: [PATCH 08/64] Add custom error to contracts. Add missing CDFacility
parameter to CDAuctioneer constructor.
---
src/policies/CDAuctioneer.sol | 11 ++++++++++-
src/policies/CDFacility.sol | 12 ++++++++++--
src/policies/EmissionManager.sol | 19 +++++++++++++------
src/scripts/deploy/DeployV2.sol | 24 +++++++++++++++++++++++-
4 files changed, 56 insertions(+), 10 deletions(-)
diff --git a/src/policies/CDAuctioneer.sol b/src/policies/CDAuctioneer.sol
index d5dc14c8..6f247acc 100644
--- a/src/policies/CDAuctioneer.sol
+++ b/src/policies/CDAuctioneer.sol
@@ -48,6 +48,10 @@ contract CDAuctioneer is Policy, RolesConsumer {
// ========== EVENTS ========== //
+ // ========== ERRORS ========== //
+
+ error CDAuctioneer_InvalidParams(string reason);
+
// ========== STATE VARIABLES ========== //
Tick public currentTick;
@@ -63,7 +67,12 @@ contract CDAuctioneer is Policy, RolesConsumer {
// ========== SETUP ========== //
- constructor(Kernel kernel_) Policy(kernel_) {}
+ constructor(Kernel kernel_, address cdFacility_) Policy(kernel_) {
+ if (cdFacility_ == address(0))
+ revert CDAuctioneer_InvalidParams("CD Facility address cannot be 0");
+
+ cdFacility = CDFacility(cdFacility_);
+ }
function configureDependencies() external override returns (Keycode[] memory dependencies) {
dependencies = new Keycode[](1);
diff --git a/src/policies/CDFacility.sol b/src/policies/CDFacility.sol
index 1c329702..eb92f3be 100644
--- a/src/policies/CDFacility.sol
+++ b/src/policies/CDFacility.sol
@@ -28,8 +28,6 @@ interface CDRC20 {
contract CDFacility is Policy, RolesConsumer {
using FullMath for uint256;
- error Misconfigured();
-
struct CD {
uint256 deposit;
uint256 convertable;
@@ -63,9 +61,19 @@ contract CDFacility is Policy, RolesConsumer {
mapping(address => CD[]) public cdInfo;
uint256 public redeemRate;
+ // ========== ERRORS ========== //
+
+ error CDFacility_InvalidParams(string reason);
+
+ error Misconfigured();
+
// ========== SETUP ========== //
constructor(Kernel kernel_, address reserve_, address sReserve_) Policy(kernel_) {
+ if (reserve_ == address(0)) revert CDFacility_InvalidParams("Reserve address cannot be 0");
+ if (sReserve_ == address(0))
+ revert CDFacility_InvalidParams("sReserve address cannot be 0");
+
reserve = ERC20(reserve_);
sReserve = ERC4626(sReserve_);
}
diff --git a/src/policies/EmissionManager.sol b/src/policies/EmissionManager.sol
index a1a94a77..4122783f 100644
--- a/src/policies/EmissionManager.sol
+++ b/src/policies/EmissionManager.sol
@@ -81,6 +81,10 @@ contract EmissionManager is IEmissionManager, Policy, RolesConsumer {
uint256 internal constant ONE_HUNDRED_PERCENT = 1e18;
+ // ========== ERRORS ========== //
+
+ error EmissionManager_InvalidParams(string reason);
+
// ========== SETUP ========== //
constructor(
@@ -94,12 +98,15 @@ contract EmissionManager is IEmissionManager, Policy, RolesConsumer {
address teller_
) Policy(kernel_) {
// Set immutable variables
- if (ohm_ == address(0)) revert("OHM address cannot be 0");
- if (gohm_ == address(0)) revert("gOHM address cannot be 0");
- if (reserve_ == address(0)) revert("DAI address cannot be 0");
- if (sReserve_ == address(0)) revert("sDAI address cannot be 0");
- if (bondAuctioneer_ == address(0)) revert("Bond Auctioneer address cannot be 0");
- if (cdAuctioneer_ == address(0)) revert("CD Auctioneer address cannot be 0");
+ if (ohm_ == address(0)) revert EmissionManager_InvalidParams("OHM address cannot be 0");
+ if (gohm_ == address(0)) revert EmissionManager_InvalidParams("gOHM address cannot be 0");
+ if (reserve_ == address(0)) revert EmissionManager_InvalidParams("DAI address cannot be 0");
+ if (sReserve_ == address(0))
+ revert EmissionManager_InvalidParams("sDAI address cannot be 0");
+ if (bondAuctioneer_ == address(0))
+ revert EmissionManager_InvalidParams("Bond Auctioneer address cannot be 0");
+ if (cdAuctioneer_ == address(0))
+ revert EmissionManager_InvalidParams("CD Auctioneer address cannot be 0");
ohm = ERC20(ohm_);
gohm = IgOHM(gohm_);
diff --git a/src/scripts/deploy/DeployV2.sol b/src/scripts/deploy/DeployV2.sol
index 13283730..5e0392bd 100644
--- a/src/scripts/deploy/DeployV2.sol
+++ b/src/scripts/deploy/DeployV2.sol
@@ -64,6 +64,7 @@ import {YieldRepurchaseFacility} from "policies/YieldRepurchaseFacility.sol";
import {ReserveMigrator} from "policies/ReserveMigrator.sol";
import {EmissionManager} from "policies/EmissionManager.sol";
import {CDAuctioneer} from "policies/CDAuctioneer.sol";
+import {CDFacility} from "policies/CDFacility.sol";
import {MockPriceFeed} from "src/test/mocks/MockPriceFeed.sol";
import {MockAuraBooster, MockAuraRewardPool, MockAuraMiningLib, MockAuraVirtualRewardPool, MockAuraStashToken} from "src/test/mocks/AuraMocks.sol";
@@ -113,6 +114,7 @@ contract OlympusDeploy is Script {
ReserveMigrator public reserveMigrator;
EmissionManager public emissionManager;
CDAuctioneer public cdAuctioneer;
+ CDFacility public cdFacility;
/// Other Olympus contracts
OlympusAuthority public burnerReplacementAuthority;
@@ -226,6 +228,7 @@ contract OlympusDeploy is Script {
selectorMap["ReserveMigrator"] = this._deployReserveMigrator.selector;
selectorMap["EmissionManager"] = this._deployEmissionManager.selector;
selectorMap["ConvertibleDebtAuctioneer"] = this._deployConvertibleDebtAuctioneer.selector;
+ selectorMap["ConvertibleDebtFacility"] = this._deployConvertibleDebtFacility.selector;
// Governance
selectorMap["Timelock"] = this._deployTimelock.selector;
@@ -316,6 +319,7 @@ contract OlympusDeploy is Script {
reserveMigrator = ReserveMigrator(envAddress("olympus.policies.ReserveMigrator"));
emissionManager = EmissionManager(envAddress("olympus.policies.EmissionManager"));
cdAuctioneer = CDAuctioneer(envAddress("olympus.policies.ConvertibleDebtAuctioneer"));
+ cdFacility = CDFacility(envAddress("olympus.policies.ConvertibleDebtFacility"));
// Governance
timelock = Timelock(payable(envAddress("olympus.governance.Timelock")));
@@ -1235,15 +1239,33 @@ contract OlympusDeploy is Script {
// Log dependencies
console2.log("ConvertibleDebtAuctioneer parameters:");
console2.log(" kernel", address(kernel));
+ console2.log(" cdFacility", address(cdFacility));
// Deploy ConvertibleDebtAuctioneer
vm.broadcast();
- cdAuctioneer = new CDAuctioneer(kernel);
+ cdAuctioneer = new CDAuctioneer(kernel, address(cdFacility));
console2.log("ConvertibleDebtAuctioneer deployed at:", address(cdAuctioneer));
return address(cdAuctioneer);
}
+ function _deployConvertibleDebtFacility(bytes calldata) public returns (address) {
+ // No additional arguments for ConvertibleDebtFacility
+
+ // Log dependencies
+ console2.log("ConvertibleDebtFacility parameters:");
+ console2.log(" kernel", address(kernel));
+ console2.log(" reserve", address(reserve));
+ console2.log(" sReserve", address(sReserve));
+
+ // Deploy ConvertibleDebtFacility
+ vm.broadcast();
+ cdFacility = new CDFacility(kernel, address(reserve), address(sReserve));
+ console2.log("ConvertibleDebtFacility deployed at:", address(cdFacility));
+
+ return address(cdFacility);
+ }
+
// ========== VERIFICATION ========== //
/// @dev Verifies that the environment variable addresses were set correctly following deployment
From 74ebfa4661391dee33ca7d3302a23a61171dfc7c Mon Sep 17 00:00:00 2001
From: Jem <0x0xjem@gmail.com>
Date: Mon, 9 Dec 2024 13:26:52 +0400
Subject: [PATCH 09/64] Extract CDAuctioneer into interface
---
src/interfaces/IConvertibleDebtAuctioneer.sol | 106 ++++++++++++++++++
src/policies/CDAuctioneer.sol | 68 ++++-------
src/policies/CDFacility.sol | 5 +
src/policies/EmissionManager.sol | 8 +-
4 files changed, 139 insertions(+), 48 deletions(-)
create mode 100644 src/interfaces/IConvertibleDebtAuctioneer.sol
diff --git a/src/interfaces/IConvertibleDebtAuctioneer.sol b/src/interfaces/IConvertibleDebtAuctioneer.sol
new file mode 100644
index 00000000..6bd7dab3
--- /dev/null
+++ b/src/interfaces/IConvertibleDebtAuctioneer.sol
@@ -0,0 +1,106 @@
+// SPDX-License-Identifier: MIT
+pragma solidity >=0.8.0;
+
+interface IConvertibleDebtAuctioneer {
+ // ========== EVENTS ========== //
+
+ // ========== ERRORS ========== //
+
+ error CDAuctioneer_InvalidParams(string reason);
+
+ // ========== DATA STRUCTURES ========== //
+
+ /// @notice State of the auction
+ ///
+ /// @param target number of ohm per day
+ /// @param tickSize number of ohm in a tick
+ /// @param minPrice minimum tick price
+ /// @param tickStep percentage increase (decrease) per tick
+ /// @param timeToExpiry time between creation and expiry of deposit
+ /// @param lastUpdate timestamp of last update to current tick
+ struct State {
+ uint256 target;
+ uint256 tickSize;
+ uint256 minPrice;
+ uint256 tickStep;
+ uint256 timeToExpiry;
+ uint256 lastUpdate;
+ }
+
+ /// @notice Tracks auction activity for a given day
+ ///
+ /// @param deposits total deposited for day
+ /// @param convertable total convertable for day
+ struct Day {
+ uint256 deposits;
+ uint256 convertable;
+ }
+
+ /// @notice Information about a tick
+ ///
+ /// @param price price of the tick
+ /// @param capacity capacity of the tick
+ struct Tick {
+ uint256 price;
+ uint256 capacity;
+ }
+
+ // ========== AUCTION ========== //
+
+ /// @notice Use a deposit to bid for CDs
+ ///
+ /// @param deposit amount of reserve tokens
+ /// @return convertable amount of convertable tokens
+ function bid(uint256 deposit) external returns (uint256 convertable);
+
+ // ========== VIEW ========== //
+
+ /// @notice Get the current tick
+ ///
+ /// @return tick info in Tick struct
+ function getCurrentTick() external view returns (Tick memory tick);
+
+ /// @notice Get the current state
+ ///
+ /// @return state info in State struct
+ function getState() external view returns (State memory state);
+
+ /// @notice Get the auction activity for the current day
+ ///
+ /// @return day info in Day struct
+ function getDay() external view returns (Day memory day);
+
+ /// @notice Get the amount of convertable tokens for a deposit at a given price
+ ///
+ /// @param deposit amount of reserve tokens
+ /// @param price price of the tick
+ /// @return convertable amount of convertable tokens
+ function convertFor(uint256 deposit, uint256 price) external view returns (uint256 convertable);
+
+ // ========== ADMIN ========== //
+
+ /// @notice Update the auction parameters
+ /// @dev only callable by the auction admin
+ ///
+ /// @param newTarget new target sale per day
+ /// @param newSize new size per tick
+ /// @param newMinPrice new minimum tick price
+ /// @return remainder amount of ohm not sold
+ function beat(
+ uint256 newTarget,
+ uint256 newSize,
+ uint256 newMinPrice
+ ) external returns (uint256 remainder);
+
+ /// @notice Set the time to expiry
+ /// @dev only callable by the admin
+ ///
+ /// @param newTime new time to expiry
+ function setTimeToExpiry(uint256 newTime) external;
+
+ /// @notice Set the tick step
+ /// @dev only callable by the admin
+ ///
+ /// @param newStep new tick step
+ function setTickStep(uint256 newStep) external;
+}
diff --git a/src/policies/CDAuctioneer.sol b/src/policies/CDAuctioneer.sol
index 6f247acc..529c85e1 100644
--- a/src/policies/CDAuctioneer.sol
+++ b/src/policies/CDAuctioneer.sol
@@ -6,9 +6,10 @@ import "src/Kernel.sol";
import {ReentrancyGuard} from "solmate/utils/ReentrancyGuard.sol";
import {ERC20} from "solmate/tokens/ERC20.sol";
-import {RolesConsumer, ROLESv1} from "modules/ROLES/OlympusRoles.sol";
+import {RolesConsumer, ROLESv1} from "src/modules/ROLES/OlympusRoles.sol";
+import {IConvertibleDebtAuctioneer} from "src/interfaces/IConvertibleDebtAuctioneer.sol";
-import {FullMath} from "libraries/FullMath.sol";
+import {FullMath} from "src/libraries/FullMath.sol";
import {CDFacility} from "./CDFacility.sol";
@@ -22,41 +23,15 @@ interface CDRC20 {
function expiry() external view returns (uint256);
}
-contract CDAuctioneer is Policy, RolesConsumer {
+contract CDAuctioneer is IConvertibleDebtAuctioneer, Policy, RolesConsumer {
using FullMath for uint256;
- // ========== DATA STRUCTURES ========== //
-
- struct State {
- uint256 target; // number of ohm per day
- uint256 tickSize; // number of ohm in a tick
- uint256 minPrice; // minimum tick price
- uint256 tickStep; // percentage increase (decrease) per tick
- uint256 timeToExpiry; // time between creation and expiry of deposit
- uint256 lastUpdate; // timestamp of last update to current tick
- }
-
- struct Day {
- uint256 deposits; // total deposited for day
- uint256 convertable; // total convertable for day
- }
-
- struct Tick {
- uint256 price;
- uint256 capacity;
- }
-
- // ========== EVENTS ========== //
-
- // ========== ERRORS ========== //
-
- error CDAuctioneer_InvalidParams(string reason);
-
// ========== STATE VARIABLES ========== //
Tick public currentTick;
State public state;
+ // TODO set decimals, make internal?
uint256 public decimals;
CDFacility public cdFacility;
@@ -90,10 +65,8 @@ contract CDAuctioneer is Policy, RolesConsumer {
// ========== AUCTION ========== //
- /// @notice use a deposit to bid for CDs
- /// @param deposit amount of reserve tokens
- /// @return convertable amount of convertable tokens
- function bid(uint256 deposit) external returns (uint256 convertable) {
+ /// @inheritdoc IConvertibleDebtAuctioneer
+ function bid(uint256 deposit) external override returns (uint256 convertable) {
// update state
currentTick = getCurrentTick();
state.lastUpdate = block.timestamp;
@@ -121,10 +94,8 @@ contract CDAuctioneer is Policy, RolesConsumer {
// ========== VIEW FUNCTIONS ========== //
- /// @notice get current tick info
- /// @dev time passing changes tick info
- /// @return tick info in Tick struct
- function getCurrentTick() public view returns (Tick memory tick) {
+ /// @inheritdoc IConvertibleDebtAuctioneer
+ function getCurrentTick() public view override returns (Tick memory tick) {
// find amount of time passed and new capacity to add
uint256 timePassed = block.timestamp - state.lastUpdate;
uint256 newCapacity = (state.target * timePassed) / 1 days;
@@ -149,12 +120,21 @@ contract CDAuctioneer is Policy, RolesConsumer {
tick.capacity = newCapacity;
}
- /// @notice get amount of cdOHM for a deposit at a tick price
- /// @return amount convertable
- function convertFor(uint256 deposit, uint256 price) public view returns (uint256) {
+ /// @inheritdoc IConvertibleDebtAuctioneer
+ function convertFor(uint256 deposit, uint256 price) public view override returns (uint256) {
return (deposit * decimals) / price;
}
+ /// @inheritdoc IConvertibleDebtAuctioneer
+ function getState() external view override returns (State memory) {
+ return state;
+ }
+
+ /// @inheritdoc IConvertibleDebtAuctioneer
+ function getDay() external view override returns (Day memory) {
+ return today;
+ }
+
// ========== ADMIN FUNCTIONS ========== //
/// @notice update auction parameters
@@ -166,7 +146,7 @@ contract CDAuctioneer is Policy, RolesConsumer {
uint256 newTarget,
uint256 newSize,
uint256 newMinPrice
- ) external onlyRole("CD_Auction_Admin") returns (uint256 remainder) {
+ ) external override onlyRole("CD_Auction_Admin") returns (uint256 remainder) {
remainder = (state.target > today.convertable) ? state.target - today.convertable : 0;
state = State(
@@ -181,13 +161,13 @@ contract CDAuctioneer is Policy, RolesConsumer {
/// @notice update time between creation and expiry of deposit
/// @param newTime number of seconds
- function setTimeToExpiry(uint256 newTime) external onlyRole("CD_Admin") {
+ function setTimeToExpiry(uint256 newTime) external override onlyRole("CD_Admin") {
state.timeToExpiry = newTime;
}
/// @notice update change between ticks
/// @param newStep percentage in decimal terms
- function setTickStep(uint256 newStep) external onlyRole("CD_Admin") {
+ function setTickStep(uint256 newStep) external override onlyRole("CD_Admin") {
state.tickStep = newStep;
}
}
diff --git a/src/policies/CDFacility.sol b/src/policies/CDFacility.sol
index eb92f3be..420c01b0 100644
--- a/src/policies/CDFacility.sol
+++ b/src/policies/CDFacility.sol
@@ -13,6 +13,7 @@ import {TRSRYv1} from "modules/TRSRY/TRSRY.v1.sol";
import {FullMath} from "libraries/FullMath.sol";
+// TODO extract into external file
interface CDRC20 {
function mint(address to, uint256 amount) external;
@@ -25,6 +26,8 @@ interface CDRC20 {
function totalSupply() external view returns (uint256);
}
+// TODO extract CDFacility interface
+
contract CDFacility is Policy, RolesConsumer {
using FullMath for uint256;
@@ -69,6 +72,8 @@ contract CDFacility is Policy, RolesConsumer {
// ========== SETUP ========== //
+ // TODO add cdUSDS parameter
+
constructor(Kernel kernel_, address reserve_, address sReserve_) Policy(kernel_) {
if (reserve_ == address(0)) revert CDFacility_InvalidParams("Reserve address cannot be 0");
if (sReserve_ == address(0))
diff --git a/src/policies/EmissionManager.sol b/src/policies/EmissionManager.sol
index 4122783f..1330f070 100644
--- a/src/policies/EmissionManager.sol
+++ b/src/policies/EmissionManager.sol
@@ -19,7 +19,7 @@ import {MINTRv1} from "modules/MINTR/MINTR.v1.sol";
import {CHREGv1} from "modules/CHREG/CHREG.v1.sol";
import {IEmissionManager} from "policies/interfaces/IEmissionManager.sol";
-import {CDAuctioneer} from "./CDAuctioneer.sol";
+import {IConvertibleDebtAuctioneer} from "src/interfaces/IConvertibleDebtAuctioneer.sol";
interface BurnableERC20 {
function burn(uint256 amount) external;
@@ -56,7 +56,7 @@ contract EmissionManager is IEmissionManager, Policy, RolesConsumer {
// External contracts
IBondSDA public bondAuctioneer;
address public teller;
- CDAuctioneer public cdAuctioneer;
+ IConvertibleDebtAuctioneer public cdAuctioneer;
// Manager variables
uint256 public baseEmissionRate;
@@ -113,7 +113,7 @@ contract EmissionManager is IEmissionManager, Policy, RolesConsumer {
reserve = ERC20(reserve_);
sReserve = ERC4626(sReserve_);
bondAuctioneer = IBondSDA(bondAuctioneer_);
- cdAuctioneer = CDAuctioneer(cdAuctioneer_);
+ cdAuctioneer = IConvertibleDebtAuctioneer(cdAuctioneer_);
teller = teller_;
_ohmDecimals = ohm.decimals();
@@ -467,7 +467,7 @@ contract EmissionManager is IEmissionManager, Policy, RolesConsumer {
// Auction contract cannot be set to the zero address
if (cdAuctioneer_ == address(0)) revert InvalidParam("cdAuctioneer");
- cdAuctioneer = CDAuctioneer(cdAuctioneer_);
+ cdAuctioneer = IConvertibleDebtAuctioneer(cdAuctioneer_);
}
/// @notice allow governance to set the CD tick size scalar
From fb68663bcb2bc18bc031898207f5dbf1091d578f Mon Sep 17 00:00:00 2001
From: Jem <0x0xjem@gmail.com>
Date: Mon, 9 Dec 2024 13:40:18 +0400
Subject: [PATCH 10/64] Fix compilation errors for EmissionManager test
---
src/policies/EmissionManager.sol | 18 ++-
.../mocks/MockConvertibleDebtAuctioneer.sol | 49 +++++++
src/test/policies/EmissionManager.t.sol | 138 +++++++++++++-----
3 files changed, 164 insertions(+), 41 deletions(-)
create mode 100644 src/test/mocks/MockConvertibleDebtAuctioneer.sol
diff --git a/src/policies/EmissionManager.sol b/src/policies/EmissionManager.sol
index 1330f070..3b849f8c 100644
--- a/src/policies/EmissionManager.sol
+++ b/src/policies/EmissionManager.sol
@@ -188,16 +188,19 @@ contract EmissionManager is IEmissionManager, Policy, RolesConsumer {
// ========== INITIALIZE ========== //
/// @notice allow governance to initialize the emission manager
+ ///
/// @param baseEmissionsRate_ percent of OHM supply to issue per day at the minimum premium, in OHM scale, i.e. 1e9 = 100%
/// @param minimumPremium_ minimum premium at which to issue OHM, a percentage where 1e18 is 100%
/// @param backing_ backing price of OHM in reserve token, in reserve scale
+ /// @param tickSizeScalar_ scalar for tick size
+ /// @param minPriceScalar_ scalar for min price
/// @param restartTimeframe_ time in seconds that the manager needs to be restarted after a shutdown, otherwise it must be re-initialized
function initialize(
uint256 baseEmissionsRate_,
uint256 minimumPremium_,
uint256 backing_,
- uint256 tickScalar,
- uint256 priceScalar,
+ uint256 tickSizeScalar_,
+ uint256 minPriceScalar_,
uint48 restartTimeframe_
) external onlyRole("emissions_admin") {
// Cannot initialize if currently active
@@ -214,18 +217,18 @@ contract EmissionManager is IEmissionManager, Policy, RolesConsumer {
if (minimumPremium_ == 0) revert InvalidParam("minimumPremium");
if (backing_ == 0) revert InvalidParam("backing");
if (restartTimeframe_ == 0) revert InvalidParam("restartTimeframe");
- if (tickScalar == 0 || tickScalar > ONE_HUNDRED_PERCENT)
- revert InvalidParam("Tick Size Scalar");
- if (priceScalar == 0 || priceScalar > ONE_HUNDRED_PERCENT)
+ if (tickSizeScalar_ == 0 || tickSizeScalar_ > ONE_HUNDRED_PERCENT)
revert InvalidParam("Tick Size Scalar");
+ if (minPriceScalar_ == 0 || minPriceScalar_ > ONE_HUNDRED_PERCENT)
+ revert InvalidParam("Min Price Scalar");
// Assign
baseEmissionRate = baseEmissionsRate_;
minimumPremium = minimumPremium_;
backing = backing_;
restartTimeframe = restartTimeframe_;
- tickSizeScalar = tickScalar;
- minPriceScalar = priceScalar;
+ tickSizeScalar = tickSizeScalar_;
+ minPriceScalar = minPriceScalar_;
// Activate
locallyActive = true;
@@ -234,6 +237,7 @@ contract EmissionManager is IEmissionManager, Policy, RolesConsumer {
emit MinimumPremiumChanged(minimumPremium_);
emit BackingChanged(backing_);
emit RestartTimeframeChanged(restartTimeframe_);
+ // TODO emit tickSizeScalar and minPriceScalar
}
// ========== BOND CALLBACK ========== //
diff --git a/src/test/mocks/MockConvertibleDebtAuctioneer.sol b/src/test/mocks/MockConvertibleDebtAuctioneer.sol
new file mode 100644
index 00000000..5ff6fd4b
--- /dev/null
+++ b/src/test/mocks/MockConvertibleDebtAuctioneer.sol
@@ -0,0 +1,49 @@
+// SPDX-License-Identifier: Unlicense
+pragma solidity 0.8.15;
+
+import {Kernel, Policy, Keycode, toKeycode, Permissions} from "src/Kernel.sol";
+import {RolesConsumer, ROLESv1} from "src/modules/ROLES/OlympusRoles.sol";
+import {IConvertibleDebtAuctioneer} from "src/interfaces/IConvertibleDebtAuctioneer.sol";
+
+contract MockConvertibleDebtAuctioneer is IConvertibleDebtAuctioneer, Policy, RolesConsumer {
+ constructor(Kernel kernel_) Policy(kernel_) {}
+
+ function configureDependencies() external override returns (Keycode[] memory dependencies) {
+ dependencies = new Keycode[](1);
+ dependencies[2] = toKeycode("ROLES");
+
+ ROLES = ROLESv1(getModuleAddress(dependencies[0]));
+ }
+
+ function requestPermissions()
+ external
+ view
+ override
+ returns (Permissions[] memory permissions)
+ {}
+
+ function bid(uint256 deposit) external override returns (uint256 convertable) {
+ return deposit;
+ }
+
+ function getCurrentTick() external view override returns (Tick memory tick) {}
+
+ function getState() external view override returns (State memory state) {}
+
+ function getDay() external view override returns (Day memory day) {}
+
+ function convertFor(
+ uint256 deposit,
+ uint256 price
+ ) external view override returns (uint256 convertable) {}
+
+ function beat(
+ uint256 newTarget,
+ uint256 newSize,
+ uint256 newMinPrice
+ ) external override returns (uint256 remainder) {}
+
+ function setTimeToExpiry(uint256 newTime) external override {}
+
+ function setTickStep(uint256 newStep) external override {}
+}
diff --git a/src/test/policies/EmissionManager.t.sol b/src/test/policies/EmissionManager.t.sol
index ec4bca17..ca435fe6 100644
--- a/src/test/policies/EmissionManager.t.sol
+++ b/src/test/policies/EmissionManager.t.sol
@@ -16,6 +16,7 @@ import {MockPrice} from "src/test/mocks/MockPrice.sol";
import {MockOhm} from "src/test/mocks/MockOhm.sol";
import {MockGohm} from "src/test/mocks/MockGohm.sol";
import {MockClearinghouse} from "src/test/mocks/MockClearinghouse.sol";
+import {MockConvertibleDebtAuctioneer} from "src/test/mocks/MockConvertibleDebtAuctioneer.sol";
import {IBondSDA} from "interfaces/IBondSDA.sol";
import {IBondAggregator} from "interfaces/IBondAggregator.sol";
@@ -44,7 +45,7 @@ contract EmissionManagerTest is Test {
RolesAuthority internal auth;
BondAggregator internal aggregator;
BondFixedTermTeller internal teller;
- BondFixedTermSDA internal auctioneer;
+ BondFixedTermSDA internal bondAuctioneer;
MockOhm internal ohm;
MockGohm internal gohm;
MockERC20 internal reserve;
@@ -59,6 +60,7 @@ contract EmissionManagerTest is Test {
OlympusClearinghouseRegistry internal CHREG;
MockClearinghouse internal clearinghouse;
+ MockConvertibleDebtAuctioneer internal cdAuctioneer;
RolesAdmin internal rolesAdmin;
EmissionManager internal emissionManager;
@@ -69,6 +71,8 @@ contract EmissionManagerTest is Test {
uint48 internal restartTimeframe = 1 days;
uint256 internal changeBy = 1e5; // 0.01% change per execution
uint48 internal changeDuration = 2; // 2 executions
+ uint256 internal tickSizeScalar = 1e18; // 100%
+ uint256 internal minPriceScalar = 1e18; // 100%
// test cases
//
@@ -119,7 +123,7 @@ contract EmissionManagerTest is Test {
// [X] it returns 0
// [X] when price is greater than backing
// [X] it returns the (price - backing) / backing
- // [X] getNextSale
+ // [X] getNextEmission
// [X] when the premium is less than the minimum premium
// [X] it returns the premium, 0, and 0
// [X] when the premium is greater than or equal to the minimum premium
@@ -205,11 +209,11 @@ contract EmissionManagerTest is Test {
// [X] when the caller doesn't have the emissions_admin role
// [X] it reverts
// [X] when the caller has the emissions_admin role
- // [X] when the new auctioneer address is the zero address
+ // [X] when the new bondAuctioneer address is the zero address
// [X] it reverts
// [X] when the new teller address is the zero address
// [X] it reverts
- // [X] it sets the auctioneer address
+ // [X] it sets the bondAuctioneer address
// [X] it sets the teller address
function setUp() public {
@@ -227,11 +231,11 @@ contract EmissionManagerTest is Test {
/// Deploy the bond system
aggregator = new BondAggregator(guardian, auth);
teller = new BondFixedTermTeller(guardian, aggregator, guardian, auth);
- auctioneer = new BondFixedTermSDA(teller, aggregator, guardian, auth);
+ bondAuctioneer = new BondFixedTermSDA(teller, aggregator, guardian, auth);
- /// Register auctioneer on the bond system
+ /// Register bondAuctioneer on the bond system
vm.prank(guardian);
- aggregator.registerAuctioneer(auctioneer);
+ aggregator.registerAuctioneer(bondAuctioneer);
}
{
@@ -269,6 +273,9 @@ contract EmissionManagerTest is Test {
/// Deploy ROLES administrator
rolesAdmin = new RolesAdmin(kernel);
+ // Deploy the mock CD auctioneer
+ cdAuctioneer = new MockConvertibleDebtAuctioneer(kernel);
+
// Deploy the emission manager
emissionManager = new EmissionManager(
kernel,
@@ -276,7 +283,8 @@ contract EmissionManagerTest is Test {
address(gohm),
address(reserve),
address(sReserve),
- address(auctioneer),
+ address(bondAuctioneer),
+ address(cdAuctioneer),
address(teller)
);
}
@@ -294,6 +302,7 @@ contract EmissionManagerTest is Test {
/// Approve policies
kernel.executeAction(Actions.ActivatePolicy, address(emissionManager));
kernel.executeAction(Actions.ActivatePolicy, address(rolesAdmin));
+ kernel.executeAction(Actions.ActivatePolicy, address(cdAuctioneer));
}
{
/// Configure access control
@@ -332,11 +341,18 @@ contract EmissionManagerTest is Test {
// Initialize the emissions manager
vm.prank(guardian);
- emissionManager.initialize(baseEmissionRate, minimumPremium, backing, restartTimeframe);
+ emissionManager.initialize(
+ baseEmissionRate,
+ minimumPremium,
+ backing,
+ tickSizeScalar,
+ minPriceScalar,
+ restartTimeframe
+ );
- // Approve the emission manager to use a bond callback on the auctioneer
+ // Approve the emission manager to use a bond callback on the bondAuctioneer
vm.prank(guardian);
- auctioneer.setCallbackAuthStatus(address(emissionManager), true);
+ bondAuctioneer.setCallbackAuthStatus(address(emissionManager), true);
// Total Reserves = $50M + $50 M = $100M
// Total Supply = 10,000,000 OHM
@@ -579,7 +595,7 @@ contract EmissionManagerTest is Test {
// Verify the bond market parameters
// Check that the market params are correct
{
- uint256 marketPrice = auctioneer.marketPrice(nextBondMarketId);
+ uint256 marketPrice = bondAuctioneer.marketPrice(nextBondMarketId);
(
address owner,
ERC20 payoutToken,
@@ -593,7 +609,7 @@ contract EmissionManagerTest is Test {
,
,
uint256 scale
- ) = auctioneer.markets(nextBondMarketId);
+ ) = bondAuctioneer.markets(nextBondMarketId);
assertEq(owner, address(emissionManager), "Owner");
assertEq(address(payoutToken), address(ohm), "Payout token");
@@ -671,7 +687,7 @@ contract EmissionManagerTest is Test {
// Verify the bond market parameters
// Check that the market params are correct
{
- uint256 marketPrice = auctioneer.marketPrice(nextBondMarketId);
+ uint256 marketPrice = bondAuctioneer.marketPrice(nextBondMarketId);
(
address owner,
ERC20 payoutToken,
@@ -685,7 +701,7 @@ contract EmissionManagerTest is Test {
,
,
uint256 scale
- ) = auctioneer.markets(nextBondMarketId);
+ ) = bondAuctioneer.markets(nextBondMarketId);
assertEq(owner, address(emissionManager), "Owner");
assertEq(address(payoutToken), address(ohm), "Payout token");
@@ -765,7 +781,7 @@ contract EmissionManagerTest is Test {
// Confirm that the capacity of the bond market uses the new base rate
assertEq(
- auctioneer.currentCapacity(nextBondMarketId),
+ bondAuctioneer.currentCapacity(nextBondMarketId),
expectedCapacity,
"Capacity should be updated"
);
@@ -827,7 +843,7 @@ contract EmissionManagerTest is Test {
// Confirm that the capacity of the bond market uses the new base rate
assertEq(
- auctioneer.currentCapacity(nextBondMarketId),
+ bondAuctioneer.currentCapacity(nextBondMarketId),
expectedCapacity,
"Capacity should be updated"
);
@@ -997,7 +1013,7 @@ contract EmissionManagerTest is Test {
// Store initial backing value
uint256 bidAmount = 1000e18;
- uint256 expectedPayout = auctioneer.payoutFor(bidAmount, nextBondMarketId, address(0));
+ uint256 expectedPayout = bondAuctioneer.payoutFor(bidAmount, nextBondMarketId, address(0));
uint256 expectedBacking;
{
uint256 reserves = emissionManager.getReserves();
@@ -1085,7 +1101,7 @@ contract EmissionManagerTest is Test {
{
// We created a market, confirm it is active
uint256 id = emissionManager.activeMarketId();
- assertTrue(auctioneer.isLive(id));
+ assertTrue(bondAuctioneer.isLive(id));
// Check that the contract is locally active
assertTrue(emissionManager.locallyActive(), "Contract should be locally active");
@@ -1111,7 +1127,7 @@ contract EmissionManagerTest is Test {
);
// Check that the market is no longer active
- assertFalse(auctioneer.isLive(id));
+ assertFalse(bondAuctioneer.isLive(id));
}
// restart tests
@@ -1193,7 +1209,14 @@ contract EmissionManagerTest is Test {
);
vm.expectRevert(err);
vm.prank(rando_);
- emissionManager.initialize(baseEmissionRate, minimumPremium, backing, restartTimeframe);
+ emissionManager.initialize(
+ baseEmissionRate,
+ minimumPremium,
+ backing,
+ tickSizeScalar,
+ minPriceScalar,
+ restartTimeframe
+ );
}
function test_initialize_whenAlreadyActive_reverts() public {
@@ -1201,7 +1224,14 @@ contract EmissionManagerTest is Test {
bytes memory err = abi.encodeWithSignature("AlreadyActive()");
vm.expectRevert(err);
vm.prank(guardian);
- emissionManager.initialize(baseEmissionRate, minimumPremium, backing, restartTimeframe);
+ emissionManager.initialize(
+ baseEmissionRate,
+ minimumPremium,
+ backing,
+ tickSizeScalar,
+ minPriceScalar,
+ restartTimeframe
+ );
}
function test_initialize_whenRestartTimeframeNotElapsed_reverts(
@@ -1226,7 +1256,14 @@ contract EmissionManagerTest is Test {
);
vm.expectRevert(err);
vm.prank(guardian);
- emissionManager.initialize(baseEmissionRate, minimumPremium, backing, restartTimeframe);
+ emissionManager.initialize(
+ baseEmissionRate,
+ minimumPremium,
+ backing,
+ tickSizeScalar,
+ minPriceScalar,
+ restartTimeframe
+ );
}
function test_initialize_whenBaseEmissionRateZero_reverts()
@@ -1240,7 +1277,14 @@ contract EmissionManagerTest is Test {
bytes memory err = abi.encodeWithSignature("InvalidParam(string)", "baseEmissionRate");
vm.expectRevert(err);
vm.prank(guardian);
- emissionManager.initialize(0, minimumPremium, backing, restartTimeframe);
+ emissionManager.initialize(
+ 0,
+ minimumPremium,
+ backing,
+ tickSizeScalar,
+ minPriceScalar,
+ restartTimeframe
+ );
}
function test_initialize_whenMinimumPremiumZero_reverts()
@@ -1254,7 +1298,14 @@ contract EmissionManagerTest is Test {
bytes memory err = abi.encodeWithSignature("InvalidParam(string)", "minimumPremium");
vm.expectRevert(err);
vm.prank(guardian);
- emissionManager.initialize(baseEmissionRate, 0, backing, restartTimeframe);
+ emissionManager.initialize(
+ baseEmissionRate,
+ 0,
+ backing,
+ tickSizeScalar,
+ minPriceScalar,
+ restartTimeframe
+ );
}
function test_initialize_whenBackingZero_reverts()
@@ -1268,7 +1319,14 @@ contract EmissionManagerTest is Test {
bytes memory err = abi.encodeWithSignature("InvalidParam(string)", "backing");
vm.expectRevert(err);
vm.prank(guardian);
- emissionManager.initialize(baseEmissionRate, minimumPremium, 0, restartTimeframe);
+ emissionManager.initialize(
+ baseEmissionRate,
+ minimumPremium,
+ 0,
+ tickSizeScalar,
+ minPriceScalar,
+ restartTimeframe
+ );
}
function test_initialize_whenRestartTimeframeZero_reverts()
@@ -1282,7 +1340,14 @@ contract EmissionManagerTest is Test {
bytes memory err = abi.encodeWithSignature("InvalidParam(string)", "restartTimeframe");
vm.expectRevert(err);
vm.prank(guardian);
- emissionManager.initialize(baseEmissionRate, minimumPremium, backing, 0);
+ emissionManager.initialize(
+ baseEmissionRate,
+ minimumPremium,
+ backing,
+ tickSizeScalar,
+ minPriceScalar,
+ 0
+ );
}
function test_initialize_success() public givenShutdown givenRestartTimeframeElapsed {
@@ -1296,6 +1361,8 @@ contract EmissionManagerTest is Test {
baseEmissionRate + 1,
minimumPremium + 1,
backing + 1,
+ tickSizeScalar,
+ minPriceScalar,
restartTimeframe + 1
);
@@ -1537,7 +1604,7 @@ contract EmissionManagerTest is Test {
}
function test_setBondContracts_whenBondAuctioneerZero_reverts() public {
- // Try to set bond auctioneer to 0, expect revert
+ // Try to set bondAuctioneer to 0, expect revert
bytes memory err = abi.encodeWithSignature("InvalidParam(string)", "auctioneer");
vm.expectRevert(err);
vm.prank(guardian);
@@ -1559,9 +1626,9 @@ contract EmissionManagerTest is Test {
// Confirm new bond contracts
assertEq(
- address(emissionManager.auctioneer()),
+ address(emissionManager.bondAuctioneer()),
address(1),
- "Bond auctioneer should be updated"
+ "BondAuctioneer should be updated"
);
assertEq(emissionManager.teller(), address(1), "Bond teller should be updated");
}
@@ -1640,11 +1707,12 @@ contract EmissionManagerTest is Test {
);
}
- // getNextSale tests
+ // getNextEmission tests
function test_getNextSale_whenPremiumBelowMinimum() public givenPremiumBelowMinimum {
// Get the next sale data
- (uint256 premium, uint256 emissionRate, uint256 emission) = emissionManager.getNextSale();
+ (uint256 premium, uint256 emissionRate, uint256 emission) = emissionManager
+ .getNextEmission();
// Expect that the premium is as set in the setup
// and the other two values are zero
@@ -1655,7 +1723,8 @@ contract EmissionManagerTest is Test {
function test_getNextSale_whenPremiumEqualToMinimum() public givenPremiumEqualToMinimum {
// Get the next sale data
- (uint256 premium, uint256 emissionRate, uint256 emission) = emissionManager.getNextSale();
+ (uint256 premium, uint256 emissionRate, uint256 emission) = emissionManager
+ .getNextEmission();
uint256 expectedEmission = 10_000e9; // 10,000 OHM (as described in setup)
@@ -1668,7 +1737,8 @@ contract EmissionManagerTest is Test {
function test_getNextSale_whenPremiumAboveMinimum() public givenPremiumAboveMinimum {
// Get the next sale data
- (uint256 premium, uint256 emissionRate, uint256 emission) = emissionManager.getNextSale();
+ (uint256 premium, uint256 emissionRate, uint256 emission) = emissionManager
+ .getNextEmission();
uint256 expectedEmission = 12_000e9; // 12,000 OHM (as described in setup)
From 2f2a0de8abc4fa513ef87a98523e9c4cbfe95dae Mon Sep 17 00:00:00 2001
From: Jem <0x0xjem@gmail.com>
Date: Mon, 9 Dec 2024 15:49:26 +0400
Subject: [PATCH 11/64] Add cdUSDS parameter to CDFacility. Extract cdUSDS into
interface.
---
src/interfaces/IConvertibleDebtToken.sol | 18 ++++++++++++
src/policies/CDFacility.sol | 35 ++++++++++++------------
2 files changed, 35 insertions(+), 18 deletions(-)
create mode 100644 src/interfaces/IConvertibleDebtToken.sol
diff --git a/src/interfaces/IConvertibleDebtToken.sol b/src/interfaces/IConvertibleDebtToken.sol
new file mode 100644
index 00000000..c8abd042
--- /dev/null
+++ b/src/interfaces/IConvertibleDebtToken.sol
@@ -0,0 +1,18 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.0;
+
+import {IERC20} from "openzeppelin-contracts/contracts/interfaces/IERC20.sol";
+
+// TODO see if there is a standard interface for this
+
+interface IConvertibleDebtToken is IERC20 {
+ function mint(address to, uint256 amount) external;
+
+ function burn(address from, uint256 amount) external;
+
+ function convertFor(uint256 amount) external view returns (uint256);
+
+ function expiry() external view returns (uint256);
+
+ function totalSupply() external view returns (uint256);
+}
diff --git a/src/policies/CDFacility.sol b/src/policies/CDFacility.sol
index 420c01b0..fc00d46a 100644
--- a/src/policies/CDFacility.sol
+++ b/src/policies/CDFacility.sol
@@ -7,24 +7,13 @@ import {ReentrancyGuard} from "solmate/utils/ReentrancyGuard.sol";
import {ERC20} from "solmate/tokens/ERC20.sol";
import {ERC4626} from "solmate/mixins/ERC4626.sol";
-import {RolesConsumer, ROLESv1} from "modules/ROLES/OlympusRoles.sol";
-import {MINTRv1} from "modules/MINTR/MINTR.v1.sol";
-import {TRSRYv1} from "modules/TRSRY/TRSRY.v1.sol";
+import {RolesConsumer, ROLESv1} from "src/modules/ROLES/OlympusRoles.sol";
+import {MINTRv1} from "src/modules/MINTR/MINTR.v1.sol";
+import {TRSRYv1} from "src/modules/TRSRY/TRSRY.v1.sol";
-import {FullMath} from "libraries/FullMath.sol";
+import {IConvertibleDebtToken} from "src/interfaces/IConvertibleDebtToken.sol";
-// TODO extract into external file
-interface CDRC20 {
- function mint(address to, uint256 amount) external;
-
- function burn(address from, uint256 amount) external;
-
- function convertFor(uint256 amount) external view returns (uint256);
-
- function expiry() external view returns (uint256);
-
- function totalSupply() external view returns (uint256);
-}
+import {FullMath} from "src/libraries/FullMath.sol";
// TODO extract CDFacility interface
@@ -56,7 +45,8 @@ contract CDFacility is Policy, RolesConsumer {
// Tokens
ERC20 public reserve;
ERC4626 public sReserve;
- CDRC20 public cdUSDS;
+ // TODO re-think whether this should use a factory pattern instead
+ IConvertibleDebtToken public cdUSDS;
// State variables
uint256 public totalDeposits;
@@ -72,15 +62,24 @@ contract CDFacility is Policy, RolesConsumer {
// ========== SETUP ========== //
+ // TODO input approved convertible debt tokens in constructor, to allow for migration
+
// TODO add cdUSDS parameter
- constructor(Kernel kernel_, address reserve_, address sReserve_) Policy(kernel_) {
+ constructor(
+ Kernel kernel_,
+ address reserve_,
+ address sReserve_,
+ address cdUSDS_
+ ) Policy(kernel_) {
if (reserve_ == address(0)) revert CDFacility_InvalidParams("Reserve address cannot be 0");
if (sReserve_ == address(0))
revert CDFacility_InvalidParams("sReserve address cannot be 0");
+ if (cdUSDS_ == address(0)) revert CDFacility_InvalidParams("cdUSDS address cannot be 0");
reserve = ERC20(reserve_);
sReserve = ERC4626(sReserve_);
+ cdUSDS = IConvertibleDebtToken(cdUSDS_);
}
function configureDependencies() external override returns (Keycode[] memory dependencies) {
From 4797b13a57919c89a4cd456cee5749c239855bd9 Mon Sep 17 00:00:00 2001
From: Jem <0x0xjem@gmail.com>
Date: Fri, 13 Dec 2024 11:43:14 +0400
Subject: [PATCH 12/64] Fix naming consistency with debt/deposit. Add missing
parameter to deployment script.
---
src/policies/CDAuctioneer.sol | 14 ++++----
src/policies/CDFacility.sol | 6 ++--
src/policies/EmissionManager.sol | 8 ++---
.../IConvertibleDepositAuctioneer.sol} | 2 +-
.../interfaces/IConvertibleDepositToken.sol} | 2 +-
src/scripts/deploy/DeployV2.sol | 35 +++++++++++--------
...l => MockConvertibleDepositAuctioneer.sol} | 4 +--
src/test/policies/EmissionManager.t.sol | 6 ++--
8 files changed, 41 insertions(+), 36 deletions(-)
rename src/{interfaces/IConvertibleDebtAuctioneer.sol => policies/interfaces/IConvertibleDepositAuctioneer.sol} (98%)
rename src/{interfaces/IConvertibleDebtToken.sol => policies/interfaces/IConvertibleDepositToken.sol} (91%)
rename src/test/mocks/{MockConvertibleDebtAuctioneer.sol => MockConvertibleDepositAuctioneer.sol} (87%)
diff --git a/src/policies/CDAuctioneer.sol b/src/policies/CDAuctioneer.sol
index 529c85e1..57e7f164 100644
--- a/src/policies/CDAuctioneer.sol
+++ b/src/policies/CDAuctioneer.sol
@@ -7,7 +7,7 @@ import {ReentrancyGuard} from "solmate/utils/ReentrancyGuard.sol";
import {ERC20} from "solmate/tokens/ERC20.sol";
import {RolesConsumer, ROLESv1} from "src/modules/ROLES/OlympusRoles.sol";
-import {IConvertibleDebtAuctioneer} from "src/interfaces/IConvertibleDebtAuctioneer.sol";
+import {IConvertibleDepositAuctioneer} from "src/policies/interfaces/IConvertibleDepositAuctioneer.sol";
import {FullMath} from "src/libraries/FullMath.sol";
@@ -23,7 +23,7 @@ interface CDRC20 {
function expiry() external view returns (uint256);
}
-contract CDAuctioneer is IConvertibleDebtAuctioneer, Policy, RolesConsumer {
+contract CDAuctioneer is IConvertibleDepositAuctioneer, Policy, RolesConsumer {
using FullMath for uint256;
// ========== STATE VARIABLES ========== //
@@ -65,7 +65,7 @@ contract CDAuctioneer is IConvertibleDebtAuctioneer, Policy, RolesConsumer {
// ========== AUCTION ========== //
- /// @inheritdoc IConvertibleDebtAuctioneer
+ /// @inheritdoc IConvertibleDepositAuctioneer
function bid(uint256 deposit) external override returns (uint256 convertable) {
// update state
currentTick = getCurrentTick();
@@ -94,7 +94,7 @@ contract CDAuctioneer is IConvertibleDebtAuctioneer, Policy, RolesConsumer {
// ========== VIEW FUNCTIONS ========== //
- /// @inheritdoc IConvertibleDebtAuctioneer
+ /// @inheritdoc IConvertibleDepositAuctioneer
function getCurrentTick() public view override returns (Tick memory tick) {
// find amount of time passed and new capacity to add
uint256 timePassed = block.timestamp - state.lastUpdate;
@@ -120,17 +120,17 @@ contract CDAuctioneer is IConvertibleDebtAuctioneer, Policy, RolesConsumer {
tick.capacity = newCapacity;
}
- /// @inheritdoc IConvertibleDebtAuctioneer
+ /// @inheritdoc IConvertibleDepositAuctioneer
function convertFor(uint256 deposit, uint256 price) public view override returns (uint256) {
return (deposit * decimals) / price;
}
- /// @inheritdoc IConvertibleDebtAuctioneer
+ /// @inheritdoc IConvertibleDepositAuctioneer
function getState() external view override returns (State memory) {
return state;
}
- /// @inheritdoc IConvertibleDebtAuctioneer
+ /// @inheritdoc IConvertibleDepositAuctioneer
function getDay() external view override returns (Day memory) {
return today;
}
diff --git a/src/policies/CDFacility.sol b/src/policies/CDFacility.sol
index fc00d46a..e04a9daf 100644
--- a/src/policies/CDFacility.sol
+++ b/src/policies/CDFacility.sol
@@ -11,7 +11,7 @@ import {RolesConsumer, ROLESv1} from "src/modules/ROLES/OlympusRoles.sol";
import {MINTRv1} from "src/modules/MINTR/MINTR.v1.sol";
import {TRSRYv1} from "src/modules/TRSRY/TRSRY.v1.sol";
-import {IConvertibleDebtToken} from "src/interfaces/IConvertibleDebtToken.sol";
+import {IConvertibleDepositToken} from "src/policies/interfaces/IConvertibleDepositToken.sol";
import {FullMath} from "src/libraries/FullMath.sol";
@@ -46,7 +46,7 @@ contract CDFacility is Policy, RolesConsumer {
ERC20 public reserve;
ERC4626 public sReserve;
// TODO re-think whether this should use a factory pattern instead
- IConvertibleDebtToken public cdUSDS;
+ IConvertibleDepositToken public cdUSDS;
// State variables
uint256 public totalDeposits;
@@ -79,7 +79,7 @@ contract CDFacility is Policy, RolesConsumer {
reserve = ERC20(reserve_);
sReserve = ERC4626(sReserve_);
- cdUSDS = IConvertibleDebtToken(cdUSDS_);
+ cdUSDS = IConvertibleDepositToken(cdUSDS_);
}
function configureDependencies() external override returns (Keycode[] memory dependencies) {
diff --git a/src/policies/EmissionManager.sol b/src/policies/EmissionManager.sol
index 3b849f8c..440dfd92 100644
--- a/src/policies/EmissionManager.sol
+++ b/src/policies/EmissionManager.sol
@@ -19,7 +19,7 @@ import {MINTRv1} from "modules/MINTR/MINTR.v1.sol";
import {CHREGv1} from "modules/CHREG/CHREG.v1.sol";
import {IEmissionManager} from "policies/interfaces/IEmissionManager.sol";
-import {IConvertibleDebtAuctioneer} from "src/interfaces/IConvertibleDebtAuctioneer.sol";
+import {IConvertibleDepositAuctioneer} from "src/policies/interfaces/IConvertibleDepositAuctioneer.sol";
interface BurnableERC20 {
function burn(uint256 amount) external;
@@ -56,7 +56,7 @@ contract EmissionManager is IEmissionManager, Policy, RolesConsumer {
// External contracts
IBondSDA public bondAuctioneer;
address public teller;
- IConvertibleDebtAuctioneer public cdAuctioneer;
+ IConvertibleDepositAuctioneer public cdAuctioneer;
// Manager variables
uint256 public baseEmissionRate;
@@ -113,7 +113,7 @@ contract EmissionManager is IEmissionManager, Policy, RolesConsumer {
reserve = ERC20(reserve_);
sReserve = ERC4626(sReserve_);
bondAuctioneer = IBondSDA(bondAuctioneer_);
- cdAuctioneer = IConvertibleDebtAuctioneer(cdAuctioneer_);
+ cdAuctioneer = IConvertibleDepositAuctioneer(cdAuctioneer_);
teller = teller_;
_ohmDecimals = ohm.decimals();
@@ -471,7 +471,7 @@ contract EmissionManager is IEmissionManager, Policy, RolesConsumer {
// Auction contract cannot be set to the zero address
if (cdAuctioneer_ == address(0)) revert InvalidParam("cdAuctioneer");
- cdAuctioneer = IConvertibleDebtAuctioneer(cdAuctioneer_);
+ cdAuctioneer = IConvertibleDepositAuctioneer(cdAuctioneer_);
}
/// @notice allow governance to set the CD tick size scalar
diff --git a/src/interfaces/IConvertibleDebtAuctioneer.sol b/src/policies/interfaces/IConvertibleDepositAuctioneer.sol
similarity index 98%
rename from src/interfaces/IConvertibleDebtAuctioneer.sol
rename to src/policies/interfaces/IConvertibleDepositAuctioneer.sol
index 6bd7dab3..ed6838fe 100644
--- a/src/interfaces/IConvertibleDebtAuctioneer.sol
+++ b/src/policies/interfaces/IConvertibleDepositAuctioneer.sol
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
-interface IConvertibleDebtAuctioneer {
+interface IConvertibleDepositAuctioneer {
// ========== EVENTS ========== //
// ========== ERRORS ========== //
diff --git a/src/interfaces/IConvertibleDebtToken.sol b/src/policies/interfaces/IConvertibleDepositToken.sol
similarity index 91%
rename from src/interfaces/IConvertibleDebtToken.sol
rename to src/policies/interfaces/IConvertibleDepositToken.sol
index c8abd042..ca2482ab 100644
--- a/src/interfaces/IConvertibleDebtToken.sol
+++ b/src/policies/interfaces/IConvertibleDepositToken.sol
@@ -5,7 +5,7 @@ import {IERC20} from "openzeppelin-contracts/contracts/interfaces/IERC20.sol";
// TODO see if there is a standard interface for this
-interface IConvertibleDebtToken is IERC20 {
+interface IConvertibleDepositToken is IERC20 {
function mint(address to, uint256 amount) external;
function burn(address from, uint256 amount) external;
diff --git a/src/scripts/deploy/DeployV2.sol b/src/scripts/deploy/DeployV2.sol
index 5e0392bd..c5ccef30 100644
--- a/src/scripts/deploy/DeployV2.sol
+++ b/src/scripts/deploy/DeployV2.sol
@@ -227,8 +227,10 @@ contract OlympusDeploy is Script {
selectorMap["YieldRepurchaseFacility"] = this._deployYieldRepurchaseFacility.selector;
selectorMap["ReserveMigrator"] = this._deployReserveMigrator.selector;
selectorMap["EmissionManager"] = this._deployEmissionManager.selector;
- selectorMap["ConvertibleDebtAuctioneer"] = this._deployConvertibleDebtAuctioneer.selector;
- selectorMap["ConvertibleDebtFacility"] = this._deployConvertibleDebtFacility.selector;
+ selectorMap["ConvertibleDepositAuctioneer"] = this
+ ._deployConvertibleDepositAuctioneer
+ .selector;
+ selectorMap["ConvertibleDepositFacility"] = this._deployConvertibleDepositFacility.selector;
// Governance
selectorMap["Timelock"] = this._deployTimelock.selector;
@@ -318,8 +320,8 @@ contract OlympusDeploy is Script {
yieldRepo = YieldRepurchaseFacility(envAddress("olympus.policies.YieldRepurchaseFacility"));
reserveMigrator = ReserveMigrator(envAddress("olympus.policies.ReserveMigrator"));
emissionManager = EmissionManager(envAddress("olympus.policies.EmissionManager"));
- cdAuctioneer = CDAuctioneer(envAddress("olympus.policies.ConvertibleDebtAuctioneer"));
- cdFacility = CDFacility(envAddress("olympus.policies.ConvertibleDebtFacility"));
+ cdAuctioneer = CDAuctioneer(envAddress("olympus.policies.ConvertibleDepositAuctioneer"));
+ cdFacility = CDFacility(envAddress("olympus.policies.ConvertibleDepositFacility"));
// Governance
timelock = Timelock(payable(envAddress("olympus.governance.Timelock")));
@@ -1233,35 +1235,38 @@ contract OlympusDeploy is Script {
return address(emissionManager);
}
- function _deployConvertibleDebtAuctioneer(bytes calldata) public returns (address) {
- // No additional arguments for ConvertibleDebtAuctioneer
+ function _deployConvertibleDepositAuctioneer(bytes calldata) public returns (address) {
+ // No additional arguments for ConvertibleDepositAuctioneer
// Log dependencies
- console2.log("ConvertibleDebtAuctioneer parameters:");
+ console2.log("ConvertibleDepositAuctioneer parameters:");
console2.log(" kernel", address(kernel));
console2.log(" cdFacility", address(cdFacility));
- // Deploy ConvertibleDebtAuctioneer
+ // Deploy ConvertibleDepositAuctioneer
vm.broadcast();
cdAuctioneer = new CDAuctioneer(kernel, address(cdFacility));
- console2.log("ConvertibleDebtAuctioneer deployed at:", address(cdAuctioneer));
+ console2.log("ConvertibleDepositAuctioneer deployed at:", address(cdAuctioneer));
return address(cdAuctioneer);
}
- function _deployConvertibleDebtFacility(bytes calldata) public returns (address) {
- // No additional arguments for ConvertibleDebtFacility
+ function _deployConvertibleDepositFacility(bytes calldata) public returns (address) {
+ // No additional arguments for ConvertibleDepositFacility
// Log dependencies
- console2.log("ConvertibleDebtFacility parameters:");
+ console2.log("ConvertibleDepositFacility parameters:");
console2.log(" kernel", address(kernel));
console2.log(" reserve", address(reserve));
console2.log(" sReserve", address(sReserve));
- // Deploy ConvertibleDebtFacility
+ // TODO add deployment of cdUSDS
+ address cdUSDS = address(0);
+
+ // Deploy ConvertibleDepositFacility
vm.broadcast();
- cdFacility = new CDFacility(kernel, address(reserve), address(sReserve));
- console2.log("ConvertibleDebtFacility deployed at:", address(cdFacility));
+ cdFacility = new CDFacility(kernel, address(reserve), address(sReserve), cdUSDS);
+ console2.log("ConvertibleDepositFacility deployed at:", address(cdFacility));
return address(cdFacility);
}
diff --git a/src/test/mocks/MockConvertibleDebtAuctioneer.sol b/src/test/mocks/MockConvertibleDepositAuctioneer.sol
similarity index 87%
rename from src/test/mocks/MockConvertibleDebtAuctioneer.sol
rename to src/test/mocks/MockConvertibleDepositAuctioneer.sol
index 5ff6fd4b..c1e154f0 100644
--- a/src/test/mocks/MockConvertibleDebtAuctioneer.sol
+++ b/src/test/mocks/MockConvertibleDepositAuctioneer.sol
@@ -3,9 +3,9 @@ pragma solidity 0.8.15;
import {Kernel, Policy, Keycode, toKeycode, Permissions} from "src/Kernel.sol";
import {RolesConsumer, ROLESv1} from "src/modules/ROLES/OlympusRoles.sol";
-import {IConvertibleDebtAuctioneer} from "src/interfaces/IConvertibleDebtAuctioneer.sol";
+import {IConvertibleDepositAuctioneer} from "src/policies/interfaces/IConvertibleDepositAuctioneer.sol";
-contract MockConvertibleDebtAuctioneer is IConvertibleDebtAuctioneer, Policy, RolesConsumer {
+contract MockConvertibleDepositAuctioneer is IConvertibleDepositAuctioneer, Policy, RolesConsumer {
constructor(Kernel kernel_) Policy(kernel_) {}
function configureDependencies() external override returns (Keycode[] memory dependencies) {
diff --git a/src/test/policies/EmissionManager.t.sol b/src/test/policies/EmissionManager.t.sol
index ca435fe6..7c5b7548 100644
--- a/src/test/policies/EmissionManager.t.sol
+++ b/src/test/policies/EmissionManager.t.sol
@@ -16,7 +16,7 @@ import {MockPrice} from "src/test/mocks/MockPrice.sol";
import {MockOhm} from "src/test/mocks/MockOhm.sol";
import {MockGohm} from "src/test/mocks/MockGohm.sol";
import {MockClearinghouse} from "src/test/mocks/MockClearinghouse.sol";
-import {MockConvertibleDebtAuctioneer} from "src/test/mocks/MockConvertibleDebtAuctioneer.sol";
+import {MockConvertibleDepositAuctioneer} from "src/test/mocks/MockConvertibleDepositAuctioneer.sol";
import {IBondSDA} from "interfaces/IBondSDA.sol";
import {IBondAggregator} from "interfaces/IBondAggregator.sol";
@@ -60,7 +60,7 @@ contract EmissionManagerTest is Test {
OlympusClearinghouseRegistry internal CHREG;
MockClearinghouse internal clearinghouse;
- MockConvertibleDebtAuctioneer internal cdAuctioneer;
+ MockConvertibleDepositAuctioneer internal cdAuctioneer;
RolesAdmin internal rolesAdmin;
EmissionManager internal emissionManager;
@@ -274,7 +274,7 @@ contract EmissionManagerTest is Test {
rolesAdmin = new RolesAdmin(kernel);
// Deploy the mock CD auctioneer
- cdAuctioneer = new MockConvertibleDebtAuctioneer(kernel);
+ cdAuctioneer = new MockConvertibleDepositAuctioneer(kernel);
// Deploy the emission manager
emissionManager = new EmissionManager(
From a6dacc8efb7d27dfe5592073fa3d63223910956a Mon Sep 17 00:00:00 2001
From: Jem <0x0xjem@gmail.com>
Date: Fri, 13 Dec 2024 16:32:36 +0400
Subject: [PATCH 13/64] Create interface for convertible deposit facility
---
src/policies/CDAuctioneer.sol | 47 +++---
src/policies/CDFacility.sol | 11 +-
.../IConvertibleDepositAuctioneer.sol | 67 ++++----
.../IConvertibleDepositFacility.sol | 143 ++++++++++++++++++
4 files changed, 202 insertions(+), 66 deletions(-)
create mode 100644 src/policies/interfaces/IConvertibleDepositFacility.sol
diff --git a/src/policies/CDAuctioneer.sol b/src/policies/CDAuctioneer.sol
index 57e7f164..e766305d 100644
--- a/src/policies/CDAuctioneer.sol
+++ b/src/policies/CDAuctioneer.sol
@@ -13,16 +13,6 @@ import {FullMath} from "src/libraries/FullMath.sol";
import {CDFacility} from "./CDFacility.sol";
-interface CDRC20 {
- function mint(address to, uint256 amount) external;
-
- function burn(address from, uint256 amount) external;
-
- function convertFor(uint256 amount) external view returns (uint256);
-
- function expiry() external view returns (uint256);
-}
-
contract CDAuctioneer is IConvertibleDepositAuctioneer, Policy, RolesConsumer {
using FullMath for uint256;
@@ -46,6 +36,8 @@ contract CDAuctioneer is IConvertibleDepositAuctioneer, Policy, RolesConsumer {
if (cdFacility_ == address(0))
revert CDAuctioneer_InvalidParams("CD Facility address cannot be 0");
+ // TODO set decimals
+
cdFacility = CDFacility(cdFacility_);
}
@@ -66,7 +58,7 @@ contract CDAuctioneer is IConvertibleDepositAuctioneer, Policy, RolesConsumer {
// ========== AUCTION ========== //
/// @inheritdoc IConvertibleDepositAuctioneer
- function bid(uint256 deposit) external override returns (uint256 convertable) {
+ function bid(uint256 deposit) external override returns (uint256 convertible) {
// update state
currentTick = getCurrentTick();
state.lastUpdate = block.timestamp;
@@ -74,7 +66,7 @@ contract CDAuctioneer is IConvertibleDepositAuctioneer, Policy, RolesConsumer {
// iterate until user has no more reserves to bid
while (deposit > 0) {
// handle spent/capacity for tick
- uint256 amount = currentTick.capacity < convertFor(deposit, currentTick.price)
+ uint256 amount = currentTick.capacity < _convertFor(deposit, currentTick.price)
? state.tickSize
: deposit;
if (amount != state.tickSize) currentTick.capacity -= amount;
@@ -82,14 +74,20 @@ contract CDAuctioneer is IConvertibleDepositAuctioneer, Policy, RolesConsumer {
// decrement bid and increment tick price
deposit -= amount;
- convertable += convertFor(amount, currentTick.price);
+ convertible += _convertFor(amount, currentTick.price);
}
today.deposits += deposit;
- today.convertable += convertable;
+ today.convertible += convertible;
// mint amount of CD token
- cdFacility.addNewCD(msg.sender, deposit, convertable, block.timestamp + state.timeToExpiry);
+ cdFacility.addNewCD(msg.sender, deposit, convertible, block.timestamp + state.timeToExpiry);
+ }
+
+ /// @inheritdoc IConvertibleDepositAuctioneer
+ function previewBid(uint256 deposit) external view override returns (uint256 convertible) {
+ // TODO
+ return 0;
}
// ========== VIEW FUNCTIONS ========== //
@@ -120,8 +118,7 @@ contract CDAuctioneer is IConvertibleDepositAuctioneer, Policy, RolesConsumer {
tick.capacity = newCapacity;
}
- /// @inheritdoc IConvertibleDepositAuctioneer
- function convertFor(uint256 deposit, uint256 price) public view override returns (uint256) {
+ function _convertFor(uint256 deposit, uint256 price) internal view returns (uint256) {
return (deposit * decimals) / price;
}
@@ -137,17 +134,13 @@ contract CDAuctioneer is IConvertibleDepositAuctioneer, Policy, RolesConsumer {
// ========== ADMIN FUNCTIONS ========== //
- /// @notice update auction parameters
- /// @dev only callable by the auction admin
- /// @param newTarget new target sale per day
- /// @param newSize new size per tick
- /// @param newMinPrice new minimum tick price
- function beat(
+ /// @inheritdoc IConvertibleDepositAuctioneer
+ function setAuctionParameters(
uint256 newTarget,
uint256 newSize,
uint256 newMinPrice
) external override onlyRole("CD_Auction_Admin") returns (uint256 remainder) {
- remainder = (state.target > today.convertable) ? state.target - today.convertable : 0;
+ remainder = (state.target > today.convertible) ? state.target - today.convertible : 0;
state = State(
newTarget,
@@ -159,14 +152,12 @@ contract CDAuctioneer is IConvertibleDepositAuctioneer, Policy, RolesConsumer {
);
}
- /// @notice update time between creation and expiry of deposit
- /// @param newTime number of seconds
+ /// @inheritdoc IConvertibleDepositAuctioneer
function setTimeToExpiry(uint256 newTime) external override onlyRole("CD_Admin") {
state.timeToExpiry = newTime;
}
- /// @notice update change between ticks
- /// @param newStep percentage in decimal terms
+ /// @inheritdoc IConvertibleDepositAuctioneer
function setTickStep(uint256 newStep) external override onlyRole("CD_Admin") {
state.tickStep = newStep;
}
diff --git a/src/policies/CDFacility.sol b/src/policies/CDFacility.sol
index e04a9daf..53f4240b 100644
--- a/src/policies/CDFacility.sol
+++ b/src/policies/CDFacility.sol
@@ -15,8 +15,6 @@ import {IConvertibleDepositToken} from "src/policies/interfaces/IConvertibleDepo
import {FullMath} from "src/libraries/FullMath.sol";
-// TODO extract CDFacility interface
-
contract CDFacility is Policy, RolesConsumer {
using FullMath for uint256;
@@ -45,7 +43,6 @@ contract CDFacility is Policy, RolesConsumer {
// Tokens
ERC20 public reserve;
ERC4626 public sReserve;
- // TODO re-think whether this should use a factory pattern instead
IConvertibleDepositToken public cdUSDS;
// State variables
@@ -62,10 +59,6 @@ contract CDFacility is Policy, RolesConsumer {
// ========== SETUP ========== //
- // TODO input approved convertible debt tokens in constructor, to allow for migration
-
- // TODO add cdUSDS parameter
-
constructor(
Kernel kernel_,
address reserve_,
@@ -79,6 +72,8 @@ contract CDFacility is Policy, RolesConsumer {
reserve = ERC20(reserve_);
sReserve = ERC4626(sReserve_);
+
+ // TODO shift to module and dependency injection
cdUSDS = IConvertibleDepositToken(cdUSDS_);
}
@@ -130,6 +125,8 @@ contract CDFacility is Policy, RolesConsumer {
// store convertable deposit info and mint cdUSDS
cdInfo[user].push(CD(amount, convertable, expiry));
+
+ // TODO consider if the ERC20 should custody the deposit token
cdUSDS.mint(user, amount);
}
diff --git a/src/policies/interfaces/IConvertibleDepositAuctioneer.sol b/src/policies/interfaces/IConvertibleDepositAuctioneer.sol
index ed6838fe..7eaf71c1 100644
--- a/src/policies/interfaces/IConvertibleDepositAuctioneer.sol
+++ b/src/policies/interfaces/IConvertibleDepositAuctioneer.sol
@@ -4,6 +4,12 @@ pragma solidity >=0.8.0;
interface IConvertibleDepositAuctioneer {
// ========== EVENTS ========== //
+ event AuctionParametersUpdated(uint256 newTarget, uint256 newSize, uint256 newMinPrice);
+
+ event TimeToExpiryUpdated(uint256 newTimeToExpiry);
+
+ event TickStepUpdated(uint256 newTickStep);
+
// ========== ERRORS ========== //
error CDAuctioneer_InvalidParams(string reason);
@@ -30,10 +36,10 @@ interface IConvertibleDepositAuctioneer {
/// @notice Tracks auction activity for a given day
///
/// @param deposits total deposited for day
- /// @param convertable total convertable for day
+ /// @param convertible total convertible for day
struct Day {
uint256 deposits;
- uint256 convertable;
+ uint256 convertible;
}
/// @notice Information about a tick
@@ -47,60 +53,59 @@ interface IConvertibleDepositAuctioneer {
// ========== AUCTION ========== //
- /// @notice Use a deposit to bid for CDs
+ /// @notice Deposit reserve tokens to bid for convertible deposit tokens
///
- /// @param deposit amount of reserve tokens
- /// @return convertable amount of convertable tokens
- function bid(uint256 deposit) external returns (uint256 convertable);
+ /// @param deposit_ Amount of reserve tokens to deposit
+ /// @return convertible_ Amount of convertible tokens minted
+ function bid(uint256 deposit_) external returns (uint256 convertible_);
- // ========== VIEW ========== //
+ /// @notice Get the amount of convertible deposit tokens issued for a deposit
+ ///
+ /// @param deposit_ Amount of reserve tokens
+ /// @return convertible_ Amount of convertible tokens
+ function previewBid(uint256 deposit_) external view returns (uint256 convertible_);
- /// @notice Get the current tick
+ // ========== STATE VARIABLES ========== //
+
+ /// @notice Get the current tick of the auction
///
- /// @return tick info in Tick struct
+ /// @return tick Tick info
function getCurrentTick() external view returns (Tick memory tick);
- /// @notice Get the current state
+ /// @notice Get the current state of the auction
///
- /// @return state info in State struct
+ /// @return state State info
function getState() external view returns (State memory state);
/// @notice Get the auction activity for the current day
///
- /// @return day info in Day struct
+ /// @return day Day info
function getDay() external view returns (Day memory day);
- /// @notice Get the amount of convertable tokens for a deposit at a given price
- ///
- /// @param deposit amount of reserve tokens
- /// @param price price of the tick
- /// @return convertable amount of convertable tokens
- function convertFor(uint256 deposit, uint256 price) external view returns (uint256 convertable);
-
// ========== ADMIN ========== //
/// @notice Update the auction parameters
/// @dev only callable by the auction admin
///
- /// @param newTarget new target sale per day
- /// @param newSize new size per tick
- /// @param newMinPrice new minimum tick price
- /// @return remainder amount of ohm not sold
- function beat(
- uint256 newTarget,
- uint256 newSize,
- uint256 newMinPrice
+ /// @param newTarget_ new target sale per day
+ /// @param newSize_ new size per tick
+ /// @param newMinPrice_ new minimum tick price
+ /// @return remainder amount of ohm not sold
+ function setAuctionParameters(
+ uint256 newTarget_,
+ uint256 newSize_,
+ uint256 newMinPrice_
) external returns (uint256 remainder);
/// @notice Set the time to expiry
/// @dev only callable by the admin
///
- /// @param newTime new time to expiry
- function setTimeToExpiry(uint256 newTime) external;
+ /// @param newTime_ new time to expiry
+ function setTimeToExpiry(uint256 newTime_) external;
/// @notice Set the tick step
/// @dev only callable by the admin
///
- /// @param newStep new tick step
- function setTickStep(uint256 newStep) external;
+ /// @param newStep_ new tick step
+ function setTickStep(uint256 newStep_) external;
}
diff --git a/src/policies/interfaces/IConvertibleDepositFacility.sol b/src/policies/interfaces/IConvertibleDepositFacility.sol
new file mode 100644
index 00000000..60ead170
--- /dev/null
+++ b/src/policies/interfaces/IConvertibleDepositFacility.sol
@@ -0,0 +1,143 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.0;
+
+import {IERC20} from "openzeppelin-contracts/contracts/interfaces/IERC20.sol";
+import {IERC4626} from "openzeppelin-contracts/contracts/interfaces/IERC4626.sol";
+
+/// @title IConvertibleDepositFacility
+/// @notice Interface for a contract that can perform functions related to convertible deposit tokens
+interface IConvertibleDepositFacility {
+ // ========== CONVERTIBLE DEPOSIT ACTIONS ========== //
+
+ /// @notice Converts convertible deposit tokens to OHM before expiry
+ /// @dev The implementing contract is expected to handle the following:
+ /// - Validating that `account_` has an active convertible deposit position
+ /// - Validating that `account_` has the required amount of convertible deposit tokens
+ /// - Validating that all of the positions have not expired
+ /// - Burning the convertible deposit tokens
+ /// - Minting OHM to `account_`
+ /// - Transferring the reserve token to the treasury
+ /// - Emitting an event
+ ///
+ /// @param account_ The address to convert for
+ /// @param positionIds_ An array of position ids that will be converted
+ /// @param amounts_ An array of amounts of convertible deposit tokens to convert
+ /// @return converted The amount of OHM minted to `account_`
+ function convertFor(
+ address account_,
+ uint256[] memory positionIds_,
+ uint256[] memory amounts_
+ ) external returns (uint256 converted);
+
+ /// @notice Reclaims convertible deposit tokens after expiry
+ /// @dev The implementing contract is expected to handle the following:
+ /// - Validating that `account_` has an active convertible deposit position
+ /// - Validating that `account_` has the required amount of convertible deposit tokens
+ /// - Validating that all of the positions have expired
+ /// - Burning the convertible deposit tokens
+ /// - Transferring the reserve token to `account_`
+ /// - Emitting an event
+ ///
+ /// @param account_ The address to reclaim for
+ /// @param positionIds_ An array of position ids that will be reclaimed
+ /// @param amounts_ An array of amounts of convertible deposit tokens to reclaim
+ /// @return reclaimed The amount of reserve token returned to `account_`
+ function reclaimFor(
+ address account_,
+ uint256[] memory positionIds_,
+ uint256[] memory amounts_
+ ) external returns (uint256 reclaimed);
+
+ // TODO decide in the depositFor and withdrawTo functions should be in the ERC20 contract (aka ERC20Wrapper)
+
+ /// @notice Deposits the reserve token in exchange for convertible deposit tokens
+ /// @dev The implementing contract is expected to handle the following:
+ /// - Validating that the caller has the required amount of reserve tokens
+ /// - Minting the convertible deposit tokens to the caller
+ /// - Emitting an event
+ ///
+ /// @param account_ The address to mint to
+ /// @param amount_ The amount of reserve token to mint
+ /// @return minted The amount of convertible deposit tokens minted to the caller
+ function depositFor(address account_, uint256 amount_) external returns (uint256 minted);
+
+ /// @notice Preview the amount of convertible deposit tokens that would be minted for a given amount of reserve token
+ ///
+ /// @param account_ The address to mint to
+ /// @param amount_ The amount of reserve token to mint
+ /// @return minted The amount of convertible deposit tokens that would be minted
+ function previewDepositFor(
+ address account_,
+ uint256 amount_
+ ) external view returns (uint256 minted);
+
+ /// @notice Withdraws the reserve token in exchange for convertible deposit tokens
+ /// @dev The implementing contract is expected to handle the following:
+ /// - Validating that the caller has the required amount of convertible deposit tokens
+ /// - Burning the convertible deposit tokens
+ /// - Transferring the reserve token to the caller
+ /// - Emitting an event
+ ///
+ /// @param account_ The address to withdraw to
+ /// @param amount_ The amount of convertible deposit tokens to burn
+ /// @return withdrawn The amount of reserve tokens returned to the caller
+ function withdrawTo(address account_, uint256 amount_) external returns (uint256 withdrawn);
+
+ /// @notice Preview the amount of reserve token that would be returned for a given amount of convertible deposit tokens
+ ///
+ /// @param account_ The address to withdraw to
+ /// @param amount_ The amount of convertible deposit tokens to burn
+ /// @return withdrawn The amount of reserve tokens that would be returned
+ function previewWithdrawTo(
+ address account_,
+ uint256 amount_
+ ) external view returns (uint256 withdrawn);
+
+ // ========== YIELD MANAGEMENT ========== //
+
+ /// @notice Claim the yield accrued on the reserve token
+ /// @dev The implementing contract is expected to handle the following:
+ /// - Validating that the caller has the correct role
+ /// - Withdrawing the yield from the sReserve token
+ /// - Transferring the yield to the caller
+ /// - Emitting an event
+ ///
+ /// @return yieldReserve The amount of reserve token that was swept
+ /// @return yieldSReserve The amount of sReserve token that was swept
+ function sweepYield() external returns (uint256 yieldReserve, uint256 yieldSReserve);
+
+ /// @notice Preview the amount of yield that would be swept
+ ///
+ /// @return yieldReserve The amount of reserve token that would be swept
+ /// @return yieldSReserve The amount of sReserve token that would be swept
+ function previewSweepYield()
+ external
+ view
+ returns (uint256 yieldReserve, uint256 yieldSReserve);
+
+ // ========== ADMIN ========== /
+
+ /// @notice Set the withdraw rate when withdrawing the convertible deposit token, where withdraw rate = reserve token output / convertible deposit token input
+ /// @dev The implementing contract is expected to handle the following:
+ /// - Validating that the caller has the correct role
+ /// - Validating that the new rate is within bounds
+ /// - Setting the new redeem rate
+ /// - Emitting an event
+ ///
+ /// @param newRate_ The new withdraw rate
+ function setWithdrawRate(uint256 newRate_) external;
+
+ // ========== STATE VARIABLES ========== //
+
+ /// @notice The reserve token that is exchanged for the convertible deposit token
+ function reserveToken() external view returns (IERC20);
+
+ /// @notice The sReserve token that the reserve token is deposited into
+ function sReserveToken() external view returns (IERC4626);
+
+ /// @notice The convertible deposit token that is minted to the user
+ function convertibleDepositToken() external view returns (IERC20);
+
+ /// @notice The withdraw rate when withdrawing the convertible deposit token
+ function withdrawRate() external view returns (uint256);
+}
From 629139902c0ffac03464c9d83b418319903736a0 Mon Sep 17 00:00:00 2001
From: Jem <0x0xjem@gmail.com>
Date: Fri, 13 Dec 2024 16:34:20 +0400
Subject: [PATCH 14/64] Fix compile errors
---
src/policies/EmissionManager.sol | 2 +-
src/test/mocks/MockConvertibleDepositAuctioneer.sol | 7 ++-----
2 files changed, 3 insertions(+), 6 deletions(-)
diff --git a/src/policies/EmissionManager.sol b/src/policies/EmissionManager.sol
index 440dfd92..dd9ee62b 100644
--- a/src/policies/EmissionManager.sol
+++ b/src/policies/EmissionManager.sol
@@ -172,7 +172,7 @@ contract EmissionManager is IEmissionManager, Policy, RolesConsumer {
// It then calculates the amount to sell for the coming day
(, , uint256 emission) = getNextEmission();
- uint256 remainder = cdAuctioneer.beat(
+ uint256 remainder = cdAuctioneer.setAuctionParameters(
emission,
getSizeFor(emission),
getMinPriceFor(PRICE.getCurrentPrice())
diff --git a/src/test/mocks/MockConvertibleDepositAuctioneer.sol b/src/test/mocks/MockConvertibleDepositAuctioneer.sol
index c1e154f0..1f965632 100644
--- a/src/test/mocks/MockConvertibleDepositAuctioneer.sol
+++ b/src/test/mocks/MockConvertibleDepositAuctioneer.sol
@@ -32,12 +32,9 @@ contract MockConvertibleDepositAuctioneer is IConvertibleDepositAuctioneer, Poli
function getDay() external view override returns (Day memory day) {}
- function convertFor(
- uint256 deposit,
- uint256 price
- ) external view override returns (uint256 convertable) {}
+ function previewBid(uint256 deposit) external view override returns (uint256 convertable) {}
- function beat(
+ function setAuctionParameters(
uint256 newTarget,
uint256 newSize,
uint256 newMinPrice
From 55397f881000a6a38342da7847f6069159a1d64d Mon Sep 17 00:00:00 2001
From: Jem <0x0xjem@gmail.com>
Date: Fri, 13 Dec 2024 16:38:59 +0400
Subject: [PATCH 15/64] Fix licenses
---
src/policies/interfaces/IConvertibleDepositAuctioneer.sol | 2 +-
src/policies/interfaces/IConvertibleDepositFacility.sol | 2 +-
src/policies/interfaces/IConvertibleDepositToken.sol | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/policies/interfaces/IConvertibleDepositAuctioneer.sol b/src/policies/interfaces/IConvertibleDepositAuctioneer.sol
index 7eaf71c1..e42109f6 100644
--- a/src/policies/interfaces/IConvertibleDepositAuctioneer.sol
+++ b/src/policies/interfaces/IConvertibleDepositAuctioneer.sol
@@ -1,4 +1,4 @@
-// SPDX-License-Identifier: MIT
+// SPDX-License-Identifier: AGPL-3.0
pragma solidity >=0.8.0;
interface IConvertibleDepositAuctioneer {
diff --git a/src/policies/interfaces/IConvertibleDepositFacility.sol b/src/policies/interfaces/IConvertibleDepositFacility.sol
index 60ead170..0b1ed28a 100644
--- a/src/policies/interfaces/IConvertibleDepositFacility.sol
+++ b/src/policies/interfaces/IConvertibleDepositFacility.sol
@@ -1,4 +1,4 @@
-// SPDX-License-Identifier: MIT
+// SPDX-License-Identifier: AGPL-3.0
pragma solidity ^0.8.0;
import {IERC20} from "openzeppelin-contracts/contracts/interfaces/IERC20.sol";
diff --git a/src/policies/interfaces/IConvertibleDepositToken.sol b/src/policies/interfaces/IConvertibleDepositToken.sol
index ca2482ab..93246071 100644
--- a/src/policies/interfaces/IConvertibleDepositToken.sol
+++ b/src/policies/interfaces/IConvertibleDepositToken.sol
@@ -1,4 +1,4 @@
-// SPDX-License-Identifier: MIT
+// SPDX-License-Identifier: AGPL-3.0
pragma solidity ^0.8.0;
import {IERC20} from "openzeppelin-contracts/contracts/interfaces/IERC20.sol";
From 7953b936eafe5088b2b3305607b8e587a3250e40 Mon Sep 17 00:00:00 2001
From: Jem <0x0xjem@gmail.com>
Date: Fri, 13 Dec 2024 17:19:48 +0400
Subject: [PATCH 16/64] Interface for a proposed CDEPO module
---
src/modules/CDEPO/CDEPO.v1.sol | 139 +++++++++++++++++++++++++++++++++
src/policies/CDFacility.sol | 8 ++
2 files changed, 147 insertions(+)
create mode 100644 src/modules/CDEPO/CDEPO.v1.sol
diff --git a/src/modules/CDEPO/CDEPO.v1.sol b/src/modules/CDEPO/CDEPO.v1.sol
new file mode 100644
index 00000000..999051b6
--- /dev/null
+++ b/src/modules/CDEPO/CDEPO.v1.sol
@@ -0,0 +1,139 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+pragma solidity 0.8.15;
+
+import {Kernel, Module} from "src/Kernel.sol";
+import {ERC20} from "solmate/tokens/ERC20.sol";
+import {ERC4626} from "solmate/mixins/ERC4626.sol";
+
+/// @title Convertible Deposit Token
+abstract contract CDEPOv1 is Module, ERC20 {
+ // ========== CONSTANTS ========== //
+
+ /// @notice Equivalent to 100%
+ uint16 public constant ONE_HUNDRED_PERCENT = 100e2;
+
+ // // ========== CONSTRUCTOR ========== //
+
+ // constructor(address kernel_, address erc4626Vault_) Module(Kernel(kernel_)) {
+ // // Store the vault and asset
+ // vault = ERC4626(erc4626Vault_);
+ // asset = ERC20(vault.asset());
+
+ // // Set the name and symbol
+ // name = string.concat("cd", asset.symbol());
+ // symbol = string.concat("cd", asset.symbol());
+ // decimals = asset.decimals();
+
+ // // Set the initial chain id and domain separator (see solmate/tokens/ERC20.sol)
+ // INITIAL_CHAIN_ID = block.chainid;
+ // INITIAL_DOMAIN_SEPARATOR = computeDomainSeparator();
+ // }
+
+ // ========== ERC20 OVERRIDES ========== //
+
+ /// @notice Mint tokens to the caller in exchange for the underlying asset
+ /// @dev The implementing function should perform the following:
+ /// - Transfers the underlying asset from the caller to the contract
+ /// - Mints the corresponding amount of convertible deposit tokens to the caller
+ /// - Deposits the underlying asset into the ERC4626 vault
+ /// - Emits a `Transfer` event
+ ///
+ /// @param amount_ The amount of underlying asset to transfer
+ function mint(uint256 amount_) external virtual;
+
+ /// @notice Mint tokens to `to_` in exchange for the underlying asset
+ /// @dev This function behaves the same as `mint`, but allows the caller to
+ /// specify the address to mint the tokens to and pull the underlying
+ /// asset from.
+ ///
+ /// @param to_ The address to mint the tokens to
+ /// @param amount_ The amount of underlying asset to transfer
+ function mintTo(address to_, uint256 amount_) external virtual;
+
+ /// @notice Preview the amount of convertible deposit tokens that would be minted for a given amount of underlying asset
+ /// @dev The implementing function should perform the following:
+ /// - Computes the amount of convertible deposit tokens that would be minted for the given amount of underlying asset
+ /// - Returns the computed amount
+ ///
+ /// @param amount_ The amount of underlying asset to transfer
+ /// @return tokensOut The amount of convertible deposit tokens that would be minted
+ function previewMint(uint256 amount_) external view virtual returns (uint256 tokensOut);
+
+ /// @notice Burn tokens from the caller and return the underlying asset
+ /// The amount of underlying asset may not be 1:1 with the amount of
+ /// convertible deposit tokens, depending on the value of `burnRate`
+ /// @dev The implementing function should perform the following:
+ /// - Withdraws the underlying asset from the ERC4626 vault
+ /// - Transfers the underlying asset to the caller
+ /// - Burns the corresponding amount of convertible deposit tokens from the caller
+ /// - Marks the forfeited amount of the underlying asset as yield
+ /// - Emits a `Transfer` event
+ ///
+ /// @param amount_ The amount of convertible deposit tokens to burn
+ function burn(uint256 amount_) external virtual;
+
+ /// @notice Burn tokens from `from_` and return the underlying asset
+ /// @dev This function behaves the same as `burn`, but allows the caller to
+ /// specify the address to burn the tokens from and transfer the underlying
+ /// asset to.
+ ///
+ /// @param from_ The address to burn the tokens from
+ /// @param amount_ The amount of convertible deposit tokens to burn
+ function burnFrom(address from_, uint256 amount_) external virtual;
+
+ /// @notice Preview the amount of underlying asset that would be returned for a given amount of convertible deposit tokens
+ /// @dev The implementing function should perform the following:
+ /// - Computes the amount of underlying asset that would be returned for the given amount of convertible deposit tokens
+ /// - Returns the computed amount
+ ///
+ /// @param amount_ The amount of convertible deposit tokens to burn
+ /// @return assetsOut The amount of underlying asset that would be returned
+ function previewBurn(uint256 amount_) external view virtual returns (uint256 assetsOut);
+
+ // ========== YIELD MANAGER ========== //
+
+ /// @notice Claim the yield accrued on the reserve token
+ /// @dev The implementing function should perform the following:
+ /// - Validating that the caller has the correct role
+ /// - Withdrawing the yield from the sReserve token
+ /// - Transferring the yield to the caller
+ /// - Emitting an event
+ ///
+ /// @return yieldReserve The amount of reserve token that was swept
+ /// @return yieldSReserve The amount of sReserve token that was swept
+ function sweepYield() external virtual returns (uint256 yieldReserve, uint256 yieldSReserve);
+
+ /// @notice Preview the amount of yield that would be swept
+ ///
+ /// @return yieldReserve The amount of reserve token that would be swept
+ /// @return yieldSReserve The amount of sReserve token that would be swept
+ function previewSweepYield()
+ external
+ view
+ virtual
+ returns (uint256 yieldReserve, uint256 yieldSReserve);
+
+ // ========== ADMIN ========== //
+
+ /// @notice Set the burn rate of the convertible deposit token
+ /// @dev The implementing function should perform the following:
+ /// - Validating that the caller has the correct role
+ /// - Validating that the new rate is within bounds
+ /// - Setting the new burn rate
+ /// - Emitting an event
+ ///
+ /// @param newBurnRate_ The new burn rate
+ function setBurnRate(uint256 newBurnRate_) external virtual;
+
+ // ========== STATE VARIABLES ========== //
+
+ /// @notice The ERC4626 vault that holds the underlying asset
+ function getVault() external view virtual returns (address);
+
+ /// @notice The underlying asset
+ function getAsset() external view virtual returns (address);
+
+ /// @notice The burn rate of the convertible deposit token
+ /// @dev A burn rate of 99e2 (99%) means that for every 100 convertible deposit tokens burned, 99 underlying asset tokens are returned
+ function burnRate() external view virtual returns (uint16);
+}
diff --git a/src/policies/CDFacility.sol b/src/policies/CDFacility.sol
index 53f4240b..2567a669 100644
--- a/src/policies/CDFacility.sol
+++ b/src/policies/CDFacility.sol
@@ -126,6 +126,8 @@ contract CDFacility is Policy, RolesConsumer {
// store convertable deposit info and mint cdUSDS
cdInfo[user].push(CD(amount, convertable, expiry));
+ // TODO shift mint/transfer functionality to CDEPO
+
// TODO consider if the ERC20 should custody the deposit token
cdUSDS.mint(user, amount);
}
@@ -202,6 +204,8 @@ contract CDFacility is Policy, RolesConsumer {
cd.convertable -= convertable; // reverts on overflow
}
+ // TODO shift burn and transfer functionality to CDEPO
+
// burn cdUSDS
cdUSDS.burn(msg.sender, returned);
@@ -220,6 +224,8 @@ contract CDFacility is Policy, RolesConsumer {
// ========== cdUSDS ========== //
+ // TODO shift these to CDEPO
+
/// @notice allow user to mint cdUSDS
/// @notice redeeming without a CD may be at a discount
/// @param amount of reserve token
@@ -251,6 +257,8 @@ contract CDFacility is Policy, RolesConsumer {
// ========== YIELD MANAGER ========== //
+ // TODO shift these to CDEPO
+
/// @notice allow yield manager to sweep yield accrued on reserves
/// @return yield yield in reserve token terms
/// @return shares yield in sReserve token terms
From 43ca59c378551ce04cff3bc05e7a89c3e72c6060 Mon Sep 17 00:00:00 2001
From: Jem <0x0xjem@gmail.com>
Date: Fri, 13 Dec 2024 18:49:47 +0400
Subject: [PATCH 17/64] Update description
---
src/modules/CDEPO/CDEPO.v1.sol | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/modules/CDEPO/CDEPO.v1.sol b/src/modules/CDEPO/CDEPO.v1.sol
index 999051b6..ae981672 100644
--- a/src/modules/CDEPO/CDEPO.v1.sol
+++ b/src/modules/CDEPO/CDEPO.v1.sol
@@ -5,7 +5,8 @@ import {Kernel, Module} from "src/Kernel.sol";
import {ERC20} from "solmate/tokens/ERC20.sol";
import {ERC4626} from "solmate/mixins/ERC4626.sol";
-/// @title Convertible Deposit Token
+/// @title CDEPOv1
+/// @notice This is a base contract for a custodial convertible deposit token. It is designed to be used in conjunction with an ERC4626 vault.
abstract contract CDEPOv1 is Module, ERC20 {
// ========== CONSTANTS ========== //
From c3a7ee87cb4cdb5a816ef6dd607698f0072381c9 Mon Sep 17 00:00:00 2001
From: Jem <0x0xjem@gmail.com>
Date: Mon, 16 Dec 2024 12:51:23 +0400
Subject: [PATCH 18/64] Add module for convertible deposit terms
---
src/modules/CDEPO/CDEPO.v1.sol | 20 ++++-
src/modules/CTERM/CTERM.v1.sol | 150 +++++++++++++++++++++++++++++++++
2 files changed, 166 insertions(+), 4 deletions(-)
create mode 100644 src/modules/CTERM/CTERM.v1.sol
diff --git a/src/modules/CDEPO/CDEPO.v1.sol b/src/modules/CDEPO/CDEPO.v1.sol
index ae981672..6ff35bbb 100644
--- a/src/modules/CDEPO/CDEPO.v1.sol
+++ b/src/modules/CDEPO/CDEPO.v1.sol
@@ -13,6 +13,18 @@ abstract contract CDEPOv1 is Module, ERC20 {
/// @notice Equivalent to 100%
uint16 public constant ONE_HUNDRED_PERCENT = 100e2;
+ // ========== STATE VARIABLES ========== //
+
+ /// @notice The burn rate of the convertible deposit token
+ /// @dev A burn rate of 99e2 (99%) means that for every 100 convertible deposit tokens burned, 99 underlying asset tokens are returned
+ uint16 internal _burnRate;
+
+ /// @notice The total amount of deposits in the contract
+ uint256 public totalDeposits;
+
+ /// @notice The total amount of vault shares in the contract
+ uint256 public totalShares;
+
// // ========== CONSTRUCTOR ========== //
// constructor(address kernel_, address erc4626Vault_) Module(Kernel(kernel_)) {
@@ -124,15 +136,15 @@ abstract contract CDEPOv1 is Module, ERC20 {
/// - Emitting an event
///
/// @param newBurnRate_ The new burn rate
- function setBurnRate(uint256 newBurnRate_) external virtual;
+ function setBurnRate(uint16 newBurnRate_) external virtual;
// ========== STATE VARIABLES ========== //
/// @notice The ERC4626 vault that holds the underlying asset
- function getVault() external view virtual returns (address);
+ function vault() external view virtual returns (ERC4626);
- /// @notice The underlying asset
- function getAsset() external view virtual returns (address);
+ /// @notice The underlying ERC20 asset
+ function asset() external view virtual returns (ERC20);
/// @notice The burn rate of the convertible deposit token
/// @dev A burn rate of 99e2 (99%) means that for every 100 convertible deposit tokens burned, 99 underlying asset tokens are returned
diff --git a/src/modules/CTERM/CTERM.v1.sol b/src/modules/CTERM/CTERM.v1.sol
new file mode 100644
index 00000000..96f2981d
--- /dev/null
+++ b/src/modules/CTERM/CTERM.v1.sol
@@ -0,0 +1,150 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+pragma solidity 0.8.15;
+
+import {Module} from "src/Kernel.sol";
+import {ERC721} from "solmate/tokens/ERC721.sol";
+
+/// @title CTERMv1
+/// @notice This defines the interface for the CTERM module.
+/// The objective of this module is to track the terms of a convertible deposit.
+abstract contract CTERMv1 is Module, ERC721 {
+ // ========== DATA STRUCTURES ========== //
+
+ /// @notice Data structure for the terms of a convertible deposit
+ ///
+ /// @param owner The address of the owner of the term
+ /// @param remainingDeposit Amount of reserve tokens remaining to be converted
+ /// @param conversionPrice Price of the reserve token in USD
+ /// @param expiry Timestamp when the term expires
+ /// @param wrapped Whether the term is wrapped
+ struct ConvertibleDepositTerm {
+ address owner;
+ uint256 remainingDeposit;
+ uint256 conversionPrice;
+ uint48 expiry;
+ bool wrapped;
+ }
+
+ // ========== STATE VARIABLES ========== //
+
+ /// @notice The number of terms created
+ uint256 public termCount;
+
+ /// @notice Mapping of term records to an ID
+ /// @dev IDs are assigned sequentially starting from 0
+ /// Mapping entries should not be deleted, but can be overwritten
+ mapping(uint256 => ConvertibleDepositTerm) internal _terms;
+
+ /// @notice Mapping of user addresses to their term IDs
+ mapping(address => uint256[]) internal _userTerms;
+
+ // ========== ERRORS ========== //
+
+ /// @notice Error thrown when an invalid term ID is provided
+ error CTERM_InvalidTermId(uint256 id_);
+
+ /// @notice Error thrown when a term has already been wrapped
+ error CTERM_AlreadyWrapped(uint256 termId_);
+
+ /// @notice Error thrown when a term has not been wrapped
+ error CTERM_NotWrapped(uint256 termId_);
+
+ /// @notice Error thrown when an invalid amount is provided
+ error CTERM_InvalidAmount(uint256 amount_);
+
+ // ========== WRAPPING ========== //
+
+ /// @notice Wraps a term into an ERC721 token
+ /// @dev This is useful if the term owner wants a tokenized representation of their term. It is functionally equivalent to the term itself.
+ ///
+ /// The implementing function should do the following:
+ /// - Validate that the caller is the owner of the term
+ /// - Validate that the term is not already wrapped
+ /// - Mint an ERC721 token to the term owner
+ ///
+ /// @param termId_ The ID of the term to wrap
+ function wrap(uint256 termId_) external virtual;
+
+ /// @notice Unwraps an ERC721 token into a term
+ /// @dev This is useful if the term owner wants to convert their token back into the term.
+ ///
+ /// The implementing function should do the following:
+ /// - Validate that the caller is the owner of the term
+ /// - Validate that the term is already wrapped
+ /// - Burn the ERC721 token
+ ///
+ /// @param termId_ The ID of the term to unwrap
+ function unwrap(uint256 termId_) external virtual;
+
+ // ========== TERM MANAGEMENT =========== //
+
+ /// @notice Creates a new convertible deposit term
+ /// @dev The implementing function should do the following:
+ /// - Validate that the caller is permissioned
+ /// - Validate that the remaining deposit is greater than 0
+ /// - Validate that the conversion price is greater than 0
+ /// - Validate that the expiry is in the future
+ /// - Create the term record
+ /// - Wrap the term if requested
+ ///
+ /// @param owner_ The address of the owner of the term
+ /// @param remainingDeposit_ The amount of reserve tokens remaining to be converted
+ /// @param conversionPrice_ The price of the reserve token in USD
+ /// @param expiry_ The timestamp when the term expires
+ /// @param wrap_ Whether the term should be wrapped
+ /// @return termId The ID of the new term
+ function create(
+ address owner_,
+ uint256 remainingDeposit_,
+ uint256 conversionPrice_,
+ uint48 expiry_,
+ bool wrap_
+ ) external virtual returns (uint256 termId);
+
+ /// @notice Updates the remaining deposit of a term
+ /// @dev The implementing function should do the following:
+ /// - Validate that the caller is permissioned
+ /// - Validate that the amount is greater than 0
+ /// - Update the remaining deposit of the term
+ ///
+ /// @param termId_ The ID of the term to update
+ /// @param amount_ The new amount of the term
+ function update(uint256 termId_, uint256 amount_) external virtual;
+
+ /// @notice Splits the specified amount of the term into a new term
+ /// This is useful if the term owner wants to split their term into multiple smaller terms.
+ /// @dev The implementing function should do the following:
+ /// - Validate that the caller is the owner of the term
+ /// - Validate that the amount is greater than 0
+ /// - Validate that the amount is less than or equal to the remaining deposit
+ /// - Validate that the to address is not the zero address
+ /// - Update the remaining deposit of the original term
+ /// - Create the new term record
+ /// - Wrap the new term if requested
+ ///
+ /// @param termId_ The ID of the term to split
+ /// @param amount_ The amount of the term to split
+ /// @param to_ The address to split the term to
+ /// @param wrap_ Whether the new term should be wrapped
+ /// @return newTermId The ID of the new term
+ function split(
+ uint256 termId_,
+ uint256 amount_,
+ address to_,
+ bool wrap_
+ ) external virtual returns (uint256 newTermId);
+
+ // ========== TERM INFORMATION ========== //
+
+ /// @notice Get the IDs of all terms for a given user
+ ///
+ /// @param user The address of the user
+ /// @return termIds An array of term IDs
+ function getUserTermIds(address user) external view virtual returns (uint256[] memory termIds);
+
+ /// @notice Get the terms for a given ID
+ ///
+ /// @param termId_ The ID of the term
+ /// @return term The terms for the given ID
+ function getTerm(uint256 termId_) external view virtual returns (ConvertibleDepositTerm memory);
+}
From 349c671c4d6a0ca8d089c1181c8b786b35250075 Mon Sep 17 00:00:00 2001
From: Jem <0x0xjem@gmail.com>
Date: Mon, 16 Dec 2024 13:17:38 +0400
Subject: [PATCH 19/64] Add CTERM events
---
src/modules/CTERM/CTERM.v1.sol | 24 ++++++++++++++++++++++++
1 file changed, 24 insertions(+)
diff --git a/src/modules/CTERM/CTERM.v1.sol b/src/modules/CTERM/CTERM.v1.sol
index 96f2981d..9d7ab2de 100644
--- a/src/modules/CTERM/CTERM.v1.sol
+++ b/src/modules/CTERM/CTERM.v1.sol
@@ -25,6 +25,30 @@ abstract contract CTERMv1 is Module, ERC721 {
bool wrapped;
}
+ // ========== EVENTS ========== //
+
+ /// @notice Emitted when a term is created
+ event TermCreated(
+ uint256 indexed termId,
+ address indexed owner,
+ uint256 remainingDeposit,
+ uint256 conversionPrice,
+ uint48 expiry,
+ bool wrapped
+ );
+
+ /// @notice Emitted when a term is updated
+ event TermUpdated(uint256 indexed termId, uint256 remainingDeposit);
+
+ /// @notice Emitted when a term is split
+ event TermSplit(uint256 indexed termId, uint256 newTermId, uint256 amount, uint256 to);
+
+ /// @notice Emitted when a term is wrapped
+ event TermWrapped(uint256 indexed termId);
+
+ /// @notice Emitted when a term is unwrapped
+ event TermUnwrapped(uint256 indexed termId);
+
// ========== STATE VARIABLES ========== //
/// @notice The number of terms created
From 6b20f808c23059b81ee44ce7ba9991e30add3451 Mon Sep 17 00:00:00 2001
From: Jem <0x0xjem@gmail.com>
Date: Mon, 16 Dec 2024 13:17:50 +0400
Subject: [PATCH 20/64] WIP implementation of CDEPO
---
src/modules/CDEPO/CDEPO.v1.sol | 33 ++---
.../CDEPO/OlympusConvertibleDepository.sol | 114 ++++++++++++++++++
2 files changed, 127 insertions(+), 20 deletions(-)
create mode 100644 src/modules/CDEPO/OlympusConvertibleDepository.sol
diff --git a/src/modules/CDEPO/CDEPO.v1.sol b/src/modules/CDEPO/CDEPO.v1.sol
index 6ff35bbb..de1f181d 100644
--- a/src/modules/CDEPO/CDEPO.v1.sol
+++ b/src/modules/CDEPO/CDEPO.v1.sol
@@ -8,6 +8,19 @@ import {ERC4626} from "solmate/mixins/ERC4626.sol";
/// @title CDEPOv1
/// @notice This is a base contract for a custodial convertible deposit token. It is designed to be used in conjunction with an ERC4626 vault.
abstract contract CDEPOv1 is Module, ERC20 {
+ // ========== EVENTS ========== //
+
+ /// @notice Emitted when the burn rate is updated
+ event BurnRateUpdated(uint16 newBurnRate);
+
+ /// @notice Emitted when the yield is swept
+ event YieldSwept(address receiver, uint256 reserveAmount, uint256 sReserveAmount);
+
+ // ========== ERRORS ========== //
+
+ /// @notice Thrown when the caller provides invalid arguments
+ error CDEPO_InvalidArgs(string reason);
+
// ========== CONSTANTS ========== //
/// @notice Equivalent to 100%
@@ -19,29 +32,9 @@ abstract contract CDEPOv1 is Module, ERC20 {
/// @dev A burn rate of 99e2 (99%) means that for every 100 convertible deposit tokens burned, 99 underlying asset tokens are returned
uint16 internal _burnRate;
- /// @notice The total amount of deposits in the contract
- uint256 public totalDeposits;
-
/// @notice The total amount of vault shares in the contract
uint256 public totalShares;
- // // ========== CONSTRUCTOR ========== //
-
- // constructor(address kernel_, address erc4626Vault_) Module(Kernel(kernel_)) {
- // // Store the vault and asset
- // vault = ERC4626(erc4626Vault_);
- // asset = ERC20(vault.asset());
-
- // // Set the name and symbol
- // name = string.concat("cd", asset.symbol());
- // symbol = string.concat("cd", asset.symbol());
- // decimals = asset.decimals();
-
- // // Set the initial chain id and domain separator (see solmate/tokens/ERC20.sol)
- // INITIAL_CHAIN_ID = block.chainid;
- // INITIAL_DOMAIN_SEPARATOR = computeDomainSeparator();
- // }
-
// ========== ERC20 OVERRIDES ========== //
/// @notice Mint tokens to the caller in exchange for the underlying asset
diff --git a/src/modules/CDEPO/OlympusConvertibleDepository.sol b/src/modules/CDEPO/OlympusConvertibleDepository.sol
new file mode 100644
index 00000000..7110013e
--- /dev/null
+++ b/src/modules/CDEPO/OlympusConvertibleDepository.sol
@@ -0,0 +1,114 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+pragma solidity 0.8.15;
+
+import {CDEPOv1} from "./CDEPO.v1.sol";
+import {Kernel, Module} from "src/Kernel.sol";
+import {ERC20} from "solmate/tokens/ERC20.sol";
+import {ERC4626} from "solmate/mixins/ERC4626.sol";
+
+contract OlympusConvertibleDepository is CDEPOv1 {
+ // ========== STATE VARIABLES ========== //
+
+ /// @inheritdoc CDEPOv1
+ ERC4626 public immutable override vault;
+
+ /// @inheritdoc CDEPOv1
+ ERC20 public immutable override asset;
+
+ /// @inheritdoc CDEPOv1
+ uint16 public override burnRate;
+
+ // ========== CONSTRUCTOR ========== //
+
+ constructor(
+ address kernel_,
+ address erc4626Vault_
+ )
+ Module(Kernel(kernel_))
+ ERC20(
+ string.concat("cd", ERC20(ERC4626(erc4626Vault_).asset()).symbol()),
+ string.concat("cd", ERC20(ERC4626(erc4626Vault_).asset()).symbol()),
+ ERC4626(erc4626Vault_).decimals()
+ )
+ {
+ // Store the vault and asset
+ vault = ERC4626(erc4626Vault_);
+ asset = ERC20(vault.asset());
+ }
+
+ // ========== ERC20 OVERRIDES ========== //
+
+ function mint(uint256 amount_) external virtual override {}
+
+ function mintTo(address to_, uint256 amount_) external virtual override {}
+
+ function previewMint(
+ uint256 amount_
+ ) external view virtual override returns (uint256 tokensOut) {}
+
+ function burn(uint256 amount_) external virtual override {}
+
+ function burnFrom(address from_, uint256 amount_) external virtual override {}
+
+ function previewBurn(
+ uint256 amount_
+ ) external view virtual override returns (uint256 assetsOut) {}
+
+ // ========== YIELD MANAGER ========== //
+
+ /// @inheritdoc CDEPOv1
+ function sweepYield()
+ external
+ virtual
+ override
+ permissioned
+ returns (uint256 yieldReserve, uint256 yieldSReserve)
+ {
+ (yieldReserve, yieldSReserve) = previewSweepYield();
+
+ // Reduce the shares tracked by the contract
+ totalShares -= yieldSReserve;
+
+ // Transfer the yield to the permissioned caller
+ vault.transfer(msg.sender, yieldSReserve);
+
+ // Emit the event
+ emit YieldSwept(msg.sender, yieldReserve, yieldSReserve);
+
+ return (yieldReserve, yieldSReserve);
+ }
+
+ /// @inheritdoc CDEPOv1
+ function previewSweepYield()
+ public
+ view
+ virtual
+ override
+ returns (uint256 yieldReserve, uint256 yieldSReserve)
+ {
+ // The yield is the difference between the quantity of underlying assets in the vault and the quantity CD tokens issued
+ yieldReserve = vault.previewRedeem(totalShares) - totalSupply;
+
+ // The yield in sReserve terms is the quantity of vault shares that would be burnt if yieldReserve was redeemed
+ yieldSReserve = vault.previewWithdraw(yieldReserve);
+
+ return (yieldReserve, yieldSReserve);
+ }
+
+ // ========== ADMIN ========== //
+
+ /// @inheritdoc CDEPOv1
+ /// @dev This function reverts if:
+ /// - The caller is not permissioned
+ /// - The new burn rate is not within bounds
+ function setBurnRate(uint16 newBurnRate_) external virtual override permissioned {
+ // Validate that the burn rate is within bounds
+ if (newBurnRate_ > ONE_HUNDRED_PERCENT) revert CDEPO_InvalidArgs("Greater than 100%");
+
+ // Update the burn rate
+ burnRate = newBurnRate_;
+
+ // Emit the event
+ emit BurnRateUpdated(newBurnRate_);
+ }
+}
From 5536d546db158bd27c7543c918ebb3e914bb4446 Mon Sep 17 00:00:00 2001
From: Jem <0x0xjem@gmail.com>
Date: Mon, 16 Dec 2024 13:34:21 +0400
Subject: [PATCH 21/64] First pass a CDEPO
---
.../CDEPO/OlympusConvertibleDepository.sol | 83 +++++++++++++++++--
1 file changed, 75 insertions(+), 8 deletions(-)
diff --git a/src/modules/CDEPO/OlympusConvertibleDepository.sol b/src/modules/CDEPO/OlympusConvertibleDepository.sol
index 7110013e..f5d968e8 100644
--- a/src/modules/CDEPO/OlympusConvertibleDepository.sol
+++ b/src/modules/CDEPO/OlympusConvertibleDepository.sol
@@ -38,25 +38,92 @@ contract OlympusConvertibleDepository is CDEPOv1 {
// ========== ERC20 OVERRIDES ========== //
- function mint(uint256 amount_) external virtual override {}
+ /// @inheritdoc CDEPOv1
+ /// @dev This function performs the following:
+ /// - Calls `mintTo` with the caller as the recipient
+ function mint(uint256 amount_) external virtual override {
+ mintTo(msg.sender, amount_);
+ }
- function mintTo(address to_, uint256 amount_) external virtual override {}
+ /// @inheritdoc CDEPOv1
+ /// @dev This function performs the following:
+ /// - Transfers the underlying asset from `to_` to the contract
+ /// - Deposits the underlying asset into the ERC4626 vault
+ /// - Mints the corresponding amount of convertible deposit tokens to `to_`
+ /// - Emits a `Transfer` event
+ ///
+ /// @param to_ The address to mint the tokens to
+ /// @param amount_ The amount of underlying asset to transfer
+ function mintTo(address to_, uint256 amount_) public virtual override {
+ // Transfer the underlying asset to the contract
+ asset.transferFrom(to_, address(this), amount_);
+
+ // Deposit the underlying asset into the vault and update the total shares
+ totalShares += vault.deposit(amount_, to_);
+
+ // Mint the CD tokens to the caller
+ _mint(to_, amount_);
+ }
+ /// @inheritdoc CDEPOv1
+ /// @dev CD tokens are minted 1:1 with underlying asset, so this function returns the amount of underlying asset
function previewMint(
uint256 amount_
- ) external view virtual override returns (uint256 tokensOut) {}
+ ) external view virtual override returns (uint256 tokensOut) {
+ return amount_;
+ }
- function burn(uint256 amount_) external virtual override {}
+ /// @inheritdoc CDEPOv1
+ /// @dev This function performs the following:
+ /// - Calls `burnFrom` with the caller as the address to burn the tokens from
+ function burn(uint256 amount_) external virtual override {
+ burnFrom(msg.sender, amount_);
+ }
- function burnFrom(address from_, uint256 amount_) external virtual override {}
+ /// @inheritdoc CDEPOv1
+ /// @dev This function performs the following:
+ /// - Burns the CD tokens from `from_`
+ /// - Calculates the quantity of underlying asset to withdraw and return
+ /// - Returns the underlying asset to `from_`
+ /// - Emits a `Transfer` event
+ ///
+ /// @param from_ The address to burn the tokens from
+ /// @param amount_ The amount of CD tokens to burn
+ function burnFrom(address from_, uint256 amount_) public virtual override {
+ // Burn the CD tokens from `from_`
+ _burn(from_, amount_);
+
+ // Calculate the quantity of underlying asset to withdraw and return
+ // This will create a difference between the quantity of underlying assets and the vault shares, which will be swept as yield
+ // TODO make sure there are no shares left over if all CD tokens are burned
+ uint256 discountedAssetsOut = previewBurn(amount_);
+ uint256 shares = vault.previewWithdraw(discountedAssetsOut);
+ totalShares -= shares;
+
+ // Return the underlying asset to `from_`
+ vault.redeem(shares, from_, address(this));
+ }
- function previewBurn(
- uint256 amount_
- ) external view virtual override returns (uint256 assetsOut) {}
+ /// @inheritdoc CDEPOv1
+ function previewBurn(uint256 amount_) public view virtual override returns (uint256 assetsOut) {
+ assetsOut = (amount_ * burnRate) / ONE_HUNDRED_PERCENT;
+ }
// ========== YIELD MANAGER ========== //
/// @inheritdoc CDEPOv1
+ /// @dev This function performs the following:
+ /// - Validates that the caller has the correct role
+ /// - Computes the amount of yield that would be swept
+ /// - Reduces the shares tracked by the contract
+ /// - Transfers the yield to the caller
+ /// - Emits an event
+ ///
+ /// This function reverts if:
+ /// - The caller is not permissioned
+ ///
+ /// @return yieldReserve The amount of reserve token that was swept
+ /// @return yieldSReserve The amount of sReserve token that was swept
function sweepYield()
external
virtual
From 9fb539ce4bfab6c77606e32b751cb82f7a888056 Mon Sep 17 00:00:00 2001
From: Jem <0x0xjem@gmail.com>
Date: Mon, 16 Dec 2024 15:46:03 +0400
Subject: [PATCH 22/64] First pass at implementation of CTERM
---
src/modules/CTERM/CTERM.v1.sol | 26 +-
.../CTERM/OlympusConvertibleDepositTerms.sol | 262 ++++++++++++++++++
2 files changed, 279 insertions(+), 9 deletions(-)
create mode 100644 src/modules/CTERM/OlympusConvertibleDepositTerms.sol
diff --git a/src/modules/CTERM/CTERM.v1.sol b/src/modules/CTERM/CTERM.v1.sol
index 9d7ab2de..fba2cd5c 100644
--- a/src/modules/CTERM/CTERM.v1.sol
+++ b/src/modules/CTERM/CTERM.v1.sol
@@ -12,13 +12,11 @@ abstract contract CTERMv1 is Module, ERC721 {
/// @notice Data structure for the terms of a convertible deposit
///
- /// @param owner The address of the owner of the term
/// @param remainingDeposit Amount of reserve tokens remaining to be converted
/// @param conversionPrice Price of the reserve token in USD
/// @param expiry Timestamp when the term expires
/// @param wrapped Whether the term is wrapped
struct ConvertibleDepositTerm {
- address owner;
uint256 remainingDeposit;
uint256 conversionPrice;
uint48 expiry;
@@ -41,7 +39,13 @@ abstract contract CTERMv1 is Module, ERC721 {
event TermUpdated(uint256 indexed termId, uint256 remainingDeposit);
/// @notice Emitted when a term is split
- event TermSplit(uint256 indexed termId, uint256 newTermId, uint256 amount, uint256 to);
+ event TermSplit(
+ uint256 indexed termId,
+ uint256 newTermId,
+ uint256 amount,
+ address to,
+ bool wrap
+ );
/// @notice Emitted when a term is wrapped
event TermWrapped(uint256 indexed termId);
@@ -64,6 +68,9 @@ abstract contract CTERMv1 is Module, ERC721 {
// ========== ERRORS ========== //
+ /// @notice Error thrown when the caller is not the owner of the term
+ error CTERM_NotOwner(uint256 termId_);
+
/// @notice Error thrown when an invalid term ID is provided
error CTERM_InvalidTermId(uint256 id_);
@@ -73,8 +80,8 @@ abstract contract CTERMv1 is Module, ERC721 {
/// @notice Error thrown when a term has not been wrapped
error CTERM_NotWrapped(uint256 termId_);
- /// @notice Error thrown when an invalid amount is provided
- error CTERM_InvalidAmount(uint256 amount_);
+ /// @notice Error thrown when an invalid parameter is provided
+ error CTERM_InvalidParams(string reason_);
// ========== WRAPPING ========== //
@@ -105,6 +112,7 @@ abstract contract CTERMv1 is Module, ERC721 {
/// @notice Creates a new convertible deposit term
/// @dev The implementing function should do the following:
/// - Validate that the caller is permissioned
+ /// - Validate that the owner is not the zero address
/// - Validate that the remaining deposit is greater than 0
/// - Validate that the conversion price is greater than 0
/// - Validate that the expiry is in the future
@@ -128,7 +136,7 @@ abstract contract CTERMv1 is Module, ERC721 {
/// @notice Updates the remaining deposit of a term
/// @dev The implementing function should do the following:
/// - Validate that the caller is permissioned
- /// - Validate that the amount is greater than 0
+ /// - Validate that the term ID is valid
/// - Update the remaining deposit of the term
///
/// @param termId_ The ID of the term to update
@@ -141,7 +149,7 @@ abstract contract CTERMv1 is Module, ERC721 {
/// - Validate that the caller is the owner of the term
/// - Validate that the amount is greater than 0
/// - Validate that the amount is less than or equal to the remaining deposit
- /// - Validate that the to address is not the zero address
+ /// - Validate that `to_` is not the zero address
/// - Update the remaining deposit of the original term
/// - Create the new term record
/// - Wrap the new term if requested
@@ -162,9 +170,9 @@ abstract contract CTERMv1 is Module, ERC721 {
/// @notice Get the IDs of all terms for a given user
///
- /// @param user The address of the user
+ /// @param user_ The address of the user
/// @return termIds An array of term IDs
- function getUserTermIds(address user) external view virtual returns (uint256[] memory termIds);
+ function getUserTermIds(address user_) external view virtual returns (uint256[] memory termIds);
/// @notice Get the terms for a given ID
///
diff --git a/src/modules/CTERM/OlympusConvertibleDepositTerms.sol b/src/modules/CTERM/OlympusConvertibleDepositTerms.sol
new file mode 100644
index 00000000..b0241c0b
--- /dev/null
+++ b/src/modules/CTERM/OlympusConvertibleDepositTerms.sol
@@ -0,0 +1,262 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+pragma solidity 0.8.15;
+
+import {ERC721} from "solmate/tokens/ERC721.sol";
+import {CTERMv1} from "./CTERM.v1.sol";
+import {Kernel, Module} from "src/Kernel.sol";
+
+contract OlympusConvertibleDepositTerms is CTERMv1 {
+ constructor(
+ address kernel_
+ ) Module(Kernel(kernel_)) ERC721("Olympus Convertible Deposit Terms", "OCDT") {}
+
+ // ========== WRAPPING ========== //
+
+ /// @inheritdoc CTERMv1
+ /// @dev This function reverts if:
+ /// - The term ID is invalid
+ /// - The caller is not the owner of the term
+ /// - The term is already wrapped
+ function wrap(
+ uint256 termId_
+ ) external virtual override onlyValidTerm(termId_) onlyTermOwner(termId_) {
+ // Does not need to check for invalid term ID because the modifier already ensures that
+ ConvertibleDepositTerm storage term = _terms[termId_];
+
+ // Validate that the term is not already wrapped
+ if (term.wrapped) revert CTERM_AlreadyWrapped(termId_);
+
+ // Mark the term as wrapped
+ term.wrapped = true;
+
+ // Mint the ERC721 token
+ _mint(msg.sender, termId_);
+
+ emit TermWrapped(termId_);
+ }
+
+ /// @inheritdoc CTERMv1
+ /// @dev This function reverts if:
+ /// - The term ID is invalid
+ /// - The caller is not the owner of the term
+ /// - The term is not wrapped
+ function unwrap(
+ uint256 termId_
+ ) external virtual override onlyValidTerm(termId_) onlyTermOwner(termId_) {
+ // Does not need to check for invalid term ID because the modifier already ensures that
+ ConvertibleDepositTerm storage term = _terms[termId_];
+
+ // Validate that the term is wrapped
+ if (!term.wrapped) revert CTERM_NotWrapped(termId_);
+
+ // Mark the term as unwrapped
+ term.wrapped = false;
+
+ // Burn the ERC721 token
+ _burn(termId_);
+
+ emit TermUnwrapped(termId_);
+ }
+
+ // ========== TERM MANAGEMENT =========== //
+
+ function _create(
+ address owner_,
+ uint256 remainingDeposit_,
+ uint256 conversionPrice_,
+ uint48 expiry_,
+ bool wrap_
+ ) internal returns (uint256 termId) {
+ // Create the term record
+ termId = ++termCount;
+ _terms[termId] = ConvertibleDepositTerm({
+ remainingDeposit: remainingDeposit_,
+ conversionPrice: conversionPrice_,
+ expiry: expiry_,
+ wrapped: wrap_
+ });
+
+ // Update ERC721 storage
+ _ownerOf[termId] = owner_;
+ _balanceOf[owner_]++;
+
+ // Add the term ID to the user's list of terms
+ _userTerms[owner_].push(termId);
+
+ // If specified, wrap the term
+ if (wrap_) _mint(owner_, termId);
+
+ // Emit the event
+ emit TermCreated(termId, owner_, remainingDeposit_, conversionPrice_, expiry_, wrap_);
+
+ return termId;
+ }
+
+ /// @inheritdoc CTERMv1
+ /// @dev This function reverts if:
+ /// - The caller is not permissioned
+ /// - The owner is the zero address
+ /// - The remaining deposit is 0
+ /// - The conversion price is 0
+ /// - The expiry is in the past
+ function create(
+ address owner_,
+ uint256 remainingDeposit_,
+ uint256 conversionPrice_,
+ uint48 expiry_,
+ bool wrap_
+ ) external virtual override permissioned returns (uint256 termId) {
+ // Validate that the owner is not the zero address
+ if (owner_ == address(0)) revert CTERM_InvalidParams("owner");
+
+ // Validate that the remaining deposit is greater than 0
+ if (remainingDeposit_ == 0) revert CTERM_InvalidParams("deposit");
+
+ // Validate that the conversion price is greater than 0
+ if (conversionPrice_ == 0) revert CTERM_InvalidParams("conversion price");
+
+ // Validate that the expiry is in the future
+ if (expiry_ <= block.timestamp) revert CTERM_InvalidParams("expiry");
+
+ return _create(owner_, remainingDeposit_, conversionPrice_, expiry_, wrap_);
+ }
+
+ /// @inheritdoc CTERMv1
+ /// @dev This function reverts if:
+ /// - The caller is not permissioned
+ /// - The term ID is invalid
+ function update(
+ uint256 termId_,
+ uint256 amount_
+ ) external virtual override permissioned onlyValidTerm(termId_) {
+ // Update the remaining deposit of the term
+ ConvertibleDepositTerm storage term = _terms[termId_];
+ term.remainingDeposit = amount_;
+
+ // Emit the event
+ emit TermUpdated(termId_, amount_);
+ }
+
+ /// @inheritdoc CTERMv1
+ /// @dev This function reverts if:
+ /// - The caller is not the owner of the term
+ /// - The amount is 0
+ /// - The amount is greater than the remaining deposit
+ /// - `to_` is the zero address
+ function split(
+ uint256 termId_,
+ uint256 amount_,
+ address to_,
+ bool wrap_
+ )
+ external
+ virtual
+ override
+ onlyValidTerm(termId_)
+ onlyTermOwner(termId_)
+ returns (uint256 newTermId)
+ {
+ ConvertibleDepositTerm storage term = _terms[termId_];
+
+ // Validate that the amount is greater than 0
+ if (amount_ == 0) revert CTERM_InvalidParams("amount");
+
+ // Validate that the amount is less than or equal to the remaining deposit
+ if (amount_ > term.remainingDeposit) revert CTERM_InvalidParams("amount");
+
+ // Validate that the to address is not the zero address
+ if (to_ == address(0)) revert CTERM_InvalidParams("to");
+
+ // Calculate the remaining deposit of the existing term
+ uint256 remainingDeposit = term.remainingDeposit - amount_;
+
+ // Update the remaining deposit of the existing term
+ term.remainingDeposit = remainingDeposit;
+
+ // Create the new term
+ newTermId = _create(to_, amount_, term.conversionPrice, term.expiry, wrap_);
+
+ // Emit the event
+ emit TermSplit(termId_, newTermId, amount_, to_, wrap_);
+
+ return newTermId;
+ }
+
+ // ========== ERC721 OVERRIDES ========== //
+
+ /// @inheritdoc ERC721
+ function tokenURI(uint256 id_) public view virtual override returns (string memory) {
+ // TODO implement tokenURI SVG
+ return "";
+ }
+
+ /// @inheritdoc ERC721
+ /// @dev This function performs the following:
+ /// - Updates the owner of the term
+ /// - Calls `transferFrom` on the parent contract
+ function transferFrom(address from_, address to_, uint256 tokenId_) public override {
+ ConvertibleDepositTerm storage term = _terms[tokenId_];
+
+ // Validate that the term is valid
+ if (term.conversionPrice == 0) revert CTERM_InvalidTermId(tokenId_);
+
+ // Ownership is validated in `transferFrom` on the parent contract
+
+ // Add to user terms on the destination address
+ _userTerms[to_].push(tokenId_);
+
+ // Remove from user terms on the source address
+ bool found = false;
+ for (uint256 i = 0; i < _userTerms[from_].length; i++) {
+ if (_userTerms[from_][i] == tokenId_) {
+ _userTerms[from_][i] = _userTerms[from_][_userTerms[from_].length - 1];
+ _userTerms[from_].pop();
+ found = true;
+ break;
+ }
+ }
+ if (!found) revert CTERM_InvalidTermId(tokenId_);
+
+ // Call `transferFrom` on the parent contract
+ super.transferFrom(from_, to_, tokenId_);
+ }
+
+ // ========== TERM INFORMATION ========== //
+
+ function _getTerm(uint256 termId_) internal view returns (ConvertibleDepositTerm memory) {
+ ConvertibleDepositTerm memory term = _terms[termId_];
+ // `create()` blocks a 0 conversion price, so this should never happen on a valid term
+ if (term.conversionPrice == 0) revert CTERM_InvalidTermId(termId_);
+
+ return term;
+ }
+
+ /// @inheritdoc CTERMv1
+ function getUserTermIds(
+ address user_
+ ) external view virtual override returns (uint256[] memory termIds) {
+ return _userTerms[user_];
+ }
+
+ /// @inheritdoc CTERMv1
+ /// @dev This function reverts if:
+ /// - The term ID is invalid
+ function getTerm(
+ uint256 termId_
+ ) external view virtual override returns (ConvertibleDepositTerm memory) {
+ return _getTerm(termId_);
+ }
+
+ // ========== MODIFIERS ========== //
+
+ modifier onlyValidTerm(uint256 termId_) {
+ if (_getTerm(termId_).conversionPrice == 0) revert CTERM_InvalidTermId(termId_);
+ _;
+ }
+
+ modifier onlyTermOwner(uint256 termId_) {
+ // This validates that the caller is the owner of the term
+ if (_ownerOf[termId_] != msg.sender) revert CTERM_NotOwner(termId_);
+ _;
+ }
+}
From e3dcecc9065d8c2be2eef09c58da7bee19b54459 Mon Sep 17 00:00:00 2001
From: Jem <0x0xjem@gmail.com>
Date: Mon, 16 Dec 2024 17:03:51 +0400
Subject: [PATCH 23/64] Add a permissioned function to CDEPO to redeem CD
tokens for assets. Shift CDFacility to use the interface and CDEPO/CTERM.
---
src/modules/CDEPO/CDEPO.v1.sol | 15 +-
.../CDEPO/OlympusConvertibleDepository.sol | 20 ++
src/modules/CTERM/CTERM.v1.sol | 2 +
src/policies/CDFacility.sol | 318 +++++++-----------
.../IConvertibleDepositFacility.sol | 173 ++++------
.../interfaces/IConvertibleDepositToken.sol | 18 -
6 files changed, 212 insertions(+), 334 deletions(-)
delete mode 100644 src/policies/interfaces/IConvertibleDepositToken.sol
diff --git a/src/modules/CDEPO/CDEPO.v1.sol b/src/modules/CDEPO/CDEPO.v1.sol
index de1f181d..d369f9ba 100644
--- a/src/modules/CDEPO/CDEPO.v1.sol
+++ b/src/modules/CDEPO/CDEPO.v1.sol
@@ -48,7 +48,7 @@ abstract contract CDEPOv1 is Module, ERC20 {
function mint(uint256 amount_) external virtual;
/// @notice Mint tokens to `to_` in exchange for the underlying asset
- /// @dev This function behaves the same as `mint`, but allows the caller to
+ /// This function behaves the same as `mint`, but allows the caller to
/// specify the address to mint the tokens to and pull the underlying
/// asset from.
///
@@ -79,7 +79,7 @@ abstract contract CDEPOv1 is Module, ERC20 {
function burn(uint256 amount_) external virtual;
/// @notice Burn tokens from `from_` and return the underlying asset
- /// @dev This function behaves the same as `burn`, but allows the caller to
+ /// This function behaves the same as `burn`, but allows the caller to
/// specify the address to burn the tokens from and transfer the underlying
/// asset to.
///
@@ -96,6 +96,17 @@ abstract contract CDEPOv1 is Module, ERC20 {
/// @return assetsOut The amount of underlying asset that would be returned
function previewBurn(uint256 amount_) external view virtual returns (uint256 assetsOut);
+ /// @notice Redeem convertible deposit tokens for the underlying asset
+ /// This differs from the burn function, in that it is an admin-level and permissioned function that does not apply the burn rate.
+ /// @dev The implementing function should perform the following:
+ /// - Validates that the caller is permissioned
+ /// - Transfers the corresponding vault shares to the caller
+ /// - Burns the corresponding amount of convertible deposit tokens from the caller
+ ///
+ /// @param amount_ The amount of convertible deposit tokens to burn
+ /// @return sharesOut The amount of shares that were transferred to the caller
+ function redeem(uint256 amount_) external virtual returns (uint256 sharesOut);
+
// ========== YIELD MANAGER ========== //
/// @notice Claim the yield accrued on the reserve token
diff --git a/src/modules/CDEPO/OlympusConvertibleDepository.sol b/src/modules/CDEPO/OlympusConvertibleDepository.sol
index f5d968e8..a387e5c4 100644
--- a/src/modules/CDEPO/OlympusConvertibleDepository.sol
+++ b/src/modules/CDEPO/OlympusConvertibleDepository.sol
@@ -109,6 +109,26 @@ contract OlympusConvertibleDepository is CDEPOv1 {
assetsOut = (amount_ * burnRate) / ONE_HUNDRED_PERCENT;
}
+ /// @inheritdoc CDEPOv1
+ /// @dev This function performs the following:
+ /// - Validates that the caller is permissioned
+ /// - Burns the CD tokens from the caller
+ /// - Calculates the quantity of underlying asset to withdraw and return
+ /// - Returns the underlying asset to the caller
+ ///
+ /// @param amount_ The amount of CD tokens to burn
+ function redeem(uint256 amount_) external override permissioned returns (uint256 sharesOut) {
+ // Burn the CD tokens from the caller
+ _burn(msg.sender, amount_);
+
+ // Calculate the quantity of shares to transfer
+ sharesOut = vault.previewWithdraw(amount_);
+ totalShares -= sharesOut;
+
+ // Transfer the shares to the caller
+ vault.transfer(msg.sender, sharesOut);
+ }
+
// ========== YIELD MANAGER ========== //
/// @inheritdoc CDEPOv1
diff --git a/src/modules/CTERM/CTERM.v1.sol b/src/modules/CTERM/CTERM.v1.sol
index fba2cd5c..0924b7e1 100644
--- a/src/modules/CTERM/CTERM.v1.sol
+++ b/src/modules/CTERM/CTERM.v1.sol
@@ -8,6 +8,8 @@ import {ERC721} from "solmate/tokens/ERC721.sol";
/// @notice This defines the interface for the CTERM module.
/// The objective of this module is to track the terms of a convertible deposit.
abstract contract CTERMv1 is Module, ERC721 {
+ // TODO rename to CDPOS
+
// ========== DATA STRUCTURES ========== //
/// @notice Data structure for the terms of a convertible deposit
diff --git a/src/policies/CDFacility.sol b/src/policies/CDFacility.sol
index 2567a669..d37ee3c5 100644
--- a/src/policies/CDFacility.sol
+++ b/src/policies/CDFacility.sol
@@ -7,30 +7,18 @@ import {ReentrancyGuard} from "solmate/utils/ReentrancyGuard.sol";
import {ERC20} from "solmate/tokens/ERC20.sol";
import {ERC4626} from "solmate/mixins/ERC4626.sol";
+import {IConvertibleDepositFacility} from "src/policies/interfaces/IConvertibleDepositFacility.sol";
import {RolesConsumer, ROLESv1} from "src/modules/ROLES/OlympusRoles.sol";
import {MINTRv1} from "src/modules/MINTR/MINTR.v1.sol";
import {TRSRYv1} from "src/modules/TRSRY/TRSRY.v1.sol";
-
-import {IConvertibleDepositToken} from "src/policies/interfaces/IConvertibleDepositToken.sol";
+import {CDEPOv1} from "src/modules/CDEPO/CDEPO.v1.sol";
+import {CTERMv1} from "src/modules/CTERM/CTERM.v1.sol";
import {FullMath} from "src/libraries/FullMath.sol";
-contract CDFacility is Policy, RolesConsumer {
+contract CDFacility is Policy, RolesConsumer, IConvertibleDepositFacility {
using FullMath for uint256;
- struct CD {
- uint256 deposit;
- uint256 convertable;
- uint256 expiry;
- }
-
- // ========== EVENTS ========== //
-
- event CreatedCD(address user, uint48 expiry, uint256 deposit, uint256 convert);
- event ConvertedCD(address user, uint256 deposit, uint256 convert);
- event ReclaimedCD(address user, uint256 deposit);
- event SweptYield(address receiver, uint256 amount);
-
// ========== STATE VARIABLES ========== //
// Constants
@@ -39,17 +27,8 @@ contract CDFacility is Policy, RolesConsumer {
// Modules
TRSRYv1 public TRSRY;
MINTRv1 public MINTR;
-
- // Tokens
- ERC20 public reserve;
- ERC4626 public sReserve;
- IConvertibleDepositToken public cdUSDS;
-
- // State variables
- uint256 public totalDeposits;
- uint256 public totalShares;
- mapping(address => CD[]) public cdInfo;
- uint256 public redeemRate;
+ CDEPOv1 public CDEPO;
+ CTERMv1 public CTERM;
// ========== ERRORS ========== //
@@ -59,33 +38,21 @@ contract CDFacility is Policy, RolesConsumer {
// ========== SETUP ========== //
- constructor(
- Kernel kernel_,
- address reserve_,
- address sReserve_,
- address cdUSDS_
- ) Policy(kernel_) {
- if (reserve_ == address(0)) revert CDFacility_InvalidParams("Reserve address cannot be 0");
- if (sReserve_ == address(0))
- revert CDFacility_InvalidParams("sReserve address cannot be 0");
- if (cdUSDS_ == address(0)) revert CDFacility_InvalidParams("cdUSDS address cannot be 0");
-
- reserve = ERC20(reserve_);
- sReserve = ERC4626(sReserve_);
-
- // TODO shift to module and dependency injection
- cdUSDS = IConvertibleDepositToken(cdUSDS_);
- }
+ constructor(address kernel_) Policy(Kernel(kernel_)) {}
function configureDependencies() external override returns (Keycode[] memory dependencies) {
- dependencies = new Keycode[](3);
+ dependencies = new Keycode[](5);
dependencies[0] = toKeycode("TRSRY");
dependencies[1] = toKeycode("MINTR");
dependencies[2] = toKeycode("ROLES");
+ dependencies[3] = toKeycode("CDEPO");
+ dependencies[4] = toKeycode("CTERM");
TRSRY = TRSRYv1(getModuleAddress(dependencies[0]));
MINTR = MINTRv1(getModuleAddress(dependencies[1]));
ROLES = ROLESv1(getModuleAddress(dependencies[2]));
+ CDEPO = CDEPOv1(getModuleAddress(dependencies[3]));
+ CTERM = CTERMv1(getModuleAddress(dependencies[4]));
}
function requestPermissions()
@@ -95,207 +62,148 @@ contract CDFacility is Policy, RolesConsumer {
returns (Permissions[] memory permissions)
{
Keycode mintrKeycode = toKeycode("MINTR");
+ Keycode cdepoKeycode = toKeycode("CDEPO");
+ Keycode ctermKeycode = toKeycode("CTERM");
- permissions = new Permissions[](2);
+ permissions = new Permissions[](5);
permissions[0] = Permissions(mintrKeycode, MINTR.increaseMintApproval.selector);
permissions[1] = Permissions(mintrKeycode, MINTR.mintOhm.selector);
+ permissions[2] = Permissions(cdepoKeycode, CDEPO.sweepYield.selector);
+ permissions[3] = Permissions(ctermKeycode, CTERM.create.selector);
+ permissions[4] = Permissions(ctermKeycode, CTERM.update.selector);
}
- // ========== EMISSIONS MANAGER ========== //
-
- /// @notice allow emissions manager to create new convertible debt
- /// @param user owner of the convertible debt
- /// @param amount amount of reserve tokens deposited
- /// @param convertable amount of OHM that can be converted into
- /// @param expiry timestamp when conversion expires
- function addNewCD(
- address user,
- uint256 amount,
- uint256 convertable,
- uint256 expiry
- ) external onlyRole("CD_Auctioneer") {
- // transfer in debt token
- reserve.transferFrom(user, address(this), amount);
+ // ========== CONVERTIBLE DEPOSIT ACTIONS ========== //
- // deploy debt token into vault
- totalShares += sReserve.deposit(amount, address(this));
+ /// @inheritdoc IConvertibleDepositFacility
+ function create(
+ address account_,
+ uint256 amount_,
+ uint256 conversionPrice_,
+ uint48 expiry_,
+ bool wrap_
+ ) external onlyRole("CD_Auctioneer") returns (uint256 termId) {
+ // Mint the CD token to the account
+ // This will also transfer the reserve token
+ CDEPO.mintTo(account_, amount_);
- // add mint approval for conversion
- MINTR.increaseMintApproval(address(this), convertable);
+ // Create a new term record in the CTERM module
+ termId = CTERM.create(account_, amount_, conversionPrice_, expiry_, wrap_);
- // store convertable deposit info and mint cdUSDS
- cdInfo[user].push(CD(amount, convertable, expiry));
+ // Pre-emptively increase the OHM mint approval
+ MINTR.increaseMintApproval(address(this), amount_);
- // TODO shift mint/transfer functionality to CDEPO
-
- // TODO consider if the ERC20 should custody the deposit token
- cdUSDS.mint(user, amount);
+ // Emit an event
+ emit CreatedDeposit(account_, termId, amount_);
}
- // ========== CD Position Owner ========== //
-
- /// @notice allow user to convert their convertible debt before expiration
- /// @param cds CD indexes to convert
- /// @param amounts CD token amounts to convert
- function convertCD(
- uint256[] memory cds,
- uint256[] memory amounts
- ) external returns (uint256 converted) {
- if (cds.length != amounts.length) revert Misconfigured();
-
- uint256 totalDeposit;
-
- // iterate through and burn CD tokens, adding deposit and conversion amounts to running totals
- for (uint256 i; i < cds.length; ++i) {
- CD storage cd = cdInfo[msg.sender][i];
- if (cd.expiry < block.timestamp) continue;
-
- uint256 amount = amounts[i];
- uint256 converting = ((cd.convertable * amount) / cd.deposit);
-
- // increment running totals
- totalDeposit += amount;
- converted += converting;
-
- // decrement deposit info
- cd.convertable -= converting; // reverts on overflow
- cd.deposit -= amount;
- }
-
- // compute and account for shares to send to treasury
- uint256 shares = sReserve.previewWithdraw(totalDeposit);
- totalShares -= shares;
+ /// @inheritdoc IConvertibleDepositFacility
+ function convert(
+ uint256[] memory positionIds_,
+ uint256[] memory amounts_
+ ) external returns (uint256 totalDeposit, uint256 converted) {
+ // Make sure the lengths of the arrays are the same
+ if (positionIds_.length != amounts_.length)
+ revert CDF_InvalidArgs("array lengths must match");
- // burn cdUSDS
- cdUSDS.burn(msg.sender, totalDeposit);
+ uint256 totalDeposits;
- // mint ohm and send underlying debt token to treasury
- MINTR.mintOhm(msg.sender, converted);
- sReserve.transfer(address(TRSRY), shares);
+ // Iterate over all positions
+ for (uint256 i; i < positionIds_.length; ++i) {
+ uint256 positionId = positionIds_[i];
+ uint256 depositAmount = amounts_[i];
- emit ConvertedCD(msg.sender, totalDeposit, converted);
- }
+ // Validate that the caller is the owner of the position
+ if (CTERM.ownerOf(positionId) != msg.sender) revert CDF_NotOwner(positionId);
- /// @notice allow user to reclaim their convertible debt deposits after expiration
- /// @param cds CD indexes to return
- /// @param amounts amounts of CD tokens to burn
- /// @return returned total reserve tokens returned
- function returnDeposit(
- uint256[] memory cds,
- uint256[] memory amounts
- ) external returns (uint256 returned) {
- if (cds.length != amounts.length) revert Misconfigured();
+ // Validate that the position is valid
+ // This will revert if the position is not valid
+ CTERMv1.ConvertibleDepositTerm memory term = CTERM.getTerm(positionId);
- uint256 unconverted;
+ // Validate that the term has not expired
+ if (block.timestamp >= term.expiry) revert CDF_PositionExpired(positionId);
- // iterate through and burn CD tokens, adding deposit and conversion amounts to running totals
- for (uint256 i; i < cds.length; ++i) {
- CD memory cd = cdInfo[msg.sender][cds[i]];
- if (cd.expiry >= block.timestamp) continue;
+ // Validate that the deposit amount is not greater than the remaining deposit
+ if (depositAmount > term.remainingDeposit)
+ revert CDF_InvalidAmount(positionId, depositAmount);
- uint256 amount = amounts[i];
- uint256 convertable = ((cd.convertable * amount) / cd.deposit);
+ uint256 convertedAmount = (depositAmount * term.conversionPrice) / DECIMALS; // TODO check decimals, rounding
- returned += amount;
- unconverted += convertable;
+ // Increment running totals
+ totalDeposits += depositAmount;
+ converted += convertedAmount;
- // decrement deposit info
- cd.deposit -= amount;
- cd.convertable -= convertable; // reverts on overflow
+ // Update the position
+ CTERM.update(positionId, term.remainingDeposit - depositAmount);
}
- // TODO shift burn and transfer functionality to CDEPO
+ // Redeem the CD deposits in bulk
+ uint256 sharesOut = CDEPO.redeem(totalDeposits);
- // burn cdUSDS
- cdUSDS.burn(msg.sender, returned);
+ // Transfer the redeemed assets to the TRSRY
+ CDEPO.vault().transfer(address(TRSRY), sharesOut);
- // compute shares to redeem
- uint256 shares = sReserve.previewWithdraw(returned);
- totalShares -= shares;
+ // Mint OHM to the owner/caller
+ MINTR.mintOhm(msg.sender, converted);
- // return debt token to user
- sReserve.redeem(shares, msg.sender, address(this));
+ // Emit event
+ emit ConvertedDeposit(msg.sender, totalDeposits, converted);
- // decrease mint approval to reflect tokens that will not convert
- MINTR.decreaseMintApproval(address(this), unconverted);
-
- emit ReclaimedCD(msg.sender, returned);
+ return (totalDeposits, converted);
}
- // ========== cdUSDS ========== //
-
- // TODO shift these to CDEPO
+ /// @inheritdoc IConvertibleDepositFacility
+ function reclaim(
+ uint256[] memory positionIds_,
+ uint256[] memory amounts_
+ ) external override returns (uint256 reclaimed) {
+ // Make sure the lengths of the arrays are the same
+ if (positionIds_.length != amounts_.length)
+ revert CDF_InvalidArgs("array lengths must match");
- /// @notice allow user to mint cdUSDS
- /// @notice redeeming without a CD may be at a discount
- /// @param amount of reserve token
- /// @return tokensOut cdUSDS out (1:1 with USDS in)
- function mint(uint256 amount) external returns (uint256 tokensOut) {
- tokensOut = amount;
-
- reserve.transferFrom(msg.sender, address(this), amount);
- totalShares += sReserve.deposit(amount, msg.sender);
+ uint256 unconverted;
- cdUSDS.mint(msg.sender, amount);
- }
+ // Iterate over all positions
+ for (uint256 i; i < positionIds_.length; ++i) {
+ uint256 positionId = positionIds_[i];
+ uint256 depositAmount = amounts_[i];
- /// @notice allow non cd holder to sell cdUSDS for USDS
- /// @notice the amount of USDS per cdUSDS is not 1:1
- /// @notice convertible depositors should use returnDeposit() for 1:1
- function redeem(uint256 amount) external returns (uint256 tokensOut) {
- // burn cdUSDS
- cdUSDS.burn(msg.sender, amount);
+ // Validate that the caller is the owner of the position
+ if (CTERM.ownerOf(positionId) != msg.sender) revert CDF_NotOwner(positionId);
- // compute shares to redeem
- tokensOut = redeemOutput(amount);
- uint256 shares = sReserve.previewWithdraw(tokensOut);
- totalShares -= shares;
+ // Validate that the position is valid
+ // This will revert if the position is not valid
+ CTERMv1.ConvertibleDepositTerm memory term = CTERM.getTerm(positionId);
- // return debt token to user
- sReserve.redeem(shares, msg.sender, address(this));
- }
+ // Validate that the term has expired
+ if (block.timestamp < term.expiry) revert CDF_PositionNotExpired(positionId);
- // ========== YIELD MANAGER ========== //
+ // Validate that the deposit amount is not greater than the remaining deposit
+ if (depositAmount > term.remainingDeposit)
+ revert CDF_InvalidAmount(positionId, depositAmount);
- // TODO shift these to CDEPO
+ uint256 convertedAmount = (depositAmount * term.conversionPrice) / DECIMALS; // TODO check decimals, rounding
- /// @notice allow yield manager to sweep yield accrued on reserves
- /// @return yield yield in reserve token terms
- /// @return shares yield in sReserve token terms
- function sweepYield()
- external
- onlyRole("CD_Yield_Manager")
- returns (uint256 yield, uint256 shares)
- {
- yield = sReserve.previewRedeem(totalShares) - cdUSDS.totalSupply();
- shares = sReserve.previewWithdraw(yield);
- totalShares -= shares;
- sReserve.transfer(msg.sender, shares);
+ // Increment running totals
+ reclaimed += depositAmount;
+ unconverted += convertedAmount;
- emit SweptYield(msg.sender, yield);
- }
+ // Update the position
+ CTERM.update(positionId, term.remainingDeposit - depositAmount);
+ }
- // ========== GOVERNOR ========== //
+ // Redeem the CD deposits in bulk
+ uint256 sharesOut = CDEPO.redeem(unconverted);
- /// @notice allow admin to change redeem rate
- /// @dev redeem rate must be lower than or equal to 1:1
- function setRedeemRate(uint256 newRate) external onlyRole("CD_Admin") {
- if (newRate > DECIMALS) revert Misconfigured();
- redeemRate = newRate;
- }
+ // Transfer the underlying assets to the caller
+ CDEPO.vault().redeem(sharesOut, msg.sender, address(this));
- // ========== VIEW FUNCTIONS ========== //
+ // Decrease the mint approval
+ MINTR.decreaseMintApproval(address(this), unconverted);
- /// @notice get yield accrued on deposited reserve tokens
- /// @return yield in reserve token terms
- function yieldAccrued() external view returns (uint256) {
- return sReserve.previewRedeem(totalShares) - totalDeposits;
- }
+ // Emit event
+ emit ReclaimedDeposit(msg.sender, reclaimed);
- /// @notice amount of deposit tokens out for amount of cdUSDS redeemed
- /// @param amount of cdUSDS in
- /// @return output amount of USDS out
- function redeemOutput(uint256 amount) public view returns (uint256) {
- return (amount * redeemRate) / DECIMALS;
+ return reclaimed;
}
}
diff --git a/src/policies/interfaces/IConvertibleDepositFacility.sol b/src/policies/interfaces/IConvertibleDepositFacility.sol
index 0b1ed28a..bcce9003 100644
--- a/src/policies/interfaces/IConvertibleDepositFacility.sol
+++ b/src/policies/interfaces/IConvertibleDepositFacility.sol
@@ -7,137 +7,92 @@ import {IERC4626} from "openzeppelin-contracts/contracts/interfaces/IERC4626.sol
/// @title IConvertibleDepositFacility
/// @notice Interface for a contract that can perform functions related to convertible deposit tokens
interface IConvertibleDepositFacility {
+ // ========== EVENTS ========== //
+
+ event CreatedDeposit(address indexed user, uint256 indexed termId, uint256 amount);
+ event ConvertedDeposit(
+ address indexed user,
+ uint256 indexed termId,
+ uint256 depositAmount,
+ uint256 convertedAmount
+ );
+ event ReclaimedDeposit(
+ address indexed user,
+ uint256 indexed termId,
+ uint256 depositAmount,
+ uint256 reclaimedAmount
+ );
+
+ // ========== ERRORS ========== //
+
+ error CDF_InvalidArgs(string reason_);
+
+ error CDF_NotOwner(uint256 positionId_);
+
+ error CDF_PositionExpired(uint256 positionId_);
+
+ error CDF_PositionNotExpired(uint256 positionId_);
+
+ error CDF_InvalidAmount(uint256 positionId_, uint256 amount_);
+
// ========== CONVERTIBLE DEPOSIT ACTIONS ========== //
+ /// @notice Creates a new convertible deposit position
+ ///
+ /// @dev The implementing contract is expected to handle the following:
+ /// - Validating that the caller has the correct role
+ /// - Depositing the reserve token into the CDEPO module and minting the convertible deposit token
+ /// - Creating a new term record in the CTERM module
+ /// - Pre-emptively increasing the OHM mint approval
+ /// - Emitting an event
+ ///
+ /// @param account_ The address to create the position for
+ /// @param amount_ The amount of reserve token to deposit
+ /// @param conversionPrice_ The price of the reserve token in USD
+ /// @param expiry_ The timestamp when the position expires
+ /// @param wrap_ Whether the position should be wrapped
+ /// @return termId The ID of the new term
+ function create(
+ address account_,
+ uint256 amount_,
+ uint256 conversionPrice_,
+ uint48 expiry_,
+ bool wrap_
+ ) external returns (uint256 termId);
+
/// @notice Converts convertible deposit tokens to OHM before expiry
/// @dev The implementing contract is expected to handle the following:
- /// - Validating that `account_` has an active convertible deposit position
- /// - Validating that `account_` has the required amount of convertible deposit tokens
+ /// - Validating that the caller is the owner of all of the positions
+ /// - Validating that all of the positions are valid
/// - Validating that all of the positions have not expired
/// - Burning the convertible deposit tokens
/// - Minting OHM to `account_`
- /// - Transferring the reserve token to the treasury
+ /// - Transferring the sReserve token to the treasury
/// - Emitting an event
///
- /// @param account_ The address to convert for
- /// @param positionIds_ An array of position ids that will be converted
- /// @param amounts_ An array of amounts of convertible deposit tokens to convert
- /// @return converted The amount of OHM minted to `account_`
- function convertFor(
- address account_,
+ /// @param positionIds_ An array of position ids that will be converted
+ /// @param amounts_ An array of amounts of convertible deposit tokens to convert
+ /// @return totalDeposit The total amount of convertible deposit tokens converted
+ /// @return converted The amount of OHM minted during conversion
+ function convert(
uint256[] memory positionIds_,
uint256[] memory amounts_
- ) external returns (uint256 converted);
+ ) external returns (uint256 totalDeposit, uint256 converted);
/// @notice Reclaims convertible deposit tokens after expiry
/// @dev The implementing contract is expected to handle the following:
- /// - Validating that `account_` has an active convertible deposit position
- /// - Validating that `account_` has the required amount of convertible deposit tokens
+ /// - Validating that the caller is the owner of all of the positions
+ /// - Validating that all of the positions are valid
/// - Validating that all of the positions have expired
/// - Burning the convertible deposit tokens
/// - Transferring the reserve token to `account_`
/// - Emitting an event
///
- /// @param account_ The address to reclaim for
/// @param positionIds_ An array of position ids that will be reclaimed
/// @param amounts_ An array of amounts of convertible deposit tokens to reclaim
- /// @return reclaimed The amount of reserve token returned to `account_`
- function reclaimFor(
- address account_,
+ /// @return reclaimed The amount of reserve token returned to the caller
+ function reclaim(
uint256[] memory positionIds_,
uint256[] memory amounts_
) external returns (uint256 reclaimed);
-
- // TODO decide in the depositFor and withdrawTo functions should be in the ERC20 contract (aka ERC20Wrapper)
-
- /// @notice Deposits the reserve token in exchange for convertible deposit tokens
- /// @dev The implementing contract is expected to handle the following:
- /// - Validating that the caller has the required amount of reserve tokens
- /// - Minting the convertible deposit tokens to the caller
- /// - Emitting an event
- ///
- /// @param account_ The address to mint to
- /// @param amount_ The amount of reserve token to mint
- /// @return minted The amount of convertible deposit tokens minted to the caller
- function depositFor(address account_, uint256 amount_) external returns (uint256 minted);
-
- /// @notice Preview the amount of convertible deposit tokens that would be minted for a given amount of reserve token
- ///
- /// @param account_ The address to mint to
- /// @param amount_ The amount of reserve token to mint
- /// @return minted The amount of convertible deposit tokens that would be minted
- function previewDepositFor(
- address account_,
- uint256 amount_
- ) external view returns (uint256 minted);
-
- /// @notice Withdraws the reserve token in exchange for convertible deposit tokens
- /// @dev The implementing contract is expected to handle the following:
- /// - Validating that the caller has the required amount of convertible deposit tokens
- /// - Burning the convertible deposit tokens
- /// - Transferring the reserve token to the caller
- /// - Emitting an event
- ///
- /// @param account_ The address to withdraw to
- /// @param amount_ The amount of convertible deposit tokens to burn
- /// @return withdrawn The amount of reserve tokens returned to the caller
- function withdrawTo(address account_, uint256 amount_) external returns (uint256 withdrawn);
-
- /// @notice Preview the amount of reserve token that would be returned for a given amount of convertible deposit tokens
- ///
- /// @param account_ The address to withdraw to
- /// @param amount_ The amount of convertible deposit tokens to burn
- /// @return withdrawn The amount of reserve tokens that would be returned
- function previewWithdrawTo(
- address account_,
- uint256 amount_
- ) external view returns (uint256 withdrawn);
-
- // ========== YIELD MANAGEMENT ========== //
-
- /// @notice Claim the yield accrued on the reserve token
- /// @dev The implementing contract is expected to handle the following:
- /// - Validating that the caller has the correct role
- /// - Withdrawing the yield from the sReserve token
- /// - Transferring the yield to the caller
- /// - Emitting an event
- ///
- /// @return yieldReserve The amount of reserve token that was swept
- /// @return yieldSReserve The amount of sReserve token that was swept
- function sweepYield() external returns (uint256 yieldReserve, uint256 yieldSReserve);
-
- /// @notice Preview the amount of yield that would be swept
- ///
- /// @return yieldReserve The amount of reserve token that would be swept
- /// @return yieldSReserve The amount of sReserve token that would be swept
- function previewSweepYield()
- external
- view
- returns (uint256 yieldReserve, uint256 yieldSReserve);
-
- // ========== ADMIN ========== /
-
- /// @notice Set the withdraw rate when withdrawing the convertible deposit token, where withdraw rate = reserve token output / convertible deposit token input
- /// @dev The implementing contract is expected to handle the following:
- /// - Validating that the caller has the correct role
- /// - Validating that the new rate is within bounds
- /// - Setting the new redeem rate
- /// - Emitting an event
- ///
- /// @param newRate_ The new withdraw rate
- function setWithdrawRate(uint256 newRate_) external;
-
- // ========== STATE VARIABLES ========== //
-
- /// @notice The reserve token that is exchanged for the convertible deposit token
- function reserveToken() external view returns (IERC20);
-
- /// @notice The sReserve token that the reserve token is deposited into
- function sReserveToken() external view returns (IERC4626);
-
- /// @notice The convertible deposit token that is minted to the user
- function convertibleDepositToken() external view returns (IERC20);
-
- /// @notice The withdraw rate when withdrawing the convertible deposit token
- function withdrawRate() external view returns (uint256);
}
diff --git a/src/policies/interfaces/IConvertibleDepositToken.sol b/src/policies/interfaces/IConvertibleDepositToken.sol
deleted file mode 100644
index 93246071..00000000
--- a/src/policies/interfaces/IConvertibleDepositToken.sol
+++ /dev/null
@@ -1,18 +0,0 @@
-// SPDX-License-Identifier: AGPL-3.0
-pragma solidity ^0.8.0;
-
-import {IERC20} from "openzeppelin-contracts/contracts/interfaces/IERC20.sol";
-
-// TODO see if there is a standard interface for this
-
-interface IConvertibleDepositToken is IERC20 {
- function mint(address to, uint256 amount) external;
-
- function burn(address from, uint256 amount) external;
-
- function convertFor(uint256 amount) external view returns (uint256);
-
- function expiry() external view returns (uint256);
-
- function totalSupply() external view returns (uint256);
-}
From 42a6d68995a932120f11058089e912e25a83b0b8 Mon Sep 17 00:00:00 2001
From: Jem <0x0xjem@gmail.com>
Date: Mon, 16 Dec 2024 17:16:31 +0400
Subject: [PATCH 24/64] Rename CTERM to CDPOS
---
src/modules/CDPOS/CDPOS.v1.sol | 184 ++++++++++++
.../OlympusConvertibleDepositPositions.sol | 270 ++++++++++++++++++
src/modules/CTERM/CTERM.v1.sol | 184 ------------
.../CTERM/OlympusConvertibleDepositTerms.sol | 262 -----------------
src/policies/CDFacility.sol | 56 ++--
.../IConvertibleDepositFacility.sol | 14 +-
6 files changed, 485 insertions(+), 485 deletions(-)
create mode 100644 src/modules/CDPOS/CDPOS.v1.sol
create mode 100644 src/modules/CDPOS/OlympusConvertibleDepositPositions.sol
delete mode 100644 src/modules/CTERM/CTERM.v1.sol
delete mode 100644 src/modules/CTERM/OlympusConvertibleDepositTerms.sol
diff --git a/src/modules/CDPOS/CDPOS.v1.sol b/src/modules/CDPOS/CDPOS.v1.sol
new file mode 100644
index 00000000..25673e90
--- /dev/null
+++ b/src/modules/CDPOS/CDPOS.v1.sol
@@ -0,0 +1,184 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+pragma solidity 0.8.15;
+
+import {Module} from "src/Kernel.sol";
+import {ERC721} from "solmate/tokens/ERC721.sol";
+
+/// @title CDPOSv1
+/// @notice This defines the interface for the CDPOS module.
+/// The objective of this module is to track the terms of a convertible deposit.
+abstract contract CDPOSv1 is Module, ERC721 {
+ // ========== DATA STRUCTURES ========== //
+
+ /// @notice Data structure for the terms of a convertible deposit
+ ///
+ /// @param remainingDeposit Amount of reserve tokens remaining to be converted
+ /// @param conversionPrice Price of the reserve token in USD
+ /// @param expiry Timestamp when the term expires
+ /// @param wrapped Whether the term is wrapped
+ struct Position {
+ uint256 remainingDeposit;
+ uint256 conversionPrice;
+ uint48 expiry;
+ bool wrapped;
+ }
+
+ // ========== EVENTS ========== //
+
+ /// @notice Emitted when a position is created
+ event PositionCreated(
+ uint256 indexed positionId,
+ address indexed owner,
+ uint256 remainingDeposit,
+ uint256 conversionPrice,
+ uint48 expiry,
+ bool wrapped
+ );
+
+ /// @notice Emitted when a position is updated
+ event PositionUpdated(uint256 indexed positionId, uint256 remainingDeposit);
+
+ /// @notice Emitted when a position is split
+ event PositionSplit(
+ uint256 indexed positionId,
+ uint256 indexed newPositionId,
+ uint256 amount,
+ address to,
+ bool wrap
+ );
+
+ /// @notice Emitted when a position is wrapped
+ event PositionWrapped(uint256 indexed positionId);
+
+ /// @notice Emitted when a position is unwrapped
+ event PositionUnwrapped(uint256 indexed positionId);
+
+ // ========== STATE VARIABLES ========== //
+
+ /// @notice The number of positions created
+ uint256 public positionCount;
+
+ /// @notice Mapping of position records to an ID
+ /// @dev IDs are assigned sequentially starting from 0
+ /// Mapping entries should not be deleted, but can be overwritten
+ mapping(uint256 => Position) internal _positions;
+
+ /// @notice Mapping of user addresses to their position IDs
+ mapping(address => uint256[]) internal _userPositions;
+
+ // ========== ERRORS ========== //
+
+ /// @notice Error thrown when the caller is not the owner of the position
+ error CDPOS_NotOwner(uint256 positionId_);
+
+ /// @notice Error thrown when an invalid position ID is provided
+ error CDPOS_InvalidPositionId(uint256 id_);
+
+ /// @notice Error thrown when a position has already been wrapped
+ error CDPOS_AlreadyWrapped(uint256 positionId_);
+
+ /// @notice Error thrown when a position has not been wrapped
+ error CDPOS_NotWrapped(uint256 positionId_);
+
+ /// @notice Error thrown when an invalid parameter is provided
+ error CDPOS_InvalidParams(string reason_);
+
+ // ========== WRAPPING ========== //
+
+ /// @notice Wraps a position into an ERC721 token
+ /// This is useful if the position owner wants a tokenized representation of their position. It is functionally equivalent to the position itself.
+ ///
+ /// @dev The implementing function should do the following:
+ /// - Validate that the caller is the owner of the position
+ /// - Validate that the position is not already wrapped
+ /// - Mint an ERC721 token to the position owner
+ ///
+ /// @param positionId_ The ID of the position to wrap
+ function wrap(uint256 positionId_) external virtual;
+
+ /// @notice Unwraps/burns an ERC721 position token
+ /// This is useful if the position owner wants to convert their token back into the position.
+ ///
+ /// @dev The implementing function should do the following:
+ /// - Validate that the caller is the owner of the position
+ /// - Validate that the position is already wrapped
+ /// - Burn the ERC721 token
+ ///
+ /// @param positionId_ The ID of the position to unwrap
+ function unwrap(uint256 positionId_) external virtual;
+
+ // ========== POSITION MANAGEMENT =========== //
+
+ /// @notice Creates a new convertible deposit position
+ /// @dev The implementing function should do the following:
+ /// - Validate that the caller is permissioned
+ /// - Validate that the owner is not the zero address
+ /// - Validate that the remaining deposit is greater than 0
+ /// - Validate that the conversion price is greater than 0
+ /// - Validate that the expiry is in the future
+ /// - Create the position record
+ /// - Wrap the position if requested
+ ///
+ /// @param owner_ The address of the owner of the position
+ /// @param remainingDeposit_ The amount of reserve tokens remaining to be converted
+ /// @param conversionPrice_ The price of the reserve token in USD
+ /// @param expiry_ The timestamp when the position expires
+ /// @param wrap_ Whether the position should be wrapped
+ /// @return positionId The ID of the new position
+ function create(
+ address owner_,
+ uint256 remainingDeposit_,
+ uint256 conversionPrice_,
+ uint48 expiry_,
+ bool wrap_
+ ) external virtual returns (uint256 positionId);
+
+ /// @notice Updates the remaining deposit of a position
+ /// @dev The implementing function should do the following:
+ /// - Validate that the caller is permissioned
+ /// - Validate that the position ID is valid
+ /// - Update the remaining deposit of the position
+ ///
+ /// @param positionId_ The ID of the position to update
+ /// @param amount_ The new amount of the position
+ function update(uint256 positionId_, uint256 amount_) external virtual;
+
+ /// @notice Splits the specified amount of the position into a new position
+ /// This is useful if the position owner wants to split their position into multiple smaller positions.
+ /// @dev The implementing function should do the following:
+ /// - Validate that the caller is the owner of the position
+ /// - Validate that the amount is greater than 0
+ /// - Validate that the amount is less than or equal to the remaining deposit
+ /// - Validate that `to_` is not the zero address
+ /// - Update the remaining deposit of the original position
+ /// - Create the new position record
+ /// - Wrap the new position if requested
+ ///
+ /// @param positionId_ The ID of the position to split
+ /// @param amount_ The amount of the position to split
+ /// @param to_ The address to split the position to
+ /// @param wrap_ Whether the new position should be wrapped
+ /// @return newPositionId The ID of the new position
+ function split(
+ uint256 positionId_,
+ uint256 amount_,
+ address to_,
+ bool wrap_
+ ) external virtual returns (uint256 newPositionId);
+
+ // ========== POSITION INFORMATION ========== //
+
+ /// @notice Get the IDs of all positions for a given user
+ ///
+ /// @param user_ The address of the user
+ /// @return positionIds An array of position IDs
+ function getUserPositionIds(
+ address user_
+ ) external view virtual returns (uint256[] memory positionIds);
+
+ /// @notice Get the positions for a given ID
+ ///
+ /// @param positionId_ The ID of the position
+ /// @return position The positions for the given ID
+ function getPosition(uint256 positionId_) external view virtual returns (Position memory);
+}
diff --git a/src/modules/CDPOS/OlympusConvertibleDepositPositions.sol b/src/modules/CDPOS/OlympusConvertibleDepositPositions.sol
new file mode 100644
index 00000000..2939431e
--- /dev/null
+++ b/src/modules/CDPOS/OlympusConvertibleDepositPositions.sol
@@ -0,0 +1,270 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+pragma solidity 0.8.15;
+
+import {ERC721} from "solmate/tokens/ERC721.sol";
+import {CDPOSv1} from "./CDPOS.v1.sol";
+import {Kernel, Module} from "src/Kernel.sol";
+
+contract OlympusConvertibleDepositPositions is CDPOSv1 {
+ constructor(
+ address kernel_
+ ) Module(Kernel(kernel_)) ERC721("Olympus Convertible Deposit Positions", "OCDP") {}
+
+ // ========== WRAPPING ========== //
+
+ /// @inheritdoc CDPOSv1
+ /// @dev This function reverts if:
+ /// - The position ID is invalid
+ /// - The caller is not the owner of the position
+ /// - The position is already wrapped
+ function wrap(
+ uint256 positionId_
+ ) external virtual override onlyValidPosition(positionId_) onlyPositionOwner(positionId_) {
+ // Does not need to check for invalid position ID because the modifier already ensures that
+ Position storage position = _positions[positionId_];
+
+ // Validate that the position is not already wrapped
+ if (position.wrapped) revert CDPOS_AlreadyWrapped(positionId_);
+
+ // Mark the position as wrapped
+ position.wrapped = true;
+
+ // Mint the ERC721 token
+ _mint(msg.sender, positionId_);
+
+ emit PositionWrapped(positionId_);
+ }
+
+ /// @inheritdoc CDPOSv1
+ /// @dev This function reverts if:
+ /// - The position ID is invalid
+ /// - The caller is not the owner of the position
+ /// - The position is not wrapped
+ function unwrap(
+ uint256 positionId_
+ ) external virtual override onlyValidPosition(positionId_) onlyPositionOwner(positionId_) {
+ // Does not need to check for invalid position ID because the modifier already ensures that
+ Position storage position = _positions[positionId_];
+
+ // Validate that the position is wrapped
+ if (!position.wrapped) revert CDPOS_NotWrapped(positionId_);
+
+ // Mark the position as unwrapped
+ position.wrapped = false;
+
+ // Burn the ERC721 token
+ _burn(positionId_);
+
+ emit PositionUnwrapped(positionId_);
+ }
+
+ // ========== POSITION MANAGEMENT =========== //
+
+ function _create(
+ address owner_,
+ uint256 remainingDeposit_,
+ uint256 conversionPrice_,
+ uint48 expiry_,
+ bool wrap_
+ ) internal returns (uint256 positionId) {
+ // Create the position record
+ positionId = ++positionCount;
+ _positions[positionId] = Position({
+ remainingDeposit: remainingDeposit_,
+ conversionPrice: conversionPrice_,
+ expiry: expiry_,
+ wrapped: wrap_
+ });
+
+ // Update ERC721 storage
+ _ownerOf[positionId] = owner_;
+ _balanceOf[owner_]++;
+
+ // Add the position ID to the user's list of positions
+ _userPositions[owner_].push(positionId);
+
+ // If specified, wrap the position
+ if (wrap_) _mint(owner_, positionId);
+
+ // Emit the event
+ emit PositionCreated(
+ positionId,
+ owner_,
+ remainingDeposit_,
+ conversionPrice_,
+ expiry_,
+ wrap_
+ );
+
+ return positionId;
+ }
+
+ /// @inheritdoc CDPOSv1
+ /// @dev This function reverts if:
+ /// - The caller is not permissioned
+ /// - The owner is the zero address
+ /// - The remaining deposit is 0
+ /// - The conversion price is 0
+ /// - The expiry is in the past
+ function create(
+ address owner_,
+ uint256 remainingDeposit_,
+ uint256 conversionPrice_,
+ uint48 expiry_,
+ bool wrap_
+ ) external virtual override permissioned returns (uint256 positionId) {
+ // Validate that the owner is not the zero address
+ if (owner_ == address(0)) revert CDPOS_InvalidParams("owner");
+
+ // Validate that the remaining deposit is greater than 0
+ if (remainingDeposit_ == 0) revert CDPOS_InvalidParams("deposit");
+
+ // Validate that the conversion price is greater than 0
+ if (conversionPrice_ == 0) revert CDPOS_InvalidParams("conversion price");
+
+ // Validate that the expiry is in the future
+ if (expiry_ <= block.timestamp) revert CDPOS_InvalidParams("expiry");
+
+ return _create(owner_, remainingDeposit_, conversionPrice_, expiry_, wrap_);
+ }
+
+ /// @inheritdoc CDPOSv1
+ /// @dev This function reverts if:
+ /// - The caller is not permissioned
+ /// - The position ID is invalid
+ function update(
+ uint256 positionId_,
+ uint256 amount_
+ ) external virtual override permissioned onlyValidPosition(positionId_) {
+ // Update the remaining deposit of the position
+ Position storage position = _positions[positionId_];
+ position.remainingDeposit = amount_;
+
+ // Emit the event
+ emit PositionUpdated(positionId_, amount_);
+ }
+
+ /// @inheritdoc CDPOSv1
+ /// @dev This function reverts if:
+ /// - The caller is not the owner of the position
+ /// - The amount is 0
+ /// - The amount is greater than the remaining deposit
+ /// - `to_` is the zero address
+ function split(
+ uint256 positionId_,
+ uint256 amount_,
+ address to_,
+ bool wrap_
+ )
+ external
+ virtual
+ override
+ onlyValidPosition(positionId_)
+ onlyPositionOwner(positionId_)
+ returns (uint256 newPositionId)
+ {
+ Position storage position = _positions[positionId_];
+
+ // Validate that the amount is greater than 0
+ if (amount_ == 0) revert CDPOS_InvalidParams("amount");
+
+ // Validate that the amount is less than or equal to the remaining deposit
+ if (amount_ > position.remainingDeposit) revert CDPOS_InvalidParams("amount");
+
+ // Validate that the to address is not the zero address
+ if (to_ == address(0)) revert CDPOS_InvalidParams("to");
+
+ // Calculate the remaining deposit of the existing position
+ uint256 remainingDeposit = position.remainingDeposit - amount_;
+
+ // Update the remaining deposit of the existing position
+ position.remainingDeposit = remainingDeposit;
+
+ // Create the new position
+ newPositionId = _create(to_, amount_, position.conversionPrice, position.expiry, wrap_);
+
+ // Emit the event
+ emit PositionSplit(positionId_, newPositionId, amount_, to_, wrap_);
+
+ return newPositionId;
+ }
+
+ // ========== ERC721 OVERRIDES ========== //
+
+ /// @inheritdoc ERC721
+ function tokenURI(uint256 id_) public view virtual override returns (string memory) {
+ // TODO implement tokenURI SVG
+ return "";
+ }
+
+ /// @inheritdoc ERC721
+ /// @dev This function performs the following:
+ /// - Updates the owner of the position
+ /// - Calls `transferFrom` on the parent contract
+ function transferFrom(address from_, address to_, uint256 tokenId_) public override {
+ Position storage position = _positions[tokenId_];
+
+ // Validate that the position is valid
+ if (position.conversionPrice == 0) revert CDPOS_InvalidPositionId(tokenId_);
+
+ // Ownership is validated in `transferFrom` on the parent contract
+
+ // Add to user positions on the destination address
+ _userPositions[to_].push(tokenId_);
+
+ // Remove from user terms on the source address
+ bool found = false;
+ for (uint256 i = 0; i < _userPositions[from_].length; i++) {
+ if (_userPositions[from_][i] == tokenId_) {
+ _userPositions[from_][i] = _userPositions[from_][_userPositions[from_].length - 1];
+ _userPositions[from_].pop();
+ found = true;
+ break;
+ }
+ }
+ if (!found) revert CDPOS_InvalidPositionId(tokenId_);
+
+ // Call `transferFrom` on the parent contract
+ super.transferFrom(from_, to_, tokenId_);
+ }
+
+ // ========== TERM INFORMATION ========== //
+
+ function _getPosition(uint256 positionId_) internal view returns (Position memory) {
+ Position memory position = _positions[positionId_];
+ // `create()` blocks a 0 conversion price, so this should never happen on a valid position
+ if (position.conversionPrice == 0) revert CDPOS_InvalidPositionId(positionId_);
+
+ return position;
+ }
+
+ /// @inheritdoc CDPOSv1
+ function getUserPositionIds(
+ address user_
+ ) external view virtual override returns (uint256[] memory positionIds) {
+ return _userPositions[user_];
+ }
+
+ /// @inheritdoc CDPOSv1
+ /// @dev This function reverts if:
+ /// - The position ID is invalid
+ function getPosition(
+ uint256 positionId_
+ ) external view virtual override returns (Position memory) {
+ return _getPosition(positionId_);
+ }
+
+ // ========== MODIFIERS ========== //
+
+ modifier onlyValidPosition(uint256 positionId_) {
+ if (_getPosition(positionId_).conversionPrice == 0)
+ revert CDPOS_InvalidPositionId(positionId_);
+ _;
+ }
+
+ modifier onlyPositionOwner(uint256 positionId_) {
+ // This validates that the caller is the owner of the position
+ if (_ownerOf[positionId_] != msg.sender) revert CDPOS_NotOwner(positionId_);
+ _;
+ }
+}
diff --git a/src/modules/CTERM/CTERM.v1.sol b/src/modules/CTERM/CTERM.v1.sol
deleted file mode 100644
index 0924b7e1..00000000
--- a/src/modules/CTERM/CTERM.v1.sol
+++ /dev/null
@@ -1,184 +0,0 @@
-// SPDX-License-Identifier: AGPL-3.0-or-later
-pragma solidity 0.8.15;
-
-import {Module} from "src/Kernel.sol";
-import {ERC721} from "solmate/tokens/ERC721.sol";
-
-/// @title CTERMv1
-/// @notice This defines the interface for the CTERM module.
-/// The objective of this module is to track the terms of a convertible deposit.
-abstract contract CTERMv1 is Module, ERC721 {
- // TODO rename to CDPOS
-
- // ========== DATA STRUCTURES ========== //
-
- /// @notice Data structure for the terms of a convertible deposit
- ///
- /// @param remainingDeposit Amount of reserve tokens remaining to be converted
- /// @param conversionPrice Price of the reserve token in USD
- /// @param expiry Timestamp when the term expires
- /// @param wrapped Whether the term is wrapped
- struct ConvertibleDepositTerm {
- uint256 remainingDeposit;
- uint256 conversionPrice;
- uint48 expiry;
- bool wrapped;
- }
-
- // ========== EVENTS ========== //
-
- /// @notice Emitted when a term is created
- event TermCreated(
- uint256 indexed termId,
- address indexed owner,
- uint256 remainingDeposit,
- uint256 conversionPrice,
- uint48 expiry,
- bool wrapped
- );
-
- /// @notice Emitted when a term is updated
- event TermUpdated(uint256 indexed termId, uint256 remainingDeposit);
-
- /// @notice Emitted when a term is split
- event TermSplit(
- uint256 indexed termId,
- uint256 newTermId,
- uint256 amount,
- address to,
- bool wrap
- );
-
- /// @notice Emitted when a term is wrapped
- event TermWrapped(uint256 indexed termId);
-
- /// @notice Emitted when a term is unwrapped
- event TermUnwrapped(uint256 indexed termId);
-
- // ========== STATE VARIABLES ========== //
-
- /// @notice The number of terms created
- uint256 public termCount;
-
- /// @notice Mapping of term records to an ID
- /// @dev IDs are assigned sequentially starting from 0
- /// Mapping entries should not be deleted, but can be overwritten
- mapping(uint256 => ConvertibleDepositTerm) internal _terms;
-
- /// @notice Mapping of user addresses to their term IDs
- mapping(address => uint256[]) internal _userTerms;
-
- // ========== ERRORS ========== //
-
- /// @notice Error thrown when the caller is not the owner of the term
- error CTERM_NotOwner(uint256 termId_);
-
- /// @notice Error thrown when an invalid term ID is provided
- error CTERM_InvalidTermId(uint256 id_);
-
- /// @notice Error thrown when a term has already been wrapped
- error CTERM_AlreadyWrapped(uint256 termId_);
-
- /// @notice Error thrown when a term has not been wrapped
- error CTERM_NotWrapped(uint256 termId_);
-
- /// @notice Error thrown when an invalid parameter is provided
- error CTERM_InvalidParams(string reason_);
-
- // ========== WRAPPING ========== //
-
- /// @notice Wraps a term into an ERC721 token
- /// @dev This is useful if the term owner wants a tokenized representation of their term. It is functionally equivalent to the term itself.
- ///
- /// The implementing function should do the following:
- /// - Validate that the caller is the owner of the term
- /// - Validate that the term is not already wrapped
- /// - Mint an ERC721 token to the term owner
- ///
- /// @param termId_ The ID of the term to wrap
- function wrap(uint256 termId_) external virtual;
-
- /// @notice Unwraps an ERC721 token into a term
- /// @dev This is useful if the term owner wants to convert their token back into the term.
- ///
- /// The implementing function should do the following:
- /// - Validate that the caller is the owner of the term
- /// - Validate that the term is already wrapped
- /// - Burn the ERC721 token
- ///
- /// @param termId_ The ID of the term to unwrap
- function unwrap(uint256 termId_) external virtual;
-
- // ========== TERM MANAGEMENT =========== //
-
- /// @notice Creates a new convertible deposit term
- /// @dev The implementing function should do the following:
- /// - Validate that the caller is permissioned
- /// - Validate that the owner is not the zero address
- /// - Validate that the remaining deposit is greater than 0
- /// - Validate that the conversion price is greater than 0
- /// - Validate that the expiry is in the future
- /// - Create the term record
- /// - Wrap the term if requested
- ///
- /// @param owner_ The address of the owner of the term
- /// @param remainingDeposit_ The amount of reserve tokens remaining to be converted
- /// @param conversionPrice_ The price of the reserve token in USD
- /// @param expiry_ The timestamp when the term expires
- /// @param wrap_ Whether the term should be wrapped
- /// @return termId The ID of the new term
- function create(
- address owner_,
- uint256 remainingDeposit_,
- uint256 conversionPrice_,
- uint48 expiry_,
- bool wrap_
- ) external virtual returns (uint256 termId);
-
- /// @notice Updates the remaining deposit of a term
- /// @dev The implementing function should do the following:
- /// - Validate that the caller is permissioned
- /// - Validate that the term ID is valid
- /// - Update the remaining deposit of the term
- ///
- /// @param termId_ The ID of the term to update
- /// @param amount_ The new amount of the term
- function update(uint256 termId_, uint256 amount_) external virtual;
-
- /// @notice Splits the specified amount of the term into a new term
- /// This is useful if the term owner wants to split their term into multiple smaller terms.
- /// @dev The implementing function should do the following:
- /// - Validate that the caller is the owner of the term
- /// - Validate that the amount is greater than 0
- /// - Validate that the amount is less than or equal to the remaining deposit
- /// - Validate that `to_` is not the zero address
- /// - Update the remaining deposit of the original term
- /// - Create the new term record
- /// - Wrap the new term if requested
- ///
- /// @param termId_ The ID of the term to split
- /// @param amount_ The amount of the term to split
- /// @param to_ The address to split the term to
- /// @param wrap_ Whether the new term should be wrapped
- /// @return newTermId The ID of the new term
- function split(
- uint256 termId_,
- uint256 amount_,
- address to_,
- bool wrap_
- ) external virtual returns (uint256 newTermId);
-
- // ========== TERM INFORMATION ========== //
-
- /// @notice Get the IDs of all terms for a given user
- ///
- /// @param user_ The address of the user
- /// @return termIds An array of term IDs
- function getUserTermIds(address user_) external view virtual returns (uint256[] memory termIds);
-
- /// @notice Get the terms for a given ID
- ///
- /// @param termId_ The ID of the term
- /// @return term The terms for the given ID
- function getTerm(uint256 termId_) external view virtual returns (ConvertibleDepositTerm memory);
-}
diff --git a/src/modules/CTERM/OlympusConvertibleDepositTerms.sol b/src/modules/CTERM/OlympusConvertibleDepositTerms.sol
deleted file mode 100644
index b0241c0b..00000000
--- a/src/modules/CTERM/OlympusConvertibleDepositTerms.sol
+++ /dev/null
@@ -1,262 +0,0 @@
-// SPDX-License-Identifier: AGPL-3.0-only
-pragma solidity 0.8.15;
-
-import {ERC721} from "solmate/tokens/ERC721.sol";
-import {CTERMv1} from "./CTERM.v1.sol";
-import {Kernel, Module} from "src/Kernel.sol";
-
-contract OlympusConvertibleDepositTerms is CTERMv1 {
- constructor(
- address kernel_
- ) Module(Kernel(kernel_)) ERC721("Olympus Convertible Deposit Terms", "OCDT") {}
-
- // ========== WRAPPING ========== //
-
- /// @inheritdoc CTERMv1
- /// @dev This function reverts if:
- /// - The term ID is invalid
- /// - The caller is not the owner of the term
- /// - The term is already wrapped
- function wrap(
- uint256 termId_
- ) external virtual override onlyValidTerm(termId_) onlyTermOwner(termId_) {
- // Does not need to check for invalid term ID because the modifier already ensures that
- ConvertibleDepositTerm storage term = _terms[termId_];
-
- // Validate that the term is not already wrapped
- if (term.wrapped) revert CTERM_AlreadyWrapped(termId_);
-
- // Mark the term as wrapped
- term.wrapped = true;
-
- // Mint the ERC721 token
- _mint(msg.sender, termId_);
-
- emit TermWrapped(termId_);
- }
-
- /// @inheritdoc CTERMv1
- /// @dev This function reverts if:
- /// - The term ID is invalid
- /// - The caller is not the owner of the term
- /// - The term is not wrapped
- function unwrap(
- uint256 termId_
- ) external virtual override onlyValidTerm(termId_) onlyTermOwner(termId_) {
- // Does not need to check for invalid term ID because the modifier already ensures that
- ConvertibleDepositTerm storage term = _terms[termId_];
-
- // Validate that the term is wrapped
- if (!term.wrapped) revert CTERM_NotWrapped(termId_);
-
- // Mark the term as unwrapped
- term.wrapped = false;
-
- // Burn the ERC721 token
- _burn(termId_);
-
- emit TermUnwrapped(termId_);
- }
-
- // ========== TERM MANAGEMENT =========== //
-
- function _create(
- address owner_,
- uint256 remainingDeposit_,
- uint256 conversionPrice_,
- uint48 expiry_,
- bool wrap_
- ) internal returns (uint256 termId) {
- // Create the term record
- termId = ++termCount;
- _terms[termId] = ConvertibleDepositTerm({
- remainingDeposit: remainingDeposit_,
- conversionPrice: conversionPrice_,
- expiry: expiry_,
- wrapped: wrap_
- });
-
- // Update ERC721 storage
- _ownerOf[termId] = owner_;
- _balanceOf[owner_]++;
-
- // Add the term ID to the user's list of terms
- _userTerms[owner_].push(termId);
-
- // If specified, wrap the term
- if (wrap_) _mint(owner_, termId);
-
- // Emit the event
- emit TermCreated(termId, owner_, remainingDeposit_, conversionPrice_, expiry_, wrap_);
-
- return termId;
- }
-
- /// @inheritdoc CTERMv1
- /// @dev This function reverts if:
- /// - The caller is not permissioned
- /// - The owner is the zero address
- /// - The remaining deposit is 0
- /// - The conversion price is 0
- /// - The expiry is in the past
- function create(
- address owner_,
- uint256 remainingDeposit_,
- uint256 conversionPrice_,
- uint48 expiry_,
- bool wrap_
- ) external virtual override permissioned returns (uint256 termId) {
- // Validate that the owner is not the zero address
- if (owner_ == address(0)) revert CTERM_InvalidParams("owner");
-
- // Validate that the remaining deposit is greater than 0
- if (remainingDeposit_ == 0) revert CTERM_InvalidParams("deposit");
-
- // Validate that the conversion price is greater than 0
- if (conversionPrice_ == 0) revert CTERM_InvalidParams("conversion price");
-
- // Validate that the expiry is in the future
- if (expiry_ <= block.timestamp) revert CTERM_InvalidParams("expiry");
-
- return _create(owner_, remainingDeposit_, conversionPrice_, expiry_, wrap_);
- }
-
- /// @inheritdoc CTERMv1
- /// @dev This function reverts if:
- /// - The caller is not permissioned
- /// - The term ID is invalid
- function update(
- uint256 termId_,
- uint256 amount_
- ) external virtual override permissioned onlyValidTerm(termId_) {
- // Update the remaining deposit of the term
- ConvertibleDepositTerm storage term = _terms[termId_];
- term.remainingDeposit = amount_;
-
- // Emit the event
- emit TermUpdated(termId_, amount_);
- }
-
- /// @inheritdoc CTERMv1
- /// @dev This function reverts if:
- /// - The caller is not the owner of the term
- /// - The amount is 0
- /// - The amount is greater than the remaining deposit
- /// - `to_` is the zero address
- function split(
- uint256 termId_,
- uint256 amount_,
- address to_,
- bool wrap_
- )
- external
- virtual
- override
- onlyValidTerm(termId_)
- onlyTermOwner(termId_)
- returns (uint256 newTermId)
- {
- ConvertibleDepositTerm storage term = _terms[termId_];
-
- // Validate that the amount is greater than 0
- if (amount_ == 0) revert CTERM_InvalidParams("amount");
-
- // Validate that the amount is less than or equal to the remaining deposit
- if (amount_ > term.remainingDeposit) revert CTERM_InvalidParams("amount");
-
- // Validate that the to address is not the zero address
- if (to_ == address(0)) revert CTERM_InvalidParams("to");
-
- // Calculate the remaining deposit of the existing term
- uint256 remainingDeposit = term.remainingDeposit - amount_;
-
- // Update the remaining deposit of the existing term
- term.remainingDeposit = remainingDeposit;
-
- // Create the new term
- newTermId = _create(to_, amount_, term.conversionPrice, term.expiry, wrap_);
-
- // Emit the event
- emit TermSplit(termId_, newTermId, amount_, to_, wrap_);
-
- return newTermId;
- }
-
- // ========== ERC721 OVERRIDES ========== //
-
- /// @inheritdoc ERC721
- function tokenURI(uint256 id_) public view virtual override returns (string memory) {
- // TODO implement tokenURI SVG
- return "";
- }
-
- /// @inheritdoc ERC721
- /// @dev This function performs the following:
- /// - Updates the owner of the term
- /// - Calls `transferFrom` on the parent contract
- function transferFrom(address from_, address to_, uint256 tokenId_) public override {
- ConvertibleDepositTerm storage term = _terms[tokenId_];
-
- // Validate that the term is valid
- if (term.conversionPrice == 0) revert CTERM_InvalidTermId(tokenId_);
-
- // Ownership is validated in `transferFrom` on the parent contract
-
- // Add to user terms on the destination address
- _userTerms[to_].push(tokenId_);
-
- // Remove from user terms on the source address
- bool found = false;
- for (uint256 i = 0; i < _userTerms[from_].length; i++) {
- if (_userTerms[from_][i] == tokenId_) {
- _userTerms[from_][i] = _userTerms[from_][_userTerms[from_].length - 1];
- _userTerms[from_].pop();
- found = true;
- break;
- }
- }
- if (!found) revert CTERM_InvalidTermId(tokenId_);
-
- // Call `transferFrom` on the parent contract
- super.transferFrom(from_, to_, tokenId_);
- }
-
- // ========== TERM INFORMATION ========== //
-
- function _getTerm(uint256 termId_) internal view returns (ConvertibleDepositTerm memory) {
- ConvertibleDepositTerm memory term = _terms[termId_];
- // `create()` blocks a 0 conversion price, so this should never happen on a valid term
- if (term.conversionPrice == 0) revert CTERM_InvalidTermId(termId_);
-
- return term;
- }
-
- /// @inheritdoc CTERMv1
- function getUserTermIds(
- address user_
- ) external view virtual override returns (uint256[] memory termIds) {
- return _userTerms[user_];
- }
-
- /// @inheritdoc CTERMv1
- /// @dev This function reverts if:
- /// - The term ID is invalid
- function getTerm(
- uint256 termId_
- ) external view virtual override returns (ConvertibleDepositTerm memory) {
- return _getTerm(termId_);
- }
-
- // ========== MODIFIERS ========== //
-
- modifier onlyValidTerm(uint256 termId_) {
- if (_getTerm(termId_).conversionPrice == 0) revert CTERM_InvalidTermId(termId_);
- _;
- }
-
- modifier onlyTermOwner(uint256 termId_) {
- // This validates that the caller is the owner of the term
- if (_ownerOf[termId_] != msg.sender) revert CTERM_NotOwner(termId_);
- _;
- }
-}
diff --git a/src/policies/CDFacility.sol b/src/policies/CDFacility.sol
index d37ee3c5..ab36174a 100644
--- a/src/policies/CDFacility.sol
+++ b/src/policies/CDFacility.sol
@@ -12,7 +12,7 @@ import {RolesConsumer, ROLESv1} from "src/modules/ROLES/OlympusRoles.sol";
import {MINTRv1} from "src/modules/MINTR/MINTR.v1.sol";
import {TRSRYv1} from "src/modules/TRSRY/TRSRY.v1.sol";
import {CDEPOv1} from "src/modules/CDEPO/CDEPO.v1.sol";
-import {CTERMv1} from "src/modules/CTERM/CTERM.v1.sol";
+import {CDPOSv1} from "src/modules/CDPOS/CDPOS.v1.sol";
import {FullMath} from "src/libraries/FullMath.sol";
@@ -28,7 +28,7 @@ contract CDFacility is Policy, RolesConsumer, IConvertibleDepositFacility {
TRSRYv1 public TRSRY;
MINTRv1 public MINTR;
CDEPOv1 public CDEPO;
- CTERMv1 public CTERM;
+ CDPOSv1 public CDPOS;
// ========== ERRORS ========== //
@@ -46,13 +46,13 @@ contract CDFacility is Policy, RolesConsumer, IConvertibleDepositFacility {
dependencies[1] = toKeycode("MINTR");
dependencies[2] = toKeycode("ROLES");
dependencies[3] = toKeycode("CDEPO");
- dependencies[4] = toKeycode("CTERM");
+ dependencies[4] = toKeycode("CDPOS");
TRSRY = TRSRYv1(getModuleAddress(dependencies[0]));
MINTR = MINTRv1(getModuleAddress(dependencies[1]));
ROLES = ROLESv1(getModuleAddress(dependencies[2]));
CDEPO = CDEPOv1(getModuleAddress(dependencies[3]));
- CTERM = CTERMv1(getModuleAddress(dependencies[4]));
+ CDPOS = CDPOSv1(getModuleAddress(dependencies[4]));
}
function requestPermissions()
@@ -63,14 +63,16 @@ contract CDFacility is Policy, RolesConsumer, IConvertibleDepositFacility {
{
Keycode mintrKeycode = toKeycode("MINTR");
Keycode cdepoKeycode = toKeycode("CDEPO");
- Keycode ctermKeycode = toKeycode("CTERM");
+ Keycode cdposKeycode = toKeycode("CDPOS");
- permissions = new Permissions[](5);
+ permissions = new Permissions[](7);
permissions[0] = Permissions(mintrKeycode, MINTR.increaseMintApproval.selector);
permissions[1] = Permissions(mintrKeycode, MINTR.mintOhm.selector);
- permissions[2] = Permissions(cdepoKeycode, CDEPO.sweepYield.selector);
- permissions[3] = Permissions(ctermKeycode, CTERM.create.selector);
- permissions[4] = Permissions(ctermKeycode, CTERM.update.selector);
+ permissions[2] = Permissions(mintrKeycode, MINTR.decreaseMintApproval.selector);
+ permissions[3] = Permissions(cdepoKeycode, CDEPO.redeem.selector);
+ permissions[4] = Permissions(cdepoKeycode, CDEPO.sweepYield.selector);
+ permissions[5] = Permissions(cdposKeycode, CDPOS.create.selector);
+ permissions[6] = Permissions(cdposKeycode, CDPOS.update.selector);
}
// ========== CONVERTIBLE DEPOSIT ACTIONS ========== //
@@ -82,19 +84,19 @@ contract CDFacility is Policy, RolesConsumer, IConvertibleDepositFacility {
uint256 conversionPrice_,
uint48 expiry_,
bool wrap_
- ) external onlyRole("CD_Auctioneer") returns (uint256 termId) {
+ ) external onlyRole("CD_Auctioneer") returns (uint256 positionId) {
// Mint the CD token to the account
// This will also transfer the reserve token
CDEPO.mintTo(account_, amount_);
- // Create a new term record in the CTERM module
- termId = CTERM.create(account_, amount_, conversionPrice_, expiry_, wrap_);
+ // Create a new term record in the CDPOS module
+ positionId = CDPOS.create(account_, amount_, conversionPrice_, expiry_, wrap_);
// Pre-emptively increase the OHM mint approval
MINTR.increaseMintApproval(address(this), amount_);
// Emit an event
- emit CreatedDeposit(account_, termId, amount_);
+ emit CreatedDeposit(account_, positionId, amount_);
}
/// @inheritdoc IConvertibleDepositFacility
@@ -114,27 +116,27 @@ contract CDFacility is Policy, RolesConsumer, IConvertibleDepositFacility {
uint256 depositAmount = amounts_[i];
// Validate that the caller is the owner of the position
- if (CTERM.ownerOf(positionId) != msg.sender) revert CDF_NotOwner(positionId);
+ if (CDPOS.ownerOf(positionId) != msg.sender) revert CDF_NotOwner(positionId);
// Validate that the position is valid
// This will revert if the position is not valid
- CTERMv1.ConvertibleDepositTerm memory term = CTERM.getTerm(positionId);
+ CDPOSv1.Position memory position = CDPOS.getPosition(positionId);
- // Validate that the term has not expired
- if (block.timestamp >= term.expiry) revert CDF_PositionExpired(positionId);
+ // Validate that the position has not expired
+ if (block.timestamp >= position.expiry) revert CDF_PositionExpired(positionId);
// Validate that the deposit amount is not greater than the remaining deposit
- if (depositAmount > term.remainingDeposit)
+ if (depositAmount > position.remainingDeposit)
revert CDF_InvalidAmount(positionId, depositAmount);
- uint256 convertedAmount = (depositAmount * term.conversionPrice) / DECIMALS; // TODO check decimals, rounding
+ uint256 convertedAmount = (depositAmount * position.conversionPrice) / DECIMALS; // TODO check decimals, rounding
// Increment running totals
totalDeposits += depositAmount;
converted += convertedAmount;
// Update the position
- CTERM.update(positionId, term.remainingDeposit - depositAmount);
+ CDPOS.update(positionId, position.remainingDeposit - depositAmount);
}
// Redeem the CD deposits in bulk
@@ -169,27 +171,27 @@ contract CDFacility is Policy, RolesConsumer, IConvertibleDepositFacility {
uint256 depositAmount = amounts_[i];
// Validate that the caller is the owner of the position
- if (CTERM.ownerOf(positionId) != msg.sender) revert CDF_NotOwner(positionId);
+ if (CDPOS.ownerOf(positionId) != msg.sender) revert CDF_NotOwner(positionId);
// Validate that the position is valid
// This will revert if the position is not valid
- CTERMv1.ConvertibleDepositTerm memory term = CTERM.getTerm(positionId);
+ CDPOSv1.Position memory position = CDPOS.getPosition(positionId);
- // Validate that the term has expired
- if (block.timestamp < term.expiry) revert CDF_PositionNotExpired(positionId);
+ // Validate that the position has expired
+ if (block.timestamp < position.expiry) revert CDF_PositionNotExpired(positionId);
// Validate that the deposit amount is not greater than the remaining deposit
- if (depositAmount > term.remainingDeposit)
+ if (depositAmount > position.remainingDeposit)
revert CDF_InvalidAmount(positionId, depositAmount);
- uint256 convertedAmount = (depositAmount * term.conversionPrice) / DECIMALS; // TODO check decimals, rounding
+ uint256 convertedAmount = (depositAmount * position.conversionPrice) / DECIMALS; // TODO check decimals, rounding
// Increment running totals
reclaimed += depositAmount;
unconverted += convertedAmount;
// Update the position
- CTERM.update(positionId, term.remainingDeposit - depositAmount);
+ CDPOS.update(positionId, position.remainingDeposit - depositAmount);
}
// Redeem the CD deposits in bulk
diff --git a/src/policies/interfaces/IConvertibleDepositFacility.sol b/src/policies/interfaces/IConvertibleDepositFacility.sol
index bcce9003..a7bd9265 100644
--- a/src/policies/interfaces/IConvertibleDepositFacility.sol
+++ b/src/policies/interfaces/IConvertibleDepositFacility.sol
@@ -10,18 +10,8 @@ interface IConvertibleDepositFacility {
// ========== EVENTS ========== //
event CreatedDeposit(address indexed user, uint256 indexed termId, uint256 amount);
- event ConvertedDeposit(
- address indexed user,
- uint256 indexed termId,
- uint256 depositAmount,
- uint256 convertedAmount
- );
- event ReclaimedDeposit(
- address indexed user,
- uint256 indexed termId,
- uint256 depositAmount,
- uint256 reclaimedAmount
- );
+ event ConvertedDeposit(address indexed user, uint256 depositAmount, uint256 convertedAmount);
+ event ReclaimedDeposit(address indexed user, uint256 reclaimedAmount);
// ========== ERRORS ========== //
From be52b0965eb421feb5ffd59b3697641eaa880990 Mon Sep 17 00:00:00 2001
From: Jem <0x0xjem@gmail.com>
Date: Mon, 16 Dec 2024 17:25:37 +0400
Subject: [PATCH 25/64] Shift CDAuctioneer to use new CDFacility interface
---
src/policies/CDAuctioneer.sol | 24 +++++++++++++------
src/policies/CDFacility.sol | 4 ++--
.../IConvertibleDepositAuctioneer.sol | 6 ++---
src/scripts/deploy/DeployV2.sol | 7 +-----
.../MockConvertibleDepositAuctioneer.sol | 2 +-
5 files changed, 24 insertions(+), 19 deletions(-)
diff --git a/src/policies/CDAuctioneer.sol b/src/policies/CDAuctioneer.sol
index e766305d..e8f34b10 100644
--- a/src/policies/CDAuctioneer.sol
+++ b/src/policies/CDAuctioneer.sol
@@ -26,8 +26,6 @@ contract CDAuctioneer is IConvertibleDepositAuctioneer, Policy, RolesConsumer {
CDFacility public cdFacility;
- mapping(uint256 => mapping(uint256 => address)) public cdTokens; // mapping(expiry => price => token)
-
Day public today;
// ========== SETUP ========== //
@@ -77,11 +75,23 @@ contract CDAuctioneer is IConvertibleDepositAuctioneer, Policy, RolesConsumer {
convertible += _convertFor(amount, currentTick.price);
}
+ // TODO extract logic to previewBid
+ // TODO update currentTick based on previewBid output
+
today.deposits += deposit;
today.convertible += convertible;
- // mint amount of CD token
- cdFacility.addNewCD(msg.sender, deposit, convertible, block.timestamp + state.timeToExpiry);
+ // TODO calculate average price for total deposit and convertible, check rounding, formula
+ uint256 conversionPrice = (deposit * decimals) / convertible;
+
+ // Create the CD tokens and position
+ cdFacility.create(
+ msg.sender,
+ deposit,
+ conversionPrice,
+ uint48(block.timestamp + state.timeToExpiry),
+ false
+ );
}
/// @inheritdoc IConvertibleDepositAuctioneer
@@ -147,13 +157,13 @@ contract CDAuctioneer is IConvertibleDepositAuctioneer, Policy, RolesConsumer {
newSize,
newMinPrice,
state.tickStep,
- state.timeToExpiry,
- state.lastUpdate
+ state.lastUpdate,
+ state.timeToExpiry
);
}
/// @inheritdoc IConvertibleDepositAuctioneer
- function setTimeToExpiry(uint256 newTime) external override onlyRole("CD_Admin") {
+ function setTimeToExpiry(uint48 newTime) external override onlyRole("CD_Admin") {
state.timeToExpiry = newTime;
}
diff --git a/src/policies/CDFacility.sol b/src/policies/CDFacility.sol
index ab36174a..f7b3da2e 100644
--- a/src/policies/CDFacility.sol
+++ b/src/policies/CDFacility.sol
@@ -129,7 +129,7 @@ contract CDFacility is Policy, RolesConsumer, IConvertibleDepositFacility {
if (depositAmount > position.remainingDeposit)
revert CDF_InvalidAmount(positionId, depositAmount);
- uint256 convertedAmount = (depositAmount * position.conversionPrice) / DECIMALS; // TODO check decimals, rounding
+ uint256 convertedAmount = (depositAmount * DECIMALS) / position.conversionPrice; // TODO check decimals, rounding
// Increment running totals
totalDeposits += depositAmount;
@@ -184,7 +184,7 @@ contract CDFacility is Policy, RolesConsumer, IConvertibleDepositFacility {
if (depositAmount > position.remainingDeposit)
revert CDF_InvalidAmount(positionId, depositAmount);
- uint256 convertedAmount = (depositAmount * position.conversionPrice) / DECIMALS; // TODO check decimals, rounding
+ uint256 convertedAmount = (depositAmount * DECIMALS) / position.conversionPrice; // TODO check decimals, rounding
// Increment running totals
reclaimed += depositAmount;
diff --git a/src/policies/interfaces/IConvertibleDepositAuctioneer.sol b/src/policies/interfaces/IConvertibleDepositAuctioneer.sol
index e42109f6..cbf8d67c 100644
--- a/src/policies/interfaces/IConvertibleDepositAuctioneer.sol
+++ b/src/policies/interfaces/IConvertibleDepositAuctioneer.sol
@@ -22,15 +22,15 @@ interface IConvertibleDepositAuctioneer {
/// @param tickSize number of ohm in a tick
/// @param minPrice minimum tick price
/// @param tickStep percentage increase (decrease) per tick
- /// @param timeToExpiry time between creation and expiry of deposit
/// @param lastUpdate timestamp of last update to current tick
+ /// @param timeToExpiry time between creation and expiry of deposit
struct State {
uint256 target;
uint256 tickSize;
uint256 minPrice;
uint256 tickStep;
- uint256 timeToExpiry;
uint256 lastUpdate;
+ uint48 timeToExpiry;
}
/// @notice Tracks auction activity for a given day
@@ -101,7 +101,7 @@ interface IConvertibleDepositAuctioneer {
/// @dev only callable by the admin
///
/// @param newTime_ new time to expiry
- function setTimeToExpiry(uint256 newTime_) external;
+ function setTimeToExpiry(uint48 newTime_) external;
/// @notice Set the tick step
/// @dev only callable by the admin
diff --git a/src/scripts/deploy/DeployV2.sol b/src/scripts/deploy/DeployV2.sol
index c5ccef30..de288622 100644
--- a/src/scripts/deploy/DeployV2.sol
+++ b/src/scripts/deploy/DeployV2.sol
@@ -1257,15 +1257,10 @@ contract OlympusDeploy is Script {
// Log dependencies
console2.log("ConvertibleDepositFacility parameters:");
console2.log(" kernel", address(kernel));
- console2.log(" reserve", address(reserve));
- console2.log(" sReserve", address(sReserve));
-
- // TODO add deployment of cdUSDS
- address cdUSDS = address(0);
// Deploy ConvertibleDepositFacility
vm.broadcast();
- cdFacility = new CDFacility(kernel, address(reserve), address(sReserve), cdUSDS);
+ cdFacility = new CDFacility(address(kernel));
console2.log("ConvertibleDepositFacility deployed at:", address(cdFacility));
return address(cdFacility);
diff --git a/src/test/mocks/MockConvertibleDepositAuctioneer.sol b/src/test/mocks/MockConvertibleDepositAuctioneer.sol
index 1f965632..67f69024 100644
--- a/src/test/mocks/MockConvertibleDepositAuctioneer.sol
+++ b/src/test/mocks/MockConvertibleDepositAuctioneer.sol
@@ -40,7 +40,7 @@ contract MockConvertibleDepositAuctioneer is IConvertibleDepositAuctioneer, Poli
uint256 newMinPrice
) external override returns (uint256 remainder) {}
- function setTimeToExpiry(uint256 newTime) external override {}
+ function setTimeToExpiry(uint48 newTime) external override {}
function setTickStep(uint256 newStep) external override {}
}
From 2c28d33e05a934b6752bd1ecb474458901eb300a Mon Sep 17 00:00:00 2001
From: Jem <0x0xjem@gmail.com>
Date: Tue, 17 Dec 2024 16:57:55 +0500
Subject: [PATCH 26/64] Check CD token before converting/reclaiming using a CD
position
---
src/libraries/Timestamp.sol | 45 +++++++++++++++++++
src/libraries/Uint2Str.sol | 27 +++++++++++
src/modules/CDPOS/CDPOS.v1.sol | 27 ++++++-----
.../OlympusConvertibleDepositPositions.sol | 37 +++++++++++++--
src/policies/CDFacility.sol | 17 ++++++-
.../IConvertibleDepositFacility.sol | 4 ++
6 files changed, 143 insertions(+), 14 deletions(-)
create mode 100644 src/libraries/Timestamp.sol
create mode 100644 src/libraries/Uint2Str.sol
diff --git a/src/libraries/Timestamp.sol b/src/libraries/Timestamp.sol
new file mode 100644
index 00000000..623c334a
--- /dev/null
+++ b/src/libraries/Timestamp.sol
@@ -0,0 +1,45 @@
+// SPDX-License-Identifier: MIT
+pragma solidity 0.8.15;
+
+import {uint2str} from "./Uint2Str.sol";
+
+library Timestamp {
+ function toPaddedString(
+ uint48 timestamp
+ ) internal pure returns (string memory, string memory, string memory) {
+ // Convert a number of days into a human-readable date, courtesy of BokkyPooBah.
+ // Source: https://github.com/bokkypoobah/BokkyPooBahsDateTimeLibrary/blob/master/contracts/BokkyPooBahsDateTimeLibrary.sol
+
+ uint256 year;
+ uint256 month;
+ uint256 day;
+ {
+ int256 __days = int256(int48(timestamp) / 1 days);
+
+ int256 num1 = __days + 68_569 + 2_440_588; // 2440588 = OFFSET19700101
+ int256 num2 = (4 * num1) / 146_097;
+ num1 = num1 - (146_097 * num2 + 3) / 4;
+ int256 _year = (4000 * (num1 + 1)) / 1_461_001;
+ num1 = num1 - (1461 * _year) / 4 + 31;
+ int256 _month = (80 * num1) / 2447;
+ int256 _day = num1 - (2447 * _month) / 80;
+ num1 = _month / 11;
+ _month = _month + 2 - 12 * num1;
+ _year = 100 * (num2 - 49) + _year + num1;
+
+ year = uint256(_year);
+ month = uint256(_month);
+ day = uint256(_day);
+ }
+
+ string memory yearStr = uint2str(year % 10_000);
+ string memory monthStr = month < 10
+ ? string(abi.encodePacked("0", uint2str(month)))
+ : uint2str(month);
+ string memory dayStr = day < 10
+ ? string(abi.encodePacked("0", uint2str(day)))
+ : uint2str(day);
+
+ return (yearStr, monthStr, dayStr);
+ }
+}
diff --git a/src/libraries/Uint2Str.sol b/src/libraries/Uint2Str.sol
new file mode 100644
index 00000000..a28d399d
--- /dev/null
+++ b/src/libraries/Uint2Str.sol
@@ -0,0 +1,27 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8;
+
+// Some fancy math to convert a uint into a string, courtesy of Provable Things.
+// Updated to work with solc 0.8.0.
+// https://github.com/provable-things/ethereum-api/blob/master/provableAPI_0.6.sol
+function uint2str(uint256 _i) pure returns (string memory) {
+ if (_i == 0) {
+ return "0";
+ }
+ uint256 j = _i;
+ uint256 len;
+ while (j != 0) {
+ len++;
+ j /= 10;
+ }
+ bytes memory bstr = new bytes(len);
+ uint256 k = len;
+ while (_i != 0) {
+ k = k - 1;
+ uint8 temp = (48 + uint8(_i - (_i / 10) * 10));
+ bytes1 b1 = bytes1(temp);
+ bstr[k] = b1;
+ _i /= 10;
+ }
+ return string(bstr);
+}
diff --git a/src/modules/CDPOS/CDPOS.v1.sol b/src/modules/CDPOS/CDPOS.v1.sol
index 25673e90..3fa1ccdd 100644
--- a/src/modules/CDPOS/CDPOS.v1.sol
+++ b/src/modules/CDPOS/CDPOS.v1.sol
@@ -12,11 +12,13 @@ abstract contract CDPOSv1 is Module, ERC721 {
/// @notice Data structure for the terms of a convertible deposit
///
- /// @param remainingDeposit Amount of reserve tokens remaining to be converted
- /// @param conversionPrice Price of the reserve token in USD
- /// @param expiry Timestamp when the term expires
- /// @param wrapped Whether the term is wrapped
+ /// @param convertibleDepositToken Address of the convertible deposit token
+ /// @param remainingDeposit Amount of reserve tokens remaining to be converted
+ /// @param conversionPrice Price of the reserve token in USD
+ /// @param expiry Timestamp when the term expires
+ /// @param wrapped Whether the term is wrapped
struct Position {
+ address convertibleDepositToken;
uint256 remainingDeposit;
uint256 conversionPrice;
uint48 expiry;
@@ -29,6 +31,7 @@ abstract contract CDPOSv1 is Module, ERC721 {
event PositionCreated(
uint256 indexed positionId,
address indexed owner,
+ address indexed convertibleDepositToken,
uint256 remainingDeposit,
uint256 conversionPrice,
uint48 expiry,
@@ -42,6 +45,7 @@ abstract contract CDPOSv1 is Module, ERC721 {
event PositionSplit(
uint256 indexed positionId,
uint256 indexed newPositionId,
+ address indexed convertibleDepositToken,
uint256 amount,
address to,
bool wrap
@@ -113,20 +117,23 @@ abstract contract CDPOSv1 is Module, ERC721 {
/// @dev The implementing function should do the following:
/// - Validate that the caller is permissioned
/// - Validate that the owner is not the zero address
+ /// - Validate that the convertible deposit token is not the zero address
/// - Validate that the remaining deposit is greater than 0
/// - Validate that the conversion price is greater than 0
/// - Validate that the expiry is in the future
/// - Create the position record
/// - Wrap the position if requested
///
- /// @param owner_ The address of the owner of the position
- /// @param remainingDeposit_ The amount of reserve tokens remaining to be converted
- /// @param conversionPrice_ The price of the reserve token in USD
- /// @param expiry_ The timestamp when the position expires
- /// @param wrap_ Whether the position should be wrapped
- /// @return positionId The ID of the new position
+ /// @param owner_ The address of the owner of the position
+ /// @param convertibleDepositToken_ The address of the convertible deposit token
+ /// @param remainingDeposit_ The amount of reserve tokens remaining to be converted
+ /// @param conversionPrice_ The price of the reserve token in USD
+ /// @param expiry_ The timestamp when the position expires
+ /// @param wrap_ Whether the position should be wrapped
+ /// @return positionId The ID of the new position
function create(
address owner_,
+ address convertibleDepositToken_,
uint256 remainingDeposit_,
uint256 conversionPrice_,
uint48 expiry_,
diff --git a/src/modules/CDPOS/OlympusConvertibleDepositPositions.sol b/src/modules/CDPOS/OlympusConvertibleDepositPositions.sol
index 2939431e..be84ad3e 100644
--- a/src/modules/CDPOS/OlympusConvertibleDepositPositions.sol
+++ b/src/modules/CDPOS/OlympusConvertibleDepositPositions.sol
@@ -62,6 +62,7 @@ contract OlympusConvertibleDepositPositions is CDPOSv1 {
function _create(
address owner_,
+ address convertibleDepositToken_,
uint256 remainingDeposit_,
uint256 conversionPrice_,
uint48 expiry_,
@@ -70,6 +71,7 @@ contract OlympusConvertibleDepositPositions is CDPOSv1 {
// Create the position record
positionId = ++positionCount;
_positions[positionId] = Position({
+ convertibleDepositToken: convertibleDepositToken_,
remainingDeposit: remainingDeposit_,
conversionPrice: conversionPrice_,
expiry: expiry_,
@@ -90,6 +92,7 @@ contract OlympusConvertibleDepositPositions is CDPOSv1 {
emit PositionCreated(
positionId,
owner_,
+ convertibleDepositToken_,
remainingDeposit_,
conversionPrice_,
expiry_,
@@ -103,11 +106,13 @@ contract OlympusConvertibleDepositPositions is CDPOSv1 {
/// @dev This function reverts if:
/// - The caller is not permissioned
/// - The owner is the zero address
+ /// - The convertible deposit token is the zero address
/// - The remaining deposit is 0
/// - The conversion price is 0
/// - The expiry is in the past
function create(
address owner_,
+ address convertibleDepositToken_,
uint256 remainingDeposit_,
uint256 conversionPrice_,
uint48 expiry_,
@@ -116,6 +121,10 @@ contract OlympusConvertibleDepositPositions is CDPOSv1 {
// Validate that the owner is not the zero address
if (owner_ == address(0)) revert CDPOS_InvalidParams("owner");
+ // Validate that the convertible deposit token is not the zero address
+ if (convertibleDepositToken_ == address(0))
+ revert CDPOS_InvalidParams("convertible deposit token");
+
// Validate that the remaining deposit is greater than 0
if (remainingDeposit_ == 0) revert CDPOS_InvalidParams("deposit");
@@ -125,7 +134,15 @@ contract OlympusConvertibleDepositPositions is CDPOSv1 {
// Validate that the expiry is in the future
if (expiry_ <= block.timestamp) revert CDPOS_InvalidParams("expiry");
- return _create(owner_, remainingDeposit_, conversionPrice_, expiry_, wrap_);
+ return
+ _create(
+ owner_,
+ convertibleDepositToken_,
+ remainingDeposit_,
+ conversionPrice_,
+ expiry_,
+ wrap_
+ );
}
/// @inheritdoc CDPOSv1
@@ -181,10 +198,24 @@ contract OlympusConvertibleDepositPositions is CDPOSv1 {
position.remainingDeposit = remainingDeposit;
// Create the new position
- newPositionId = _create(to_, amount_, position.conversionPrice, position.expiry, wrap_);
+ newPositionId = _create(
+ to_,
+ position.convertibleDepositToken,
+ amount_,
+ position.conversionPrice,
+ position.expiry,
+ wrap_
+ );
// Emit the event
- emit PositionSplit(positionId_, newPositionId, amount_, to_, wrap_);
+ emit PositionSplit(
+ positionId_,
+ newPositionId,
+ position.convertibleDepositToken,
+ amount_,
+ to_,
+ wrap_
+ );
return newPositionId;
}
diff --git a/src/policies/CDFacility.sol b/src/policies/CDFacility.sol
index f7b3da2e..8a525295 100644
--- a/src/policies/CDFacility.sol
+++ b/src/policies/CDFacility.sol
@@ -90,7 +90,14 @@ contract CDFacility is Policy, RolesConsumer, IConvertibleDepositFacility {
CDEPO.mintTo(account_, amount_);
// Create a new term record in the CDPOS module
- positionId = CDPOS.create(account_, amount_, conversionPrice_, expiry_, wrap_);
+ positionId = CDPOS.create(
+ account_,
+ address(CDEPO),
+ amount_,
+ conversionPrice_,
+ expiry_,
+ wrap_
+ );
// Pre-emptively increase the OHM mint approval
MINTR.increaseMintApproval(address(this), amount_);
@@ -122,6 +129,10 @@ contract CDFacility is Policy, RolesConsumer, IConvertibleDepositFacility {
// This will revert if the position is not valid
CDPOSv1.Position memory position = CDPOS.getPosition(positionId);
+ // Validate that the position is CDEPO
+ if (position.convertibleDepositToken != address(CDEPO))
+ revert CDF_InvalidToken(positionId, position.convertibleDepositToken);
+
// Validate that the position has not expired
if (block.timestamp >= position.expiry) revert CDF_PositionExpired(positionId);
@@ -177,6 +188,10 @@ contract CDFacility is Policy, RolesConsumer, IConvertibleDepositFacility {
// This will revert if the position is not valid
CDPOSv1.Position memory position = CDPOS.getPosition(positionId);
+ // Validate that the position is CDEPO
+ if (position.convertibleDepositToken != address(CDEPO))
+ revert CDF_InvalidToken(positionId, position.convertibleDepositToken);
+
// Validate that the position has expired
if (block.timestamp < position.expiry) revert CDF_PositionNotExpired(positionId);
diff --git a/src/policies/interfaces/IConvertibleDepositFacility.sol b/src/policies/interfaces/IConvertibleDepositFacility.sol
index a7bd9265..01448bf2 100644
--- a/src/policies/interfaces/IConvertibleDepositFacility.sol
+++ b/src/policies/interfaces/IConvertibleDepositFacility.sol
@@ -25,6 +25,8 @@ interface IConvertibleDepositFacility {
error CDF_InvalidAmount(uint256 positionId_, uint256 amount_);
+ error CDF_InvalidToken(uint256 positionId_, address token_);
+
// ========== CONVERTIBLE DEPOSIT ACTIONS ========== //
/// @notice Creates a new convertible deposit position
@@ -53,6 +55,7 @@ interface IConvertibleDepositFacility {
/// @notice Converts convertible deposit tokens to OHM before expiry
/// @dev The implementing contract is expected to handle the following:
/// - Validating that the caller is the owner of all of the positions
+ /// - Validating that convertible deposit token in the position is CDEPO
/// - Validating that all of the positions are valid
/// - Validating that all of the positions have not expired
/// - Burning the convertible deposit tokens
@@ -72,6 +75,7 @@ interface IConvertibleDepositFacility {
/// @notice Reclaims convertible deposit tokens after expiry
/// @dev The implementing contract is expected to handle the following:
/// - Validating that the caller is the owner of all of the positions
+ /// - Validating that convertible deposit token in the position is CDEPO
/// - Validating that all of the positions are valid
/// - Validating that all of the positions have expired
/// - Burning the convertible deposit tokens
From f1dbb752786f2b344d63f82933bb4fa4afd0b893 Mon Sep 17 00:00:00 2001
From: Jem <0x0xjem@gmail.com>
Date: Tue, 17 Dec 2024 16:58:08 +0500
Subject: [PATCH 27/64] First pass at SVG for CD positions
---
.../OlympusConvertibleDepositPositions.sol | 80 ++++++++++++++++++-
1 file changed, 77 insertions(+), 3 deletions(-)
diff --git a/src/modules/CDPOS/OlympusConvertibleDepositPositions.sol b/src/modules/CDPOS/OlympusConvertibleDepositPositions.sol
index be84ad3e..01948eb6 100644
--- a/src/modules/CDPOS/OlympusConvertibleDepositPositions.sol
+++ b/src/modules/CDPOS/OlympusConvertibleDepositPositions.sol
@@ -4,11 +4,14 @@ pragma solidity 0.8.15;
import {ERC721} from "solmate/tokens/ERC721.sol";
import {CDPOSv1} from "./CDPOS.v1.sol";
import {Kernel, Module} from "src/Kernel.sol";
+import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
+import {Base64} from "@openzeppelin/contracts/utils/Base64.sol";
+import {Timestamp} from "src/libraries/Timestamp.sol";
contract OlympusConvertibleDepositPositions is CDPOSv1 {
constructor(
address kernel_
- ) Module(Kernel(kernel_)) ERC721("Olympus Convertible Deposit Positions", "OCDP") {}
+ ) Module(Kernel(kernel_)) ERC721("Olympus Convertible Deposit Position", "OCDP") {}
// ========== WRAPPING ========== //
@@ -222,12 +225,83 @@ contract OlympusConvertibleDepositPositions is CDPOSv1 {
// ========== ERC721 OVERRIDES ========== //
+ function _getTimeString(uint48 time_) internal pure returns (string memory) {
+ (string memory year, string memory month, string memory day) = Timestamp.toPaddedString(
+ time_
+ );
+
+ return string.concat(year, "-", month, "-", day);
+ }
+
+ // solhint-disable quotes
+ function _render(
+ uint256 positionId_,
+ Position memory position_
+ ) internal pure returns (string memory) {
+ // TODO consider adding conversion price and remaining deposit to the SVG. How to display as decimal values?
+ return
+ string.concat(
+ '"
+ );
+ }
+
+ // solhint-enable quotes
+
/// @inheritdoc ERC721
+ // solhint-disable quotes
function tokenURI(uint256 id_) public view virtual override returns (string memory) {
- // TODO implement tokenURI SVG
- return "";
+ Position memory position = _getPosition(id_);
+
+ // solhint-disable-next-line quotes
+ string memory jsonContent = string.concat(
+ "{",
+ string.concat('"name": "', name, '",'),
+ string.concat('"symbol": "', symbol, '",'),
+ '"attributes": [',
+ string.concat('{"trait_type": "Position ID", "value": "', Strings.toString(id_), '"},'),
+ string.concat(
+ '{"trait_type": "Convertible Deposit Token", "value": "',
+ Strings.toHexString(position.convertibleDepositToken),
+ '"},'
+ ),
+ string.concat(
+ '{"trait_type": "Expiry", "display_type": "date", "value": "',
+ Strings.toString(position.expiry),
+ '"},'
+ ),
+ "]",
+ string.concat(
+ '"image": "',
+ "data:image/svg+xml;base64,",
+ Base64.encode(bytes(_render(id_, position))),
+ '"}'
+ ),
+ "}"
+ );
+
+ return string.concat("data:application/json;base64,", Base64.encode(bytes(jsonContent)));
}
+ // solhint-enable quotes
+
/// @inheritdoc ERC721
/// @dev This function performs the following:
/// - Updates the owner of the position
From 6b9888fd6416fd5357593c60523553d427b4f67a Mon Sep 17 00:00:00 2001
From: Jem <0x0xjem@gmail.com>
Date: Tue, 17 Dec 2024 17:08:58 +0500
Subject: [PATCH 28/64] Extract previewBid functionality
---
src/policies/CDAuctioneer.sol | 58 +++++++++++++++++++++++------------
1 file changed, 39 insertions(+), 19 deletions(-)
diff --git a/src/policies/CDAuctioneer.sol b/src/policies/CDAuctioneer.sol
index e8f34b10..c02e6af7 100644
--- a/src/policies/CDAuctioneer.sol
+++ b/src/policies/CDAuctioneer.sol
@@ -57,30 +57,23 @@ contract CDAuctioneer is IConvertibleDepositAuctioneer, Policy, RolesConsumer {
/// @inheritdoc IConvertibleDepositAuctioneer
function bid(uint256 deposit) external override returns (uint256 convertible) {
- // update state
+ // Update state
currentTick = getCurrentTick();
state.lastUpdate = block.timestamp;
- // iterate until user has no more reserves to bid
- while (deposit > 0) {
- // handle spent/capacity for tick
- uint256 amount = currentTick.capacity < _convertFor(deposit, currentTick.price)
- ? state.tickSize
- : deposit;
- if (amount != state.tickSize) currentTick.capacity -= amount;
- else currentTick.price *= state.tickStep / decimals;
-
- // decrement bid and increment tick price
- deposit -= amount;
- convertible += _convertFor(amount, currentTick.price);
- }
-
- // TODO extract logic to previewBid
- // TODO update currentTick based on previewBid output
+ // Get bid results
+ uint256 currentTickCapacity;
+ uint256 currentTickPrice;
+ (currentTickCapacity, currentTickPrice, convertible) = _previewBid(deposit);
+ // Update day state
today.deposits += deposit;
today.convertible += convertible;
+ // Update current tick
+ currentTick.capacity = currentTickCapacity;
+ currentTick.price = currentTickPrice;
+
// TODO calculate average price for total deposit and convertible, check rounding, formula
uint256 conversionPrice = (deposit * decimals) / convertible;
@@ -92,12 +85,39 @@ contract CDAuctioneer is IConvertibleDepositAuctioneer, Policy, RolesConsumer {
uint48(block.timestamp + state.timeToExpiry),
false
);
+
+ return convertible;
+ }
+
+ function _previewBid(
+ uint256 deposit_
+ )
+ internal
+ view
+ returns (uint256 currentTickCapacity, uint256 currentTickPrice, uint256 convertible)
+ {
+ Tick memory tick = getCurrentTick();
+ uint256 remainingDeposit = deposit_;
+
+ while (remainingDeposit > 0) {
+ uint256 amount = tick.capacity < _convertFor(remainingDeposit, tick.price)
+ ? state.tickSize
+ : remainingDeposit;
+ if (amount != state.tickSize) tick.capacity -= amount;
+ else tick.price *= state.tickStep / decimals;
+
+ remainingDeposit -= amount;
+ convertible += _convertFor(amount, tick.price);
+ }
+
+ return (tick.capacity, tick.price, convertible);
}
/// @inheritdoc IConvertibleDepositAuctioneer
function previewBid(uint256 deposit) external view override returns (uint256 convertible) {
- // TODO
- return 0;
+ (, , convertible) = _previewBid(deposit);
+
+ return convertible;
}
// ========== VIEW FUNCTIONS ========== //
From fddaa3b05e4a52ea38fa7a450421c21d27b29107 Mon Sep 17 00:00:00 2001
From: Jem <0x0xjem@gmail.com>
Date: Tue, 17 Dec 2024 17:17:15 +0500
Subject: [PATCH 29/64] Docs
---
src/policies/CDAuctioneer.sol | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/src/policies/CDAuctioneer.sol b/src/policies/CDAuctioneer.sol
index c02e6af7..8a475aeb 100644
--- a/src/policies/CDAuctioneer.sol
+++ b/src/policies/CDAuctioneer.sol
@@ -89,6 +89,13 @@ contract CDAuctioneer is IConvertibleDepositAuctioneer, Policy, RolesConsumer {
return convertible;
}
+ /// @notice Internal function to preview the number of convertible tokens that can be purchased for a given deposit amount
+ /// @dev The function also returns the adjusted capacity and price of the current tick
+ ///
+ /// @param deposit_ The amount of deposit to be bid
+ /// @return currentTickCapacity The adjusted capacity of the current tick
+ /// @return currentTickPrice The adjusted price of the current tick
+ /// @return convertible The number of convertible tokens that can be purchased
function _previewBid(
uint256 deposit_
)
From e3ad735966c42da37e8ef787c1a58170cf35cebe Mon Sep 17 00:00:00 2001
From: Jem <0x0xjem@gmail.com>
Date: Wed, 18 Dec 2024 12:25:08 +0500
Subject: [PATCH 30/64] Add isExpired getter to CDPOS
---
src/modules/CDPOS/CDPOS.v1.sol | 6 ++++++
src/modules/CDPOS/OlympusConvertibleDepositPositions.sol | 9 +++++++++
2 files changed, 15 insertions(+)
diff --git a/src/modules/CDPOS/CDPOS.v1.sol b/src/modules/CDPOS/CDPOS.v1.sol
index 3fa1ccdd..c0d4e277 100644
--- a/src/modules/CDPOS/CDPOS.v1.sol
+++ b/src/modules/CDPOS/CDPOS.v1.sol
@@ -188,4 +188,10 @@ abstract contract CDPOSv1 is Module, ERC721 {
/// @param positionId_ The ID of the position
/// @return position The positions for the given ID
function getPosition(uint256 positionId_) external view virtual returns (Position memory);
+
+ /// @notice Check if a position is expired
+ ///
+ /// @param positionId_ The ID of the position
+ /// @return expired_ Whether the position is expired
+ function isExpired(uint256 positionId_) external view virtual returns (bool);
}
diff --git a/src/modules/CDPOS/OlympusConvertibleDepositPositions.sol b/src/modules/CDPOS/OlympusConvertibleDepositPositions.sol
index 01948eb6..5ba6fb3a 100644
--- a/src/modules/CDPOS/OlympusConvertibleDepositPositions.sol
+++ b/src/modules/CDPOS/OlympusConvertibleDepositPositions.sol
@@ -359,6 +359,15 @@ contract OlympusConvertibleDepositPositions is CDPOSv1 {
return _getPosition(positionId_);
}
+ /// @inheritdoc CDPOSv1
+ /// @dev This function reverts if:
+ /// - The position ID is invalid
+ ///
+ /// @return Returns true if the expiry timestamp is now or in the past
+ function isExpired(uint256 positionId_) external view virtual override returns (bool) {
+ return _getPosition(positionId_).expiry <= block.timestamp;
+ }
+
// ========== MODIFIERS ========== //
modifier onlyValidPosition(uint256 positionId_) {
From 77e278771d55b2b4c0a3960e27b2c8b62372611d Mon Sep 17 00:00:00 2001
From: Jem <0x0xjem@gmail.com>
Date: Wed, 18 Dec 2024 12:25:38 +0500
Subject: [PATCH 31/64] Adjust CDPOS SVG
---
src/modules/CDPOS/OlympusConvertibleDepositPositions.sol | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/modules/CDPOS/OlympusConvertibleDepositPositions.sol b/src/modules/CDPOS/OlympusConvertibleDepositPositions.sol
index 5ba6fb3a..975b2f87 100644
--- a/src/modules/CDPOS/OlympusConvertibleDepositPositions.sol
+++ b/src/modules/CDPOS/OlympusConvertibleDepositPositions.sol
@@ -244,18 +244,18 @@ contract OlympusConvertibleDepositPositions is CDPOSv1 {
'