From 527db32b6b7bb6bd1308964f6f3df6217103994d Mon Sep 17 00:00:00 2001 From: rndquu Date: Thu, 4 Apr 2024 10:12:48 +0300 Subject: [PATCH 1/7] feat: add collateral ratio setter --- .../src/dollar/facets/UbiquityPoolFacet.sol | 10 ++++++ .../src/dollar/interfaces/IUbiquityPool.sol | 28 +++++++++++++-- .../src/dollar/libraries/LibUbiquityPool.sol | 35 +++++++++++++++++++ .../diamond/facets/UbiquityPoolFacet.t.sol | 25 +++++++++++++ 4 files changed, 95 insertions(+), 3 deletions(-) diff --git a/packages/contracts/src/dollar/facets/UbiquityPoolFacet.sol b/packages/contracts/src/dollar/facets/UbiquityPoolFacet.sol index e9cd91a37..a2b4b215b 100644 --- a/packages/contracts/src/dollar/facets/UbiquityPoolFacet.sol +++ b/packages/contracts/src/dollar/facets/UbiquityPoolFacet.sol @@ -32,6 +32,11 @@ contract UbiquityPoolFacet is IUbiquityPool, Modifiers { return LibUbiquityPool.collateralInformation(collateralAddress); } + /// @inheritdoc IUbiquityPool + function collateralRatio() external view returns (uint256) { + return LibUbiquityPool.collateralRatio(); + } + /// @inheritdoc IUbiquityPool function collateralUsdBalance() external @@ -180,6 +185,11 @@ contract UbiquityPoolFacet is IUbiquityPool, Modifiers { ); } + /// @inheritdoc IUbiquityPool + function setCollateralRatio(uint256 newCollateralRatio) external onlyAdmin { + LibUbiquityPool.setCollateralRatio(newCollateralRatio); + } + /// @inheritdoc IUbiquityPool function setFees( uint256 collateralIndex, diff --git a/packages/contracts/src/dollar/interfaces/IUbiquityPool.sol b/packages/contracts/src/dollar/interfaces/IUbiquityPool.sol index 23e934e4f..e0dff3ad0 100644 --- a/packages/contracts/src/dollar/interfaces/IUbiquityPool.sol +++ b/packages/contracts/src/dollar/interfaces/IUbiquityPool.sol @@ -32,6 +32,12 @@ interface IUbiquityPool { view returns (LibUbiquityPool.CollateralInformation memory returnData); + /** + * @notice Returns current collateral ratio + * @return Collateral ratio + */ + function collateralRatio() external view returns (uint256); + /** * @notice Returns USD value of all collateral tokens held in the pool, in E18 * @return balanceTally USD value of all collateral tokens @@ -128,6 +134,12 @@ interface IUbiquityPool { uint256 collateralIndex ) external returns (uint256 collateralAmount); + /** + * @notice Updates collateral token price in USD from ChainLink price feed + * @param collateralIndex Collateral token index + */ + function updateChainLinkCollateralPrice(uint256 collateralIndex) external; + //========================= // AMO minters functions //========================= @@ -181,10 +193,20 @@ interface IUbiquityPool { ) external; /** - * @notice Updates collateral token price in USD from ChainLink price feed - * @param collateralIndex Collateral token index + * @notice Sets collateral ratio + * @dev How much collateral/governance tokens user should provide/get to mint/redeem Dollar tokens, 1e6 precision + * + * @dev Example (1_000_000 = 100%): + * - Mint: user provides 1 collateral token to get 1 Dollar + * - Redeem: user gets 1 collateral token for 1 Dollar + * + * @dev Example (900_000 = 90%): + * - Mint: user provides 0.9 collateral token and 0.1 Governance token to get 1 Dollar + * - Redeem: user gets 0.9 collateral token and 0.1 Governance token for 1 Dollar + * + * @param newCollateralRatio New collateral ratio */ - function updateChainLinkCollateralPrice(uint256 collateralIndex) external; + function setCollateralRatio(uint256 newCollateralRatio) external; /** * @notice Sets mint and redeem fees, 1_000_000 = 100% diff --git a/packages/contracts/src/dollar/libraries/LibUbiquityPool.sol b/packages/contracts/src/dollar/libraries/LibUbiquityPool.sol index d3447d50f..ea577e4f5 100644 --- a/packages/contracts/src/dollar/libraries/LibUbiquityPool.sol +++ b/packages/contracts/src/dollar/libraries/LibUbiquityPool.sol @@ -48,6 +48,8 @@ library LibUbiquityPool { uint256[] collateralPriceFeedStalenessThresholds; // collateral index -> collateral price uint256[] collateralPrices; + // how much collateral/governance tokens user should provide/get to mint/redeem Dollar tokens, 1e6 precision + uint256 collateralRatio; // array collateral symbols string[] collateralSymbols; // collateral address -> is it enabled @@ -138,6 +140,8 @@ library LibUbiquityPool { ); /// @notice Emitted on setting a collateral price event CollateralPriceSet(uint256 collateralIndex, uint256 newPrice); + /// @notice Emitted on setting a collateral ratio + event CollateralRatioSet(uint256 newCollateralRatio); /// @notice Emitted on enabling/disabling a particular collateral token event CollateralToggled(uint256 collateralIndex, bool newState); /// @notice Emitted when fees are updated @@ -258,6 +262,15 @@ library LibUbiquityPool { ); } + /** + * @notice Returns current collateral ratio + * @return Collateral ratio + */ + function collateralRatio() internal view returns (uint256) { + UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage(); + return poolStorage.collateralRatio; + } + /** * @notice Returns USD value of all collateral tokens held in the pool, in E18 * @return balanceTally USD value of all collateral tokens @@ -778,6 +791,28 @@ library LibUbiquityPool { ); } + /** + * @notice Sets collateral ratio + * @dev How much collateral/governance tokens user should provide/get to mint/redeem Dollar tokens, 1e6 precision + * + * @dev Example (1_000_000 = 100%): + * - Mint: user provides 1 collateral token to get 1 Dollar + * - Redeem: user gets 1 collateral token for 1 Dollar + * + * @dev Example (900_000 = 90%): + * - Mint: user provides 0.9 collateral token and 0.1 Governance token to get 1 Dollar + * - Redeem: user gets 0.9 collateral token and 0.1 Governance token for 1 Dollar + * + * @param newCollateralRatio New collateral ratio + */ + function setCollateralRatio(uint256 newCollateralRatio) internal { + UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage(); + + poolStorage.collateralRatio = newCollateralRatio; + + emit CollateralRatioSet(newCollateralRatio); + } + /** * @notice Sets mint and redeem fees, 1_000_000 = 100% * @param collateralIndex Collateral token index diff --git a/packages/contracts/test/diamond/facets/UbiquityPoolFacet.t.sol b/packages/contracts/test/diamond/facets/UbiquityPoolFacet.t.sol index 78309d118..40771972c 100644 --- a/packages/contracts/test/diamond/facets/UbiquityPoolFacet.t.sol +++ b/packages/contracts/test/diamond/facets/UbiquityPoolFacet.t.sol @@ -37,6 +37,7 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { uint256 stalenessThreshold ); event CollateralPriceSet(uint256 collateralIndex, uint256 newPrice); + event CollateralRatioSet(uint256 newCollateralRatio); event CollateralToggled(uint256 collateralIndex, bool newState); event FeesSet( uint256 collateralIndex, @@ -95,6 +96,9 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { 1 days // price feed staleness threshold in seconds ); + // set collateral ratio to 100% + ubiquityPoolFacet.setCollateralRatio(1_000_000); + // enable collateral at index 0 ubiquityPoolFacet.toggleCollateral(0); // set mint and redeem fees @@ -197,6 +201,11 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { assertEq(info.redemptionFee, 20000); } + function testCollateralRatio_ShouldReturnCollateralRatio() public { + uint256 collateralRatio = ubiquityPoolFacet.collateralRatio(); + assertEq(collateralRatio, 1_000_000); + } + function testCollateralUsdBalance_ShouldReturnTotalAmountOfCollateralInUsd() public { @@ -883,6 +892,22 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { vm.stopPrank(); } + function testSetCollateralRatio_ShouldSetCollateralRatio() public { + vm.startPrank(admin); + + uint256 oldCollateralRatio = ubiquityPoolFacet.collateralRatio(); + assertEq(oldCollateralRatio, 1_000_000); + + uint256 newCollateralRatio = 900_000; + vm.expectEmit(address(ubiquityPoolFacet)); + emit CollateralRatioSet(newCollateralRatio); + ubiquityPoolFacet.setCollateralRatio(newCollateralRatio); + + assertEq(ubiquityPoolFacet.collateralRatio(), newCollateralRatio); + + vm.stopPrank(); + } + function testSetFees_ShouldSetMintAndRedeemFees() public { vm.startPrank(admin); From d145f3a990c21b7de7d67d1b24d6baaaf08caf29 Mon Sep 17 00:00:00 2001 From: rndquu Date: Thu, 4 Apr 2024 13:27:03 +0300 Subject: [PATCH 2/7] feat: add governance pool setter --- cspell.json | 1 + .../src/dollar/facets/UbiquityPoolFacet.sol | 14 +++++ .../interfaces/ICurveTwocryptoOptimized.sol | 20 +++++++ .../src/dollar/interfaces/IUbiquityPool.sol | 21 ++++++++ .../src/dollar/libraries/LibUbiquityPool.sol | 37 +++++++++++++ .../mocks/MockCurveTwocryptoOptimized.sol | 15 ++++++ .../diamond/facets/UbiquityPoolFacet.t.sol | 54 +++++++++++++++++-- 7 files changed, 159 insertions(+), 3 deletions(-) create mode 100644 packages/contracts/src/dollar/interfaces/ICurveTwocryptoOptimized.sol create mode 100644 packages/contracts/src/dollar/mocks/MockCurveTwocryptoOptimized.sol diff --git a/cspell.json b/cspell.json index 07237c723..326d1e1ac 100644 --- a/cspell.json +++ b/cspell.json @@ -140,6 +140,7 @@ "twap", "typechain", "TYPEHASH", + "Twocrypto", "Ubiqui", "UbiquiStick", "Unassigns", diff --git a/packages/contracts/src/dollar/facets/UbiquityPoolFacet.sol b/packages/contracts/src/dollar/facets/UbiquityPoolFacet.sol index a2b4b215b..6186dc1b0 100644 --- a/packages/contracts/src/dollar/facets/UbiquityPoolFacet.sol +++ b/packages/contracts/src/dollar/facets/UbiquityPoolFacet.sol @@ -86,6 +86,11 @@ contract UbiquityPoolFacet is IUbiquityPool, Modifiers { ); } + /// @inheritdoc IUbiquityPool + function governanceEthPoolAddress() external view returns (address) { + return LibUbiquityPool.governanceEthPoolAddress(); + } + //==================== // Public functions //==================== @@ -199,6 +204,15 @@ contract UbiquityPoolFacet is IUbiquityPool, Modifiers { LibUbiquityPool.setFees(collateralIndex, newMintFee, newRedeemFee); } + /// @inheritdoc IUbiquityPool + function setGovernanceEthPoolAddress( + address newGovernanceEthPoolAddress + ) external onlyAdmin { + LibUbiquityPool.setGovernanceEthPoolAddress( + newGovernanceEthPoolAddress + ); + } + /// @inheritdoc IUbiquityPool function setPoolCeiling( uint256 collateralIndex, diff --git a/packages/contracts/src/dollar/interfaces/ICurveTwocryptoOptimized.sol b/packages/contracts/src/dollar/interfaces/ICurveTwocryptoOptimized.sol new file mode 100644 index 000000000..d02170332 --- /dev/null +++ b/packages/contracts/src/dollar/interfaces/ICurveTwocryptoOptimized.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {ICurveStableSwapMetaNG} from "./ICurveStableSwapMetaNG.sol"; + +/** + * @notice Curve's CurveTwocryptoOptimized interface + * + * @dev Differences between Curve's crypto and stable swap meta pools (and how Ubiquity organization uses them): + * 1. They contain different tokens: + * a) Curve's stable swap metapool containts Dollar/3CRVLP pair + * b) Curve's crypto pool contains Governance/ETH pair + * 2. They use different bonding curve shapes: + * a) Curve's stable swap metapool is more straight (because underlying tokens are pegged to USD) + * b) Curve's crypto pool resembles Uniswap's bonding curve (because underlying tokens are not USD pegged) + * + * @dev Basically `ICurveTwocryptoOptimized` has the same interface as `ICurveStableSwapMetaNG` + * but we distinguish them in the code for clarity. + */ +interface ICurveTwocryptoOptimized is ICurveStableSwapMetaNG {} diff --git a/packages/contracts/src/dollar/interfaces/IUbiquityPool.sol b/packages/contracts/src/dollar/interfaces/IUbiquityPool.sol index e0dff3ad0..74ccd6043 100644 --- a/packages/contracts/src/dollar/interfaces/IUbiquityPool.sol +++ b/packages/contracts/src/dollar/interfaces/IUbiquityPool.sol @@ -84,6 +84,12 @@ interface IUbiquityPool { uint256 collateralIndex ) external view returns (uint256); + /** + * @notice Returns pool address for Governance/ETH pair + * @return Pool address + */ + function governanceEthPoolAddress() external view returns (address); + //==================== // Public functions //==================== @@ -220,6 +226,21 @@ interface IUbiquityPool { uint256 newRedeemFee ) external; + /** + * @notice Sets a new pool address for Governance/ETH pair + * + * @dev Based on Curve's CurveTwocryptoOptimized contract. Used for fetching Governance token USD price. + * How it works: + * 1. Fetch Governance/ETH price from CurveTwocryptoOptimized's built-in oracle + * 2. Fetch ETH/USD price from chainlink feed + * 3. Calculate Governance token price in USD + * + * @param newGovernanceEthPoolAddress New pool address for Governance/ETH pair + */ + function setGovernanceEthPoolAddress( + address newGovernanceEthPoolAddress + ) external; + /** * @notice Sets max amount of collateral for a particular collateral token * @param collateralIndex Collateral token index diff --git a/packages/contracts/src/dollar/libraries/LibUbiquityPool.sol b/packages/contracts/src/dollar/libraries/LibUbiquityPool.sol index ea577e4f5..2d387b88e 100644 --- a/packages/contracts/src/dollar/libraries/LibUbiquityPool.sol +++ b/packages/contracts/src/dollar/libraries/LibUbiquityPool.sol @@ -89,6 +89,11 @@ library LibUbiquityPool { bool[] isMintPaused; // whether redeeming is paused for a particular collateral index bool[] isRedeemPaused; + //==================================== + // Governance token pricing related + //==================================== + // Curve's CurveTwocryptoOptimized contract for Governance/ETH pair + address governanceEthPoolAddress; } /// @notice Struct used for detailed collateral information @@ -150,6 +155,8 @@ library LibUbiquityPool { uint256 newMintFee, uint256 newRedeemFee ); + /// @notice Emitted on setting a pool for Governance/ETH pair + event GovernanceEthPoolSet(address newGovernanceEthPoolAddress); /// @notice Emitted on toggling pause for mint/redeem/borrow event MintRedeemBorrowToggled(uint256 collateralIndex, uint8 toggleIndex); /// @notice Emitted when new pool ceiling (i.e. max amount of collateral) is set @@ -360,6 +367,15 @@ library LibUbiquityPool { poolStorage.redeemCollateralBalances[userAddress][collateralIndex]; } + /** + * @notice Returns pool address for Governance/ETH pair + * @return Pool address + */ + function governanceEthPoolAddress() internal view returns (address) { + UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage(); + return poolStorage.governanceEthPoolAddress; + } + //==================== // Public functions //==================== @@ -832,6 +848,27 @@ library LibUbiquityPool { emit FeesSet(collateralIndex, newMintFee, newRedeemFee); } + /** + * @notice Sets a new pool address for Governance/ETH pair + * + * @dev Based on Curve's CurveTwocryptoOptimized contract. Used for fetching Governance token USD price. + * How it works: + * 1. Fetch Governance/ETH price from CurveTwocryptoOptimized's built-in oracle + * 2. Fetch ETH/USD price from chainlink feed + * 3. Calculate Governance token price in USD + * + * @param newGovernanceEthPoolAddress New pool address for Governance/ETH pair + */ + function setGovernanceEthPoolAddress( + address newGovernanceEthPoolAddress + ) internal { + UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage(); + + poolStorage.governanceEthPoolAddress = newGovernanceEthPoolAddress; + + emit GovernanceEthPoolSet(newGovernanceEthPoolAddress); + } + /** * @notice Sets max amount of collateral for a particular collateral token * @param collateralIndex Collateral token index diff --git a/packages/contracts/src/dollar/mocks/MockCurveTwocryptoOptimized.sol b/packages/contracts/src/dollar/mocks/MockCurveTwocryptoOptimized.sol new file mode 100644 index 000000000..9101669e9 --- /dev/null +++ b/packages/contracts/src/dollar/mocks/MockCurveTwocryptoOptimized.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {ICurveTwocryptoOptimized} from "../interfaces/ICurveTwocryptoOptimized.sol"; +import {MockCurveStableSwapMetaNG} from "./MockCurveStableSwapMetaNG.sol"; + +contract MockCurveTwocryptoOptimized is + ICurveTwocryptoOptimized, + MockCurveStableSwapMetaNG +{ + constructor( + address _token0, + address _token1 + ) MockCurveStableSwapMetaNG(_token0, _token1) {} +} diff --git a/packages/contracts/test/diamond/facets/UbiquityPoolFacet.t.sol b/packages/contracts/test/diamond/facets/UbiquityPoolFacet.t.sol index 40771972c..3369e9066 100644 --- a/packages/contracts/test/diamond/facets/UbiquityPoolFacet.t.sol +++ b/packages/contracts/test/diamond/facets/UbiquityPoolFacet.t.sol @@ -8,6 +8,7 @@ import {LibUbiquityPool} from "../../../src/dollar/libraries/LibUbiquityPool.sol import {MockChainLinkFeed} from "../../../src/dollar/mocks/MockChainLinkFeed.sol"; import {MockERC20} from "../../../src/dollar/mocks/MockERC20.sol"; import {MockCurveStableSwapMetaNG} from "../../../src/dollar/mocks/MockCurveStableSwapMetaNG.sol"; +import {MockCurveTwocryptoOptimized} from "../../../src/dollar/mocks/MockCurveTwocryptoOptimized.sol"; contract MockDollarAmoMinter is IDollarAmoMinter { function collateralDollarBalance() external pure returns (uint256) { @@ -24,7 +25,9 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { MockERC20 collateralToken; MockChainLinkFeed collateralTokenPriceFeed; MockCurveStableSwapMetaNG curveDollarMetaPool; + MockCurveTwocryptoOptimized curveGovernanceEthPool; MockERC20 curveTriPoolLpToken; + MockERC20 wethToken; address user = address(1); @@ -44,6 +47,7 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { uint256 newMintFee, uint256 newRedeemFee ); + event GovernanceEthPoolSet(address newGovernanceEthPoolAddress); event MintRedeemBorrowToggled(uint256 collateralIndex, uint8 toggleIndex); event PoolCeilingSet(uint256 collateralIndex, uint256 newCeiling); event PriceThresholdsSet( @@ -63,6 +67,9 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { // init collateral price feed collateralTokenPriceFeed = new MockChainLinkFeed(); + // init WETH token + wethToken = new MockERC20("WETH", "WETH", 18); + // init Curve 3CRV-LP token curveTriPoolLpToken = new MockERC20("3CRV", "3CRV", 18); @@ -72,6 +79,12 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { address(curveTriPoolLpToken) ); + // init Curve Governance-WETH crypto pool + curveGovernanceEthPool = new MockCurveTwocryptoOptimized( + address(governanceToken), + address(wethToken) + ); + // add collateral token to the pool uint256 poolCeiling = 50_000e18; // max 50_000 of collateral tokens is allowed ubiquityPoolFacet.addCollateralToken( @@ -96,9 +109,6 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { 1 days // price feed staleness threshold in seconds ); - // set collateral ratio to 100% - ubiquityPoolFacet.setCollateralRatio(1_000_000); - // enable collateral at index 0 ubiquityPoolFacet.toggleCollateral(0); // set mint and redeem fees @@ -111,6 +121,12 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { ubiquityPoolFacet.setRedemptionDelayBlocks(2); // set mint price threshold to $1.01 and redeem price to $0.99 ubiquityPoolFacet.setPriceThresholds(1010000, 990000); + // set collateral ratio to 100% + ubiquityPoolFacet.setCollateralRatio(1_000_000); + // set Governance-ETH pool + ubiquityPoolFacet.setGovernanceEthPoolAddress( + address(curveGovernanceEthPool) + ); // init AMO minter dollarAmoMinter = new MockDollarAmoMinter(); @@ -303,6 +319,14 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { assertEq(redeemCollateralBalance, 97.02e18); } + function testGovernanceEthPoolAddress_ShouldReturnGovernanceEthPoolAddress() + public + { + address governanceEthPoolAddress = ubiquityPoolFacet + .governanceEthPoolAddress(); + assertEq(governanceEthPoolAddress, address(curveGovernanceEthPool)); + } + //==================== // Public functions //==================== @@ -918,6 +942,30 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { vm.stopPrank(); } + function testSetGovernanceEthPoolAddress_ShouldSetGovernanceEthPoolAddress() + public + { + vm.startPrank(admin); + + address oldGovernanceEthPoolAddress = ubiquityPoolFacet + .governanceEthPoolAddress(); + assertEq(oldGovernanceEthPoolAddress, address(curveGovernanceEthPool)); + + address newGovernanceEthPoolAddress = address(1); + vm.expectEmit(address(ubiquityPoolFacet)); + emit GovernanceEthPoolSet(newGovernanceEthPoolAddress); + ubiquityPoolFacet.setGovernanceEthPoolAddress( + newGovernanceEthPoolAddress + ); + + assertEq( + ubiquityPoolFacet.governanceEthPoolAddress(), + newGovernanceEthPoolAddress + ); + + vm.stopPrank(); + } + function testSetPoolCeiling_ShouldSetMaxAmountOfTokensAllowedForCollateral() public { From abd3024aca18d2af028f12018dccbb245c73f187 Mon Sep 17 00:00:00 2001 From: rndquu Date: Thu, 4 Apr 2024 17:49:04 +0300 Subject: [PATCH 3/7] feat: add ETH/USD price feed setter --- .../src/dollar/facets/UbiquityPoolFacet.sol | 20 ++++++ .../src/dollar/interfaces/IUbiquityPool.sol | 19 ++++++ .../src/dollar/libraries/LibUbiquityPool.sol | 42 +++++++++++++ .../diamond/facets/UbiquityPoolFacet.t.sol | 63 +++++++++++++++++++ 4 files changed, 144 insertions(+) diff --git a/packages/contracts/src/dollar/facets/UbiquityPoolFacet.sol b/packages/contracts/src/dollar/facets/UbiquityPoolFacet.sol index 6186dc1b0..0c1e95e4e 100644 --- a/packages/contracts/src/dollar/facets/UbiquityPoolFacet.sol +++ b/packages/contracts/src/dollar/facets/UbiquityPoolFacet.sol @@ -46,6 +46,15 @@ contract UbiquityPoolFacet is IUbiquityPool, Modifiers { return LibUbiquityPool.collateralUsdBalance(); } + /// @inheritdoc IUbiquityPool + function ethUsdPriceFeedInformation() + external + view + returns (address, uint256) + { + return LibUbiquityPool.ethUsdPriceFeedInformation(); + } + /// @inheritdoc IUbiquityPool function freeCollateralBalance( uint256 collateralIndex @@ -195,6 +204,17 @@ contract UbiquityPoolFacet is IUbiquityPool, Modifiers { LibUbiquityPool.setCollateralRatio(newCollateralRatio); } + /// @inheritdoc IUbiquityPool + function setEthUsdChainLinkPriceFeed( + address newPriceFeedAddress, + uint256 newStalenessThreshold + ) external onlyAdmin { + LibUbiquityPool.setEthUsdChainLinkPriceFeed( + newPriceFeedAddress, + newStalenessThreshold + ); + } + /// @inheritdoc IUbiquityPool function setFees( uint256 collateralIndex, diff --git a/packages/contracts/src/dollar/interfaces/IUbiquityPool.sol b/packages/contracts/src/dollar/interfaces/IUbiquityPool.sol index 74ccd6043..cd3c0a762 100644 --- a/packages/contracts/src/dollar/interfaces/IUbiquityPool.sol +++ b/packages/contracts/src/dollar/interfaces/IUbiquityPool.sol @@ -47,6 +47,15 @@ interface IUbiquityPool { view returns (uint256 balanceTally); + /** + * @notice Returns chainlink price feed information for ETH/USD pair + * @return Price feed address and staleness threshold in seconds + */ + function ethUsdPriceFeedInformation() + external + view + returns (address, uint256); + /** * @notice Returns free collateral balance (i.e. that can be borrowed by AMO minters) * @param collateralIndex collateral token index @@ -214,6 +223,16 @@ interface IUbiquityPool { */ function setCollateralRatio(uint256 newCollateralRatio) external; + /** + * @notice Sets chainlink params for ETH/USD price feed + * @param newPriceFeedAddress New chainlink price feed address for ETH/USD pair + * @param newStalenessThreshold New threshold in seconds when chainlink's ETH/USD price feed answer should be considered stale + */ + function setEthUsdChainLinkPriceFeed( + address newPriceFeedAddress, + uint256 newStalenessThreshold + ) external; + /** * @notice Sets mint and redeem fees, 1_000_000 = 100% * @param collateralIndex Collateral token index diff --git a/packages/contracts/src/dollar/libraries/LibUbiquityPool.sol b/packages/contracts/src/dollar/libraries/LibUbiquityPool.sol index 2d387b88e..d67dc6fb6 100644 --- a/packages/contracts/src/dollar/libraries/LibUbiquityPool.sol +++ b/packages/contracts/src/dollar/libraries/LibUbiquityPool.sol @@ -92,6 +92,10 @@ library LibUbiquityPool { //==================================== // Governance token pricing related //==================================== + // chainlink price feed for ETH/USD pair + address ethUsdPriceFeedAddress; + // threshold in seconds when chainlink's ETH/USD price feed answer should be considered stale + uint256 ethUsdPriceFeedStalenessThreshold; // Curve's CurveTwocryptoOptimized contract for Governance/ETH pair address governanceEthPoolAddress; } @@ -149,6 +153,11 @@ library LibUbiquityPool { event CollateralRatioSet(uint256 newCollateralRatio); /// @notice Emitted on enabling/disabling a particular collateral token event CollateralToggled(uint256 collateralIndex, bool newState); + /// @notice Emitted on setting chainlink's price feed for ETH/USD pair + event EthUsdPriceFeedSet( + address newPriceFeedAddress, + uint256 newStalenessThreshold + ); /// @notice Emitted when fees are updated event FeesSet( uint256 collateralIndex, @@ -298,6 +307,22 @@ library LibUbiquityPool { } } + /** + * @notice Returns chainlink price feed information for ETH/USD pair + * @return Price feed address and staleness threshold in seconds + */ + function ethUsdPriceFeedInformation() + internal + view + returns (address, uint256) + { + UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage(); + return ( + poolStorage.ethUsdPriceFeedAddress, + poolStorage.ethUsdPriceFeedStalenessThreshold + ); + } + /** * @notice Returns free collateral balance (i.e. that can be borrowed by AMO minters) * @param collateralIndex collateral token index @@ -829,6 +854,23 @@ library LibUbiquityPool { emit CollateralRatioSet(newCollateralRatio); } + /** + * @notice Sets chainlink params for ETH/USD price feed + * @param newPriceFeedAddress New chainlink price feed address for ETH/USD pair + * @param newStalenessThreshold New threshold in seconds when chainlink's ETH/USD price feed answer should be considered stale + */ + function setEthUsdChainLinkPriceFeed( + address newPriceFeedAddress, + uint256 newStalenessThreshold + ) internal { + UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage(); + + poolStorage.ethUsdPriceFeedAddress = newPriceFeedAddress; + poolStorage.ethUsdPriceFeedStalenessThreshold = newStalenessThreshold; + + emit EthUsdPriceFeedSet(newPriceFeedAddress, newStalenessThreshold); + } + /** * @notice Sets mint and redeem fees, 1_000_000 = 100% * @param collateralIndex Collateral token index diff --git a/packages/contracts/test/diamond/facets/UbiquityPoolFacet.t.sol b/packages/contracts/test/diamond/facets/UbiquityPoolFacet.t.sol index 3369e9066..efa3c8ecb 100644 --- a/packages/contracts/test/diamond/facets/UbiquityPoolFacet.t.sol +++ b/packages/contracts/test/diamond/facets/UbiquityPoolFacet.t.sol @@ -27,6 +27,7 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { MockCurveStableSwapMetaNG curveDollarMetaPool; MockCurveTwocryptoOptimized curveGovernanceEthPool; MockERC20 curveTriPoolLpToken; + MockChainLinkFeed ethUsdPriceFeed; MockERC20 wethToken; address user = address(1); @@ -42,6 +43,10 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { event CollateralPriceSet(uint256 collateralIndex, uint256 newPrice); event CollateralRatioSet(uint256 newCollateralRatio); event CollateralToggled(uint256 collateralIndex, bool newState); + event EthUsdPriceFeedSet( + address newPriceFeedAddress, + uint256 newStalenessThreshold + ); event FeesSet( uint256 collateralIndex, uint256 newMintFee, @@ -67,6 +72,9 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { // init collateral price feed collateralTokenPriceFeed = new MockChainLinkFeed(); + // init ETH/USD price feed + ethUsdPriceFeed = new MockChainLinkFeed(); + // init WETH token wethToken = new MockERC20("WETH", "WETH", 18); @@ -102,6 +110,15 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { 1 // answered in round ); + // set ETH/USD price feed mock params + ethUsdPriceFeed.updateMockParams( + 1, // round id + 3000_00000000, // answer, 3000_00000000 = $3000 (8 decimals) + block.timestamp, // started at + block.timestamp, // updated at + 1 // answered in round + ); + // set price feed for collateral token ubiquityPoolFacet.setCollateralChainLinkPriceFeed( address(collateralToken), // collateral token address @@ -109,6 +126,12 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { 1 days // price feed staleness threshold in seconds ); + // set price feed for ETH/USD pair + ubiquityPoolFacet.setEthUsdChainLinkPriceFeed( + address(ethUsdPriceFeed), // price feed address + 1 days // price feed staleness threshold in seconds + ); + // enable collateral at index 0 ubiquityPoolFacet.toggleCollateral(0); // set mint and redeem fees @@ -244,6 +267,15 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { assertEq(balanceTally, 100e18); } + function testEthUsdPriceFeedInformation_ShouldReturnEthUsdPriceFeedInformation() + public + { + (address priceFeed, uint256 stalenessThreshold) = ubiquityPoolFacet + .ethUsdPriceFeedInformation(); + assertEq(priceFeed, address(ethUsdPriceFeed)); + assertEq(stalenessThreshold, 1 days); + } + function testFreeCollateralBalance_ShouldReturnCollateralAmountAvailableForBorrowingByAmoMinters() public { @@ -932,6 +964,37 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { vm.stopPrank(); } + function testSetEthUsdChainLinkPriceFeed_ShouldSetEthUsdChainLinkPriceFeed() + public + { + vm.startPrank(admin); + + ( + address oldPriceFeedAddress, + uint256 oldStalenessThreshold + ) = ubiquityPoolFacet.ethUsdPriceFeedInformation(); + assertEq(oldPriceFeedAddress, address(ethUsdPriceFeed)); + assertEq(oldStalenessThreshold, 1 days); + + address newPriceFeedAddress = address(1); + uint256 newStalenessThreshold = 2 days; + vm.expectEmit(address(ubiquityPoolFacet)); + emit EthUsdPriceFeedSet(newPriceFeedAddress, newStalenessThreshold); + ubiquityPoolFacet.setEthUsdChainLinkPriceFeed( + newPriceFeedAddress, + newStalenessThreshold + ); + + ( + address updatedPriceFeedAddress, + uint256 updatedStalenessThreshold + ) = ubiquityPoolFacet.ethUsdPriceFeedInformation(); + assertEq(updatedPriceFeedAddress, newPriceFeedAddress); + assertEq(updatedStalenessThreshold, newStalenessThreshold); + + vm.stopPrank(); + } + function testSetFees_ShouldSetMintAndRedeemFees() public { vm.startPrank(admin); From 5fc94685aa501cfc564e546d270d0a5c9a484966 Mon Sep 17 00:00:00 2001 From: rndquu Date: Fri, 5 Apr 2024 13:04:26 +0300 Subject: [PATCH 4/7] feat: fetch Governance USD price --- .../src/dollar/facets/UbiquityPoolFacet.sol | 9 ++++ .../interfaces/ICurveTwocryptoOptimized.sol | 12 ++++- .../src/dollar/interfaces/IUbiquityPool.sol | 13 +++++ .../src/dollar/libraries/LibUbiquityPool.sol | 50 +++++++++++++++++++ .../mocks/MockCurveTwocryptoOptimized.sol | 4 ++ .../diamond/facets/UbiquityPoolFacet.t.sol | 47 +++++++++++++++++ 6 files changed, 134 insertions(+), 1 deletion(-) diff --git a/packages/contracts/src/dollar/facets/UbiquityPoolFacet.sol b/packages/contracts/src/dollar/facets/UbiquityPoolFacet.sol index 0c1e95e4e..e1699281c 100644 --- a/packages/contracts/src/dollar/facets/UbiquityPoolFacet.sol +++ b/packages/contracts/src/dollar/facets/UbiquityPoolFacet.sol @@ -83,6 +83,15 @@ contract UbiquityPoolFacet is IUbiquityPool, Modifiers { return LibUbiquityPool.getDollarPriceUsd(); } + /// @inheritdoc IUbiquityPool + function getGovernancePriceUsd() + external + view + returns (uint256 governancePriceUsd) + { + return LibUbiquityPool.getGovernancePriceUsd(); + } + /// @inheritdoc IUbiquityPool function getRedeemCollateralBalance( address userAddress, diff --git a/packages/contracts/src/dollar/interfaces/ICurveTwocryptoOptimized.sol b/packages/contracts/src/dollar/interfaces/ICurveTwocryptoOptimized.sol index d02170332..e6ddce950 100644 --- a/packages/contracts/src/dollar/interfaces/ICurveTwocryptoOptimized.sol +++ b/packages/contracts/src/dollar/interfaces/ICurveTwocryptoOptimized.sol @@ -13,8 +13,18 @@ import {ICurveStableSwapMetaNG} from "./ICurveStableSwapMetaNG.sol"; * 2. They use different bonding curve shapes: * a) Curve's stable swap metapool is more straight (because underlying tokens are pegged to USD) * b) Curve's crypto pool resembles Uniswap's bonding curve (because underlying tokens are not USD pegged) + * 3. The `price_oracle()` method works differently: + * a) Curve's stable swap metapool `price_oracle(uint256 i)` accepts coin index parameter + * b) Curve's crypto pool `price_oracle()` doesn't accept coin index parameter and always returns oracle price for coin at index 1 * * @dev Basically `ICurveTwocryptoOptimized` has the same interface as `ICurveStableSwapMetaNG` * but we distinguish them in the code for clarity. */ -interface ICurveTwocryptoOptimized is ICurveStableSwapMetaNG {} +interface ICurveTwocryptoOptimized is ICurveStableSwapMetaNG { + /** + * @notice Getter for the oracle price of the coin at index 1 with regard to the coin at index 0. + * The price oracle is an exponential moving average with a periodicity determined by `ma_time`. + * @return Price oracle + */ + function price_oracle() external view returns (uint256); +} diff --git a/packages/contracts/src/dollar/interfaces/IUbiquityPool.sol b/packages/contracts/src/dollar/interfaces/IUbiquityPool.sol index cd3c0a762..cf4c2a868 100644 --- a/packages/contracts/src/dollar/interfaces/IUbiquityPool.sol +++ b/packages/contracts/src/dollar/interfaces/IUbiquityPool.sol @@ -82,6 +82,19 @@ interface IUbiquityPool { */ function getDollarPriceUsd() external view returns (uint256 dollarPriceUsd); + /** + * @notice Returns Governance token price in USD (6 decimals precision) + * @dev How it works: + * 1. Fetch ETH/USD price from chainlink oracle + * 2. Fetch Governance/ETH price from Curve's oracle + * 3. Calculate Governance token price in USD + * @return governancePriceUsd Governance token price in USD + */ + function getGovernancePriceUsd() + external + view + returns (uint256 governancePriceUsd); + /** * @notice Returns user's balance available for redemption * @param userAddress User address diff --git a/packages/contracts/src/dollar/libraries/LibUbiquityPool.sol b/packages/contracts/src/dollar/libraries/LibUbiquityPool.sol index d67dc6fb6..ec24f7342 100644 --- a/packages/contracts/src/dollar/libraries/LibUbiquityPool.sol +++ b/packages/contracts/src/dollar/libraries/LibUbiquityPool.sol @@ -7,6 +7,7 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {SafeMath} from "@openzeppelin/contracts/utils/math/SafeMath.sol"; import {ICurveStableSwapMetaNG} from "../interfaces/ICurveStableSwapMetaNG.sol"; +import {ICurveTwocryptoOptimized} from "../interfaces/ICurveTwocryptoOptimized.sol"; import {IDollarAmoMinter} from "../interfaces/IDollarAmoMinter.sol"; import {IERC20Ubiquity} from "../interfaces/IERC20Ubiquity.sol"; import {UBIQUITY_POOL_PRICE_PRECISION} from "./Constants.sol"; @@ -377,6 +378,55 @@ library LibUbiquityPool { .div(1e18); } + /** + * @notice Returns Governance token price in USD (6 decimals precision) + * @dev How it works: + * 1. Fetch ETH/USD price from chainlink oracle + * 2. Fetch Governance/ETH price from Curve's oracle + * 3. Calculate Governance token price in USD + * @return governancePriceUsd Governance token price in USD + */ + function getGovernancePriceUsd() + internal + view + returns (uint256 governancePriceUsd) + { + UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage(); + + // fetch latest ETH/USD price + AggregatorV3Interface ethUsdPriceFeed = AggregatorV3Interface( + poolStorage.ethUsdPriceFeedAddress + ); + (, int256 answer, , uint256 updatedAt, ) = ethUsdPriceFeed + .latestRoundData(); + uint256 ethUsdPriceFeedDecimals = ethUsdPriceFeed.decimals(); + + // validate ETH/USD chainlink response + require(answer > 0, "Invalid price"); + require( + block.timestamp - updatedAt < + poolStorage.ethUsdPriceFeedStalenessThreshold, + "Stale data" + ); + + // convert ETH/USD chainlink price to 6 decimals + uint256 ethUsdPrice = uint256(answer) + .mul(UBIQUITY_POOL_PRICE_PRECISION) + .div(10 ** ethUsdPriceFeedDecimals); + + // fetch ETH/Governance price (18 decimals) + uint256 ethGovernancePriceD18 = ICurveTwocryptoOptimized( + poolStorage.governanceEthPoolAddress + ).price_oracle(); + // calculate Governance/ETH price (18 decimals) + uint256 governanceEthPriceD18 = uint256(1e18).mul(1e18).div( + ethGovernancePriceD18 + ); + + // calculate Governance token price in USD (6 decimals) + governancePriceUsd = governanceEthPriceD18.mul(ethUsdPrice).div(1e18); + } + /** * @notice Returns user's balance available for redemption * @param userAddress User address diff --git a/packages/contracts/src/dollar/mocks/MockCurveTwocryptoOptimized.sol b/packages/contracts/src/dollar/mocks/MockCurveTwocryptoOptimized.sol index 9101669e9..d03c66fa8 100644 --- a/packages/contracts/src/dollar/mocks/MockCurveTwocryptoOptimized.sol +++ b/packages/contracts/src/dollar/mocks/MockCurveTwocryptoOptimized.sol @@ -12,4 +12,8 @@ contract MockCurveTwocryptoOptimized is address _token0, address _token1 ) MockCurveStableSwapMetaNG(_token0, _token1) {} + + function price_oracle() external view returns (uint256) { + return priceOracle; + } } diff --git a/packages/contracts/test/diamond/facets/UbiquityPoolFacet.t.sol b/packages/contracts/test/diamond/facets/UbiquityPoolFacet.t.sol index efa3c8ecb..77b93333d 100644 --- a/packages/contracts/test/diamond/facets/UbiquityPoolFacet.t.sol +++ b/packages/contracts/test/diamond/facets/UbiquityPoolFacet.t.sol @@ -119,6 +119,9 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { 1 // answered in round ); + // set ETH/Governance price to 30k in Curve pool mock + curveGovernanceEthPool.updateMockParams(30_000e18); + // set price feed for collateral token ubiquityPoolFacet.setCollateralChainLinkPriceFeed( address(collateralToken), // collateral token address @@ -320,6 +323,50 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { assertEq(dollarPriceUsd, 1_000_000); } + function testGetGovernancePriceUsd_ShouldRevertOnInvalidChainlinkAnswer() + public + { + // set invalid answer from chainlink + ethUsdPriceFeed.updateMockParams( + 1, // round id + 0, // invalid answer + block.timestamp, // started at + block.timestamp, // updated at + 1 // answered in round + ); + + vm.expectRevert("Invalid price"); + ubiquityPoolFacet.getGovernancePriceUsd(); + } + + function testGetGovernancePriceUsd_ShouldRevertIfChainlinkAnswerIsStale() + public + { + // set stale answer from chainlink + collateralTokenPriceFeed.updateMockParams( + 1, // round id + 100_000_000, // answer, 100_000_000 = $1.00 + block.timestamp, // started at + block.timestamp, // updated at + 1 // answered in round + ); + + // wait 1 day + vm.warp(block.timestamp + 1 days); + + vm.expectRevert("Stale data"); + ubiquityPoolFacet.getGovernancePriceUsd(); + } + + function testGetGovernancePriceUsd_ShouldReturnGovernanceTokenPriceInUsd() + public + { + uint256 governancePriceUsd = ubiquityPoolFacet.getGovernancePriceUsd(); + // 1 ETH = $3000, 1 ETH = 30_000 Governance tokens + // Governance token USD price = (1 / 30000) * 3000 = 0.1 + assertEq(governancePriceUsd, 99999); // ~$0.09 + } + function testGetRedeemCollateralBalance_ShouldReturnRedeemCollateralBalance() public { From 535dafb950c8ced166aeec564368f6e7d8ae1261 Mon Sep 17 00:00:00 2001 From: rndquu Date: Fri, 5 Apr 2024 18:24:26 +0300 Subject: [PATCH 5/7] feat: add fractional mints --- .../src/dollar/facets/UbiquityPoolFacet.sol | 14 +- .../src/dollar/interfaces/IUbiquityPool.sol | 15 +- .../src/dollar/libraries/LibUbiquityPool.sol | 72 ++++-- .../test/diamond/DiamondTestSetup.sol | 6 +- .../diamond/facets/UbiquityPoolFacet.t.sol | 224 +++++++++++++++--- 5 files changed, 276 insertions(+), 55 deletions(-) diff --git a/packages/contracts/src/dollar/facets/UbiquityPoolFacet.sol b/packages/contracts/src/dollar/facets/UbiquityPoolFacet.sol index e1699281c..d47df48e4 100644 --- a/packages/contracts/src/dollar/facets/UbiquityPoolFacet.sol +++ b/packages/contracts/src/dollar/facets/UbiquityPoolFacet.sol @@ -118,18 +118,26 @@ contract UbiquityPoolFacet is IUbiquityPool, Modifiers { uint256 collateralIndex, uint256 dollarAmount, uint256 dollarOutMin, - uint256 maxCollateralIn + uint256 maxCollateralIn, + uint256 maxGovernanceIn, + bool isOneToOne ) external nonReentrant - returns (uint256 totalDollarMint, uint256 collateralNeeded) + returns ( + uint256 totalDollarMint, + uint256 collateralNeeded, + uint256 governanceNeeded + ) { return LibUbiquityPool.mintDollar( collateralIndex, dollarAmount, dollarOutMin, - maxCollateralIn + maxCollateralIn, + maxGovernanceIn, + isOneToOne ); } diff --git a/packages/contracts/src/dollar/interfaces/IUbiquityPool.sol b/packages/contracts/src/dollar/interfaces/IUbiquityPool.sol index cf4c2a868..15cf4ef3d 100644 --- a/packages/contracts/src/dollar/interfaces/IUbiquityPool.sol +++ b/packages/contracts/src/dollar/interfaces/IUbiquityPool.sol @@ -122,15 +122,26 @@ interface IUbiquityPool { * @param dollarAmount Amount of dollars to mint * @param dollarOutMin Min amount of dollars to mint (slippage protection) * @param maxCollateralIn Max amount of collateral to send (slippage protection) + * @param maxGovernanceIn Max amount of Governance tokens to send (slippage protection) + * @param isOneToOne Force providing only collateral without Governance tokens * @return totalDollarMint Amount of Dollars minted * @return collateralNeeded Amount of collateral sent to the pool + * @return governanceNeeded Amount of Governance tokens burnt from sender */ function mintDollar( uint256 collateralIndex, uint256 dollarAmount, uint256 dollarOutMin, - uint256 maxCollateralIn - ) external returns (uint256 totalDollarMint, uint256 collateralNeeded); + uint256 maxCollateralIn, + uint256 maxGovernanceIn, + bool isOneToOne + ) + external + returns ( + uint256 totalDollarMint, + uint256 collateralNeeded, + uint256 governanceNeeded + ); /** * @notice Burns redeemable Ubiquity Dollars and sends back 1 USD of collateral token for every 1 Ubiquity Dollar burned diff --git a/packages/contracts/src/dollar/libraries/LibUbiquityPool.sol b/packages/contracts/src/dollar/libraries/LibUbiquityPool.sol index ec24f7342..1cb3be24d 100644 --- a/packages/contracts/src/dollar/libraries/LibUbiquityPool.sol +++ b/packages/contracts/src/dollar/libraries/LibUbiquityPool.sol @@ -461,44 +461,79 @@ library LibUbiquityPool { * @param dollarAmount Amount of dollars to mint * @param dollarOutMin Min amount of dollars to mint (slippage protection) * @param maxCollateralIn Max amount of collateral to send (slippage protection) + * @param maxGovernanceIn Max amount of Governance tokens to send (slippage protection) + * @param isOneToOne Force providing only collateral without Governance tokens * @return totalDollarMint Amount of Dollars minted * @return collateralNeeded Amount of collateral sent to the pool + * @return governanceNeeded Amount of Governance tokens burnt from sender */ function mintDollar( uint256 collateralIndex, uint256 dollarAmount, uint256 dollarOutMin, - uint256 maxCollateralIn + uint256 maxCollateralIn, + uint256 maxGovernanceIn, + bool isOneToOne ) internal collateralEnabled(collateralIndex) - returns (uint256 totalDollarMint, uint256 collateralNeeded) + returns ( + uint256 totalDollarMint, + uint256 collateralNeeded, + uint256 governanceNeeded + ) { - UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage(); - require( - poolStorage.isMintPaused[collateralIndex] == false, + ubiquityPoolStorage().isMintPaused[collateralIndex] == false, "Minting is paused" ); - // prevent unnecessary mints require( - getDollarPriceUsd() >= poolStorage.mintPriceThreshold, + getDollarPriceUsd() >= ubiquityPoolStorage().mintPriceThreshold, "Dollar price too low" ); // update collateral price updateChainLinkCollateralPrice(collateralIndex); - // get amount of collateral for minting Dollars - collateralNeeded = getDollarInCollateral(collateralIndex, dollarAmount); - require(collateralNeeded > 0, "Cannot mint with zero collateral"); + // user forces 1-to-1 override or collateral ratio >= 100% + if ( + isOneToOne || + ubiquityPoolStorage().collateralRatio >= + UBIQUITY_POOL_PRICE_PRECISION + ) { + // get amount of collateral for minting Dollars + collateralNeeded = getDollarInCollateral( + collateralIndex, + dollarAmount + ); + governanceNeeded = 0; + } else if (ubiquityPoolStorage().collateralRatio == 0) { + // collateral ratio is 0%, Dollar tokens can be minted by providing only Governance tokens (i.e. fully algorithmic stablecoin) + collateralNeeded = 0; + governanceNeeded = dollarAmount + .mul(UBIQUITY_POOL_PRICE_PRECISION) + .div(getGovernancePriceUsd()); + } else { + // fractional, user has to provide both collateral and Governance tokens + uint256 dollarForCollateral = dollarAmount + .mul(ubiquityPoolStorage().collateralRatio) + .div(UBIQUITY_POOL_PRICE_PRECISION); + uint256 dollarForGovernance = dollarAmount.sub(dollarForCollateral); + collateralNeeded = getDollarInCollateral( + collateralIndex, + dollarForCollateral + ); + governanceNeeded = dollarForGovernance + .mul(UBIQUITY_POOL_PRICE_PRECISION) + .div(getGovernancePriceUsd()); + } // subtract the minting fee totalDollarMint = dollarAmount .mul( UBIQUITY_POOL_PRICE_PRECISION.sub( - poolStorage.mintingFee[collateralIndex] + ubiquityPoolStorage().mintingFee[collateralIndex] ) ) .div(UBIQUITY_POOL_PRICE_PRECISION); @@ -506,23 +541,26 @@ library LibUbiquityPool { // check slippages require((totalDollarMint >= dollarOutMin), "Dollar slippage"); require((collateralNeeded <= maxCollateralIn), "Collateral slippage"); + require((governanceNeeded <= maxGovernanceIn), "Governance slippage"); // check the pool ceiling require( freeCollateralBalance(collateralIndex).add(collateralNeeded) <= - poolStorage.poolCeilings[collateralIndex], + ubiquityPoolStorage().poolCeilings[collateralIndex], "Pool ceiling" ); - // take collateral first - IERC20(poolStorage.collateralAddresses[collateralIndex]) + // burn Governance tokens from sender and send collateral to the pool + IERC20Ubiquity(LibAppStorage.appStorage().governanceTokenAddress) + .burnFrom(msg.sender, governanceNeeded); + IERC20(ubiquityPoolStorage().collateralAddresses[collateralIndex]) .safeTransferFrom(msg.sender, address(this), collateralNeeded); // mint Dollars - IERC20Ubiquity ubiquityDollarToken = IERC20Ubiquity( - LibAppStorage.appStorage().dollarTokenAddress + IERC20Ubiquity(LibAppStorage.appStorage().dollarTokenAddress).mint( + msg.sender, + totalDollarMint ); - ubiquityDollarToken.mint(msg.sender, totalDollarMint); } /** diff --git a/packages/contracts/test/diamond/DiamondTestSetup.sol b/packages/contracts/test/diamond/DiamondTestSetup.sol index 7073c5ac0..eb94fd64a 100644 --- a/packages/contracts/test/diamond/DiamondTestSetup.sol +++ b/packages/contracts/test/diamond/DiamondTestSetup.sol @@ -446,11 +446,15 @@ abstract contract DiamondTestSetup is DiamondTestHelper, UUPSTestHelper { CREDIT_TOKEN_BURNER_ROLE, address(diamond) ); - // grant diamond token admin rights + // grant diamond Governance token admin and burner rights accessControlFacet.grantRole( GOVERNANCE_TOKEN_MANAGER_ROLE, address(diamond) ); + accessControlFacet.grantRole( + GOVERNANCE_TOKEN_BURNER_ROLE, + address(diamond) + ); // grant diamond token minter rights accessControlFacet.grantRole( STAKING_SHARE_MINTER_ROLE, diff --git a/packages/contracts/test/diamond/facets/UbiquityPoolFacet.t.sol b/packages/contracts/test/diamond/facets/UbiquityPoolFacet.t.sol index 77b93333d..9d7cd0359 100644 --- a/packages/contracts/test/diamond/facets/UbiquityPoolFacet.t.sol +++ b/packages/contracts/test/diamond/facets/UbiquityPoolFacet.t.sol @@ -165,6 +165,8 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { // stop being admin vm.stopPrank(); + // mint 2000 Governance tokens to the user + deal(address(governanceToken), user, 2000e18); // mint 100 collateral tokens to the user collateralToken.mint(address(user), 100e18); // user approves the pool to transfer collateral @@ -186,7 +188,7 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { // user tries to mint Dollars vm.prank(user); vm.expectRevert("Collateral disabled"); - ubiquityPoolFacet.mintDollar(0, 1, 1, 1); + ubiquityPoolFacet.mintDollar(0, 1, 1, 1, 1, false); } function testOnlyAmoMinter_ShouldRevert_IfCalledNoByAmoMinter() public { @@ -263,7 +265,9 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { 0, // collateral index 100e18, // Dollar amount 99e18, // min amount of Dollars to mint - 100e18 // max collateral to send + 100e18, // max collateral to send, + 0, // max Governance tokens to send + false // force 1-to-1 mint (i.e. provide only collateral without Governance tokens) ); uint256 balanceTally = ubiquityPoolFacet.collateralUsdBalance(); @@ -294,7 +298,9 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { 0, // collateral index 100e18, // Dollar amount 99e18, // min amount of Dollars to mint - 100e18 // max collateral to send + 100e18, // max collateral to send + 0, // max Governance tokens to send + false // force 1-to-1 mint (i.e. provide only collateral without Governance tokens) ); // user redeems 99 Dollars for 97.02 (accounts for 2% redemption fee) collateral tokens @@ -382,7 +388,9 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { 0, // collateral index 100e18, // Dollar amount 99e18, // min amount of Dollars to mint - 100e18 // max collateral to send + 100e18, // max collateral to send + 0, // max Governance tokens to send + false // force 1-to-1 mint (i.e. provide only collateral without Governance tokens) ); // user redeems 99 Dollars for 97.02 (accounts for 2% redemption fee) collateral tokens @@ -421,7 +429,9 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { 0, // collateral index 100e18, // Dollar amount 90e18, // min amount of Dollars to mint - 100e18 // max collateral to send + 100e18, // max collateral to send + 0, // max Governance tokens to send + false // force 1-to-1 mint (i.e. provide only collateral without Governance tokens) ); } @@ -432,32 +442,32 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { 0, // collateral index 100e18, // Dollar amount 90e18, // min amount of Dollars to mint - 100e18 // max collateral to send + 100e18, // max collateral to send + 0, // max Governance tokens to send + false // force 1-to-1 mint (i.e. provide only collateral without Governance tokens) ); } - function testMintDollar_ShouldRevert_IfZeroCollateralAvailable() public { + function testMintDollar_ShouldRevert_OnDollarAmountSlippage() public { vm.prank(admin); ubiquityPoolFacet.setPriceThresholds( 1000000, // mint threshold - 1000000 // redeem threshold + 990000 // redeem threshold ); - // reset collateral fees to 0 - vm.prank(admin); - ubiquityPoolFacet.setFees(0, 0, 0); - // user tries to mint with zero collateral vm.prank(user); - vm.expectRevert("Cannot mint with zero collateral"); + vm.expectRevert("Dollar slippage"); ubiquityPoolFacet.mintDollar( 0, // collateral index - 0, // Dollar amount + 100e18, // Dollar amount 100e18, // min amount of Dollars to mint - 0 // max collateral to send + 100e18, // max collateral to send + 0, // max Governance tokens to send + false // force 1-to-1 mint (i.e. provide only collateral without Governance tokens) ); } - function testMintDollar_ShouldRevert_OnDollarAmountSlippage() public { + function testMintDollar_ShouldRevert_OnCollateralAmountSlippage() public { vm.prank(admin); ubiquityPoolFacet.setPriceThresholds( 1000000, // mint threshold @@ -465,29 +475,37 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { ); vm.prank(user); - vm.expectRevert("Dollar slippage"); + vm.expectRevert("Collateral slippage"); ubiquityPoolFacet.mintDollar( 0, // collateral index 100e18, // Dollar amount - 100e18, // min amount of Dollars to mint - 100e18 // max collateral to send + 90e18, // min amount of Dollars to mint + 10e18, // max collateral to send + 0, // max Governance tokens to send + false // force 1-to-1 mint (i.e. provide only collateral without Governance tokens) ); } - function testMintDollar_ShouldRevert_OnCollateralAmountSlippage() public { + function testMintDollar_ShouldRevert_OnGovernanceAmountSlippage() public { vm.prank(admin); ubiquityPoolFacet.setPriceThresholds( 1000000, // mint threshold 990000 // redeem threshold ); + // admin sets collateral ratio to 0% + vm.prank(admin); + ubiquityPoolFacet.setCollateralRatio(0); + vm.prank(user); - vm.expectRevert("Collateral slippage"); + vm.expectRevert("Governance slippage"); ubiquityPoolFacet.mintDollar( 0, // collateral index 100e18, // Dollar amount 90e18, // min amount of Dollars to mint - 10e18 // max collateral to send + 10e18, // max collateral to send + 0, // max Governance tokens to send + false // force 1-to-1 mint (i.e. provide only collateral without Governance tokens) ); } @@ -504,35 +522,167 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { 0, // collateral index 60_000e18, // Dollar amount 59_000e18, // min amount of Dollars to mint - 60_000e18 // max collateral to send + 60_000e18, // max collateral to send + 0, // max Governance tokens to send + false // force 1-to-1 mint (i.e. provide only collateral without Governance tokens) ); } - function testMintDollar_ShouldMintDollars() public { + function testMintDollar_ShouldMintDollars_IfUserForcesOneToOneOverride() + public + { vm.prank(admin); ubiquityPoolFacet.setPriceThresholds( 1000000, // mint threshold 990000 // redeem threshold ); + // admin sets collateral ratio to 0% + vm.prank(admin); + ubiquityPoolFacet.setCollateralRatio(0); + // balances before assertEq(collateralToken.balanceOf(address(ubiquityPoolFacet)), 0); assertEq(dollarToken.balanceOf(user), 0); + assertEq(governanceToken.balanceOf(user), 2000e18); vm.prank(user); - (uint256 totalDollarMint, uint256 collateralNeeded) = ubiquityPoolFacet - .mintDollar( + ( + uint256 totalDollarMint, + uint256 collateralNeeded, + uint256 governanceNeeded + ) = ubiquityPoolFacet.mintDollar( 0, // collateral index 100e18, // Dollar amount 99e18, // min amount of Dollars to mint - 100e18 // max collateral to send + 100e18, // max collateral to send + 1100e18, // max Governance tokens to send + true // force 1-to-1 mint (i.e. provide only collateral without Governance tokens) ); + assertEq(totalDollarMint, 99e18); assertEq(collateralNeeded, 100e18); + assertEq(governanceNeeded, 0); // balances after assertEq(collateralToken.balanceOf(address(ubiquityPoolFacet)), 100e18); assertEq(dollarToken.balanceOf(user), 99e18); + assertEq(governanceToken.balanceOf(user), 2000e18); + } + + function testMintDollar_ShouldMintDollars_IfCollateralRatioIs100() public { + vm.prank(admin); + ubiquityPoolFacet.setPriceThresholds( + 1000000, // mint threshold + 990000 // redeem threshold + ); + + // balances before + assertEq(collateralToken.balanceOf(address(ubiquityPoolFacet)), 0); + assertEq(dollarToken.balanceOf(user), 0); + assertEq(governanceToken.balanceOf(user), 2000e18); + + vm.prank(user); + ( + uint256 totalDollarMint, + uint256 collateralNeeded, + uint256 governanceNeeded + ) = ubiquityPoolFacet.mintDollar( + 0, // collateral index + 100e18, // Dollar amount + 99e18, // min amount of Dollars to mint + 100e18, // max collateral to send + 0, // max Governance tokens to send + false // force 1-to-1 mint (i.e. provide only collateral without Governance tokens) + ); + assertEq(totalDollarMint, 99e18); + assertEq(collateralNeeded, 100e18); + assertEq(governanceNeeded, 0); + + // balances after + assertEq(collateralToken.balanceOf(address(ubiquityPoolFacet)), 100e18); + assertEq(dollarToken.balanceOf(user), 99e18); + assertEq(governanceToken.balanceOf(user), 2000e18); + } + + function testMintDollar_ShouldMintDollars_IfCollateralRatioIs0() public { + vm.prank(admin); + ubiquityPoolFacet.setPriceThresholds( + 1000000, // mint threshold + 990000 // redeem threshold + ); + + // admin sets collateral ratio to 0% + vm.prank(admin); + ubiquityPoolFacet.setCollateralRatio(0); + + // balances before + assertEq(collateralToken.balanceOf(address(ubiquityPoolFacet)), 0); + assertEq(dollarToken.balanceOf(user), 0); + assertEq(governanceToken.balanceOf(user), 2000e18); + + vm.prank(user); + ( + uint256 totalDollarMint, + uint256 collateralNeeded, + uint256 governanceNeeded + ) = ubiquityPoolFacet.mintDollar( + 0, // collateral index + 100e18, // Dollar amount + 99e18, // min amount of Dollars to mint + 100e18, // max collateral to send + 1100e18, // max Governance tokens to send + false // force 1-to-1 mint (i.e. provide only collateral without Governance tokens) + ); + + assertEq(totalDollarMint, 99e18); + assertEq(collateralNeeded, 0); + assertEq(governanceNeeded, 1000010000100001000010); // ~1000.01 = 100 Dollar * $0.1 Governance from oracle + + // balances after + assertEq(collateralToken.balanceOf(address(ubiquityPoolFacet)), 0); + assertEq(dollarToken.balanceOf(user), 99e18); + assertEq(governanceToken.balanceOf(user), 2000e18 - governanceNeeded); + } + + function testMintDollar_ShouldMintDollars_IfCollateralRatioIs95() public { + vm.prank(admin); + ubiquityPoolFacet.setPriceThresholds( + 1000000, // mint threshold + 990000 // redeem threshold + ); + + // admin sets collateral ratio to 95% + vm.prank(admin); + ubiquityPoolFacet.setCollateralRatio(950_000); + + // balances before + assertEq(collateralToken.balanceOf(address(ubiquityPoolFacet)), 0); + assertEq(dollarToken.balanceOf(user), 0); + assertEq(governanceToken.balanceOf(user), 2000e18); + + vm.prank(user); + ( + uint256 totalDollarMint, + uint256 collateralNeeded, + uint256 governanceNeeded + ) = ubiquityPoolFacet.mintDollar( + 0, // collateral index + 100e18, // Dollar amount + 99e18, // min amount of Dollars to mint + 100e18, // max collateral to send + 1100e18, // max Governance tokens to send + false // force 1-to-1 mint (i.e. provide only collateral without Governance tokens) + ); + + assertEq(totalDollarMint, 99e18); + assertEq(collateralNeeded, 95e18); + assertEq(governanceNeeded, 50000500005000050000); // ~50 Governance tokens = $5 USD / $0.1 Governance from oracle + + // balances after + assertEq(collateralToken.balanceOf(address(ubiquityPoolFacet)), 95e18); + assertEq(dollarToken.balanceOf(user), 99e18); + assertEq(governanceToken.balanceOf(user), 2000e18 - governanceNeeded); } function testRedeemDollar_ShouldRevert_IfRedeemingIsPaused() public { @@ -590,7 +740,9 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { 0, // collateral index 100e18, // Dollar amount 99e18, // min amount of Dollars to mint - 100e18 // max collateral to send + 100e18, // max collateral to send + 0, // max Governance tokens to send + false // force 1-to-1 mint (i.e. provide only collateral without Governance tokens) ); vm.prank(user); @@ -615,7 +767,9 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { 0, // collateral index 100e18, // Dollar amount 99e18, // min amount of Dollars to mint - 100e18 // max collateral to send + 100e18, // max collateral to send + 0, // max Governance tokens to send + false // force 1-to-1 mint (i.e. provide only collateral without Governance tokens) ); // balances before @@ -663,7 +817,9 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { 0, // collateral index 100e18, // Dollar amount 99e18, // min amount of Dollars to mint - 100e18 // max collateral to send + 100e18, // max collateral to send + 0, // max Governance tokens to send + false // force 1-to-1 mint (i.e. provide only collateral without Governance tokens) ); // user redeems 99 Dollars for collateral @@ -705,7 +861,9 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { 0, // collateral index 100e18, // Dollar amount 99e18, // min amount of Dollars to mint - 100e18 // max collateral to send + 100e18, // max collateral to send + 0, // max Governance tokens to send + false // force 1-to-1 mint (i.e. provide only collateral without Governance tokens) ); vm.prank(user); @@ -829,7 +987,9 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { 0, // collateral index 100e18, // Dollar amount 99e18, // min amount of Dollars to mint - 100e18 // max collateral to send + 100e18, // max collateral to send + 0, // max Governance tokens to send + false // force 1-to-1 mint (i.e. provide only collateral without Governance tokens) ); // user redeems 99 Dollars for 97.02 (accounts for 2% redemption fee) collateral tokens From 8b3bfb15b8e465bcf313b668b0ead7c451e9e97b Mon Sep 17 00:00:00 2001 From: rndquu Date: Sat, 6 Apr 2024 01:09:33 +0300 Subject: [PATCH 6/7] feat: add fractional redeems --- .../src/dollar/facets/UbiquityPoolFacet.sol | 15 +- .../src/dollar/interfaces/IUbiquityPool.sol | 13 +- .../src/dollar/libraries/LibUbiquityPool.sol | 71 +++++- .../test/diamond/DiamondTestSetup.sol | 6 +- .../diamond/facets/UbiquityPoolFacet.t.sol | 205 +++++++++++++++++- 5 files changed, 299 insertions(+), 11 deletions(-) diff --git a/packages/contracts/src/dollar/facets/UbiquityPoolFacet.sol b/packages/contracts/src/dollar/facets/UbiquityPoolFacet.sol index d47df48e4..2d191c4cd 100644 --- a/packages/contracts/src/dollar/facets/UbiquityPoolFacet.sol +++ b/packages/contracts/src/dollar/facets/UbiquityPoolFacet.sol @@ -104,6 +104,13 @@ contract UbiquityPoolFacet is IUbiquityPool, Modifiers { ); } + /// @inheritdoc IUbiquityPool + function getRedeemGovernanceBalance( + address userAddress + ) external view returns (uint256) { + return LibUbiquityPool.getRedeemGovernanceBalance(userAddress); + } + /// @inheritdoc IUbiquityPool function governanceEthPoolAddress() external view returns (address) { return LibUbiquityPool.governanceEthPoolAddress(); @@ -145,12 +152,18 @@ contract UbiquityPoolFacet is IUbiquityPool, Modifiers { function redeemDollar( uint256 collateralIndex, uint256 dollarAmount, + uint256 governanceOutMin, uint256 collateralOutMin - ) external nonReentrant returns (uint256 collateralOut) { + ) + external + nonReentrant + returns (uint256 collateralOut, uint256 governanceOut) + { return LibUbiquityPool.redeemDollar( collateralIndex, dollarAmount, + governanceOutMin, collateralOutMin ); } diff --git a/packages/contracts/src/dollar/interfaces/IUbiquityPool.sol b/packages/contracts/src/dollar/interfaces/IUbiquityPool.sol index 15cf4ef3d..25c7f7a6f 100644 --- a/packages/contracts/src/dollar/interfaces/IUbiquityPool.sol +++ b/packages/contracts/src/dollar/interfaces/IUbiquityPool.sol @@ -106,6 +106,15 @@ interface IUbiquityPool { uint256 collateralIndex ) external view returns (uint256); + /** + * @notice Returns user's Governance tokens balance available for redemption + * @param userAddress User address + * @return User's Governance tokens balance available for redemption + */ + function getRedeemGovernanceBalance( + address userAddress + ) external view returns (uint256); + /** * @notice Returns pool address for Governance/ETH pair * @return Pool address @@ -151,14 +160,16 @@ interface IUbiquityPool { * @dev This is done in order to prevent someone using a flash loan of a collateral token to mint, redeem, and collect in a single transaction/block * @param collateralIndex Collateral token index being withdrawn * @param dollarAmount Amount of Ubiquity Dollars being burned + * @param governanceOutMin Minimum amount of Governance tokens that'll be withdrawn, used to set acceptable slippage * @param collateralOutMin Minimum amount of collateral tokens that'll be withdrawn, used to set acceptable slippage * @return collateralOut Amount of collateral tokens ready for redemption */ function redeemDollar( uint256 collateralIndex, uint256 dollarAmount, + uint256 governanceOutMin, uint256 collateralOutMin - ) external returns (uint256 collateralOut); + ) external returns (uint256 collateralOut, uint256 governanceOut); /** * @notice Used to collect collateral tokens after redeeming/burning Ubiquity Dollars diff --git a/packages/contracts/src/dollar/libraries/LibUbiquityPool.sol b/packages/contracts/src/dollar/libraries/LibUbiquityPool.sol index 1cb3be24d..9fff2cf5d 100644 --- a/packages/contracts/src/dollar/libraries/LibUbiquityPool.sol +++ b/packages/contracts/src/dollar/libraries/LibUbiquityPool.sol @@ -70,10 +70,14 @@ library LibUbiquityPool { uint256 redeemPriceThreshold; // address -> collateral index -> balance mapping(address user => mapping(uint256 collateralIndex => uint256 amount)) redeemCollateralBalances; + // address -> balance + mapping(address user => uint256 amount) redeemGovernanceBalances; // number of blocks to wait before being able to collectRedemption() uint256 redemptionDelayBlocks; // collateral index -> balance uint256[] unclaimedPoolCollateral; + // total amount of unclaimed Governance tokens in the pool + uint256 unclaimedPoolGovernance; //================ // Fees related //================ @@ -442,6 +446,18 @@ library LibUbiquityPool { poolStorage.redeemCollateralBalances[userAddress][collateralIndex]; } + /** + * @notice Returns user's Governance tokens balance available for redemption + * @param userAddress User address + * @return User's Governance tokens balance available for redemption + */ + function getRedeemGovernanceBalance( + address userAddress + ) internal view returns (uint256) { + UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage(); + return poolStorage.redeemGovernanceBalances[userAddress]; + } + /** * @notice Returns pool address for Governance/ETH pair * @return Pool address @@ -571,17 +587,19 @@ library LibUbiquityPool { * @dev This is done in order to prevent someone using a flash loan of a collateral token to mint, redeem, and collect in a single transaction/block * @param collateralIndex Collateral token index being withdrawn * @param dollarAmount Amount of Ubiquity Dollars being burned + * @param governanceOutMin Minimum amount of Governance tokens that'll be withdrawn, used to set acceptable slippage * @param collateralOutMin Minimum amount of collateral tokens that'll be withdrawn, used to set acceptable slippage * @return collateralOut Amount of collateral tokens ready for redemption */ function redeemDollar( uint256 collateralIndex, uint256 dollarAmount, + uint256 governanceOutMin, uint256 collateralOutMin ) internal collateralEnabled(collateralIndex) - returns (uint256 collateralOut) + returns (uint256 collateralOut, uint256 governanceOut) { UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage(); @@ -607,8 +625,33 @@ library LibUbiquityPool { // update collateral price updateChainLinkCollateralPrice(collateralIndex); - // get collateral output for incoming Dollars - collateralOut = getDollarInCollateral(collateralIndex, dollarAfterFee); + // get current collateral ratio + uint256 currentCollateralRatio = poolStorage.collateralRatio; + + // fully collateralized + if (currentCollateralRatio >= UBIQUITY_POOL_PRICE_PRECISION) { + // get collateral output for incoming Dollars + collateralOut = getDollarInCollateral( + collateralIndex, + dollarAfterFee + ); + governanceOut = 0; + } else if (currentCollateralRatio == 0) { + // algorithmic, fully covered by Governance tokens + collateralOut = 0; + governanceOut = dollarAfterFee + .mul(UBIQUITY_POOL_PRICE_PRECISION) + .div(getGovernancePriceUsd()); + } else { + // fractional, partially covered by collateral and Governance tokens + collateralOut = getDollarInCollateral( + collateralIndex, + dollarAfterFee + ).mul(currentCollateralRatio).div(UBIQUITY_POOL_PRICE_PRECISION); + governanceOut = dollarAfterFee + .mul(UBIQUITY_POOL_PRICE_PRECISION.sub(currentCollateralRatio)) + .div(getGovernancePriceUsd()); + } // checks require( @@ -619,8 +662,9 @@ library LibUbiquityPool { "Insufficient pool collateral" ); require(collateralOut >= collateralOutMin, "Collateral slippage"); + require(governanceOut >= governanceOutMin, "Governance slippage"); - // account for the redeem delay + // increase collateral redemption balances poolStorage.redeemCollateralBalances[msg.sender][ collateralIndex ] = poolStorage @@ -631,13 +675,26 @@ library LibUbiquityPool { .unclaimedPoolCollateral[collateralIndex] .add(collateralOut); + // increase Governance redemption balances + poolStorage.redeemGovernanceBalances[msg.sender] = poolStorage + .redeemGovernanceBalances[msg.sender] + .add(governanceOut); + poolStorage.unclaimedPoolGovernance = poolStorage + .unclaimedPoolGovernance + .add(governanceOut); + poolStorage.lastRedeemedBlock[msg.sender] = block.number; // burn Dollars - IERC20Ubiquity ubiquityDollarToken = IERC20Ubiquity( - LibAppStorage.appStorage().dollarTokenAddress + IERC20Ubiquity(LibAppStorage.appStorage().dollarTokenAddress).burnFrom( + msg.sender, + dollarAmount + ); + // mint Governance tokens to this address + IERC20Ubiquity(LibAppStorage.appStorage().governanceTokenAddress).mint( + address(this), + governanceOut ); - ubiquityDollarToken.burnFrom(msg.sender, dollarAmount); } /** diff --git a/packages/contracts/test/diamond/DiamondTestSetup.sol b/packages/contracts/test/diamond/DiamondTestSetup.sol index eb94fd64a..c15c8edfa 100644 --- a/packages/contracts/test/diamond/DiamondTestSetup.sol +++ b/packages/contracts/test/diamond/DiamondTestSetup.sol @@ -446,11 +446,15 @@ abstract contract DiamondTestSetup is DiamondTestHelper, UUPSTestHelper { CREDIT_TOKEN_BURNER_ROLE, address(diamond) ); - // grant diamond Governance token admin and burner rights + // grant diamond Governance token admin, minter and burner rights accessControlFacet.grantRole( GOVERNANCE_TOKEN_MANAGER_ROLE, address(diamond) ); + accessControlFacet.grantRole( + GOVERNANCE_TOKEN_MINTER_ROLE, + address(diamond) + ); accessControlFacet.grantRole( GOVERNANCE_TOKEN_BURNER_ROLE, address(diamond) diff --git a/packages/contracts/test/diamond/facets/UbiquityPoolFacet.t.sol b/packages/contracts/test/diamond/facets/UbiquityPoolFacet.t.sol index 9d7cd0359..7bc67a450 100644 --- a/packages/contracts/test/diamond/facets/UbiquityPoolFacet.t.sol +++ b/packages/contracts/test/diamond/facets/UbiquityPoolFacet.t.sol @@ -308,6 +308,7 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { ubiquityPoolFacet.redeemDollar( 0, // collateral index 99e18, // Dollar amount + 0, // min Governance out 90e18 // min collateral out ); @@ -398,6 +399,7 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { ubiquityPoolFacet.redeemDollar( 0, // collateral index 99e18, // Dollar amount + 0, // min Governance out 90e18 // min collateral out ); @@ -406,6 +408,45 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { assertEq(redeemCollateralBalance, 97.02e18); } + function testGetRedeemGovernanceBalance_ShouldReturnRedeemGovernanceBalance() + public + { + vm.prank(admin); + ubiquityPoolFacet.setPriceThresholds( + 1000000, // mint threshold + 1000000 // redeem threshold + ); + + // admin sets collateral ratio to 0% + vm.prank(admin); + ubiquityPoolFacet.setCollateralRatio(0); + + // user burns 1000 Governance tokens and gets 99 Dollars (-1% mint fee) + vm.prank(user); + ubiquityPoolFacet.mintDollar( + 0, // collateral index + 100e18, // Dollar amount + 99e18, // min amount of Dollars to mint + 0, // max collateral to send + 1100e18, // max Governance tokens to send + false // force 1-to-1 mint (i.e. provide only collateral without Governance tokens) + ); + + // user redeems 99 Dollars + vm.prank(user); + ubiquityPoolFacet.redeemDollar( + 0, // collateral index + 99e18, // Dollar amount + 0, // min Governance out + 0 // min collateral out + ); + + assertEq( + ubiquityPoolFacet.getRedeemGovernanceBalance(user), + 970209702097020970209 + ); + } + function testGovernanceEthPoolAddress_ShouldReturnGovernanceEthPoolAddress() public { @@ -695,6 +736,7 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { ubiquityPoolFacet.redeemDollar( 0, // collateral index 100e18, // Dollar amount + 0, // min Governance out 90e18 // min collateral out ); } @@ -705,6 +747,7 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { ubiquityPoolFacet.redeemDollar( 0, // collateral index 100e18, // Dollar amount + 0, // min Governance out 90e18 // min collateral out ); } @@ -723,6 +766,7 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { ubiquityPoolFacet.redeemDollar( 0, // collateral index 100e18, // Dollar amount + 0, // min Governance out 90e18 // min collateral out ); } @@ -750,11 +794,46 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { ubiquityPoolFacet.redeemDollar( 0, // collateral index 100e18, // Dollar amount + 0, // min Governance out 100e18 // min collateral out ); } - function testRedeemDollar_ShouldRedeemCollateral() public { + function testRedeemDollar_ShouldRevert_OnGovernanceSlippage() public { + vm.prank(admin); + ubiquityPoolFacet.setPriceThresholds( + 1000000, // mint threshold + 1000000 // redeem threshold + ); + + // admin sets collateral ratio to 0% + vm.prank(admin); + ubiquityPoolFacet.setCollateralRatio(0); + + // user burns ~1000 Governance tokens and gets 99 Dollars (-1% mint fee) + vm.prank(user); + ubiquityPoolFacet.mintDollar( + 0, // collateral index + 100e18, // Dollar amount + 99e18, // min amount of Dollars to mint + 0, // max collateral to send + 1100e18, // max Governance tokens to send + false // force 1-to-1 mint (i.e. provide only collateral without Governance tokens) + ); + + vm.prank(user); + vm.expectRevert("Governance slippage"); + ubiquityPoolFacet.redeemDollar( + 0, // collateral index + 99e18, // Dollar amount + 1100e18, // min Governance out + 0 // min collateral out + ); + } + + function testRedeemDollar_ShouldRedeemCollateral_IfCollateralRatioIs100() + public + { vm.prank(admin); ubiquityPoolFacet.setPriceThresholds( 1000000, // mint threshold @@ -774,16 +853,137 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { // balances before assertEq(dollarToken.balanceOf(user), 99e18); + assertEq(governanceToken.balanceOf(user), 2000e18); + assertEq(governanceToken.balanceOf(address(ubiquityPoolFacet)), 0); + assertEq(ubiquityPoolFacet.getRedeemCollateralBalance(user, 0), 0); + assertEq(ubiquityPoolFacet.getRedeemGovernanceBalance(user), 0); vm.prank(user); ubiquityPoolFacet.redeemDollar( 0, // collateral index 99e18, // Dollar amount + 0, // min Governance out 90e18 // min collateral out ); // balances after assertEq(dollarToken.balanceOf(user), 0); + assertEq(governanceToken.balanceOf(user), 2000e18); + assertEq(governanceToken.balanceOf(address(ubiquityPoolFacet)), 0); + assertEq( + ubiquityPoolFacet.getRedeemCollateralBalance(user, 0), + 97.02 ether + ); + assertEq(ubiquityPoolFacet.getRedeemGovernanceBalance(user), 0); + } + + function testRedeemDollar_ShouldRedeemCollateral_IfCollateralRatioIs0() + public + { + vm.prank(admin); + ubiquityPoolFacet.setPriceThresholds( + 1000000, // mint threshold + 1000000 // redeem threshold + ); + + // admin sets collateral ratio to 0% + vm.prank(admin); + ubiquityPoolFacet.setCollateralRatio(0); + + // user burns 1000 Governance tokens and gets 99 Dollars (-1% mint fee) + vm.prank(user); + ubiquityPoolFacet.mintDollar( + 0, // collateral index + 100e18, // Dollar amount + 99e18, // min amount of Dollars to mint + 0, // max collateral to send + 1100e18, // max Governance tokens to send + false // force 1-to-1 mint (i.e. provide only collateral without Governance tokens) + ); + + // balances before + assertEq(dollarToken.balanceOf(user), 99e18); + assertEq(governanceToken.balanceOf(user), 999989999899998999990); + assertEq(governanceToken.balanceOf(address(ubiquityPoolFacet)), 0); + assertEq(ubiquityPoolFacet.getRedeemCollateralBalance(user, 0), 0); + assertEq(ubiquityPoolFacet.getRedeemGovernanceBalance(user), 0); + + vm.prank(user); + ubiquityPoolFacet.redeemDollar( + 0, // collateral index + 99e18, // Dollar amount + 0, // min Governance out + 0 // min collateral out + ); + + // balances after + assertEq(dollarToken.balanceOf(user), 0); + assertEq(governanceToken.balanceOf(user), 999989999899998999990); + assertEq( + governanceToken.balanceOf(address(ubiquityPoolFacet)), + 970209702097020970209 + ); + assertEq(ubiquityPoolFacet.getRedeemCollateralBalance(user, 0), 0); + assertEq( + ubiquityPoolFacet.getRedeemGovernanceBalance(user), + 970209702097020970209 + ); + } + + function testRedeemDollar_ShouldRedeemCollateral_IfCollateralRatioIs95() + public + { + vm.prank(admin); + ubiquityPoolFacet.setPriceThresholds( + 1000000, // mint threshold + 1000000 // redeem threshold + ); + + // admin sets collateral ratio to 95% + vm.prank(admin); + ubiquityPoolFacet.setCollateralRatio(950_000); + + // user burns 50 Governance tokens (worth $0.1) + 95 collateral tokens and gets 99 Dollars (-1% mint fee) + vm.prank(user); + ubiquityPoolFacet.mintDollar( + 0, // collateral index + 100e18, // Dollar amount + 99e18, // min amount of Dollars to mint + 100e18, // max collateral to send + 1100e18, // max Governance tokens to send + false // force 1-to-1 mint (i.e. provide only collateral without Governance tokens) + ); + + // balances before + assertEq(dollarToken.balanceOf(user), 99e18); + assertEq(governanceToken.balanceOf(user), 1949999499994999950000); // ~1950 + assertEq(governanceToken.balanceOf(address(ubiquityPoolFacet)), 0); + assertEq(ubiquityPoolFacet.getRedeemCollateralBalance(user, 0), 0); + assertEq(ubiquityPoolFacet.getRedeemGovernanceBalance(user), 0); + + vm.prank(user); + ubiquityPoolFacet.redeemDollar( + 0, // collateral index + 99e18, // Dollar amount + 0, // min Governance out + 0 // min collateral out + ); + + // balances after + assertEq(dollarToken.balanceOf(user), 0); + assertEq(governanceToken.balanceOf(user), 1949999499994999950000); // ~1950 + assertEq( + governanceToken.balanceOf(address(ubiquityPoolFacet)), + 48510485104851048510 + ); // ~48.5 + assertEq( + ubiquityPoolFacet.getRedeemCollateralBalance(user, 0), + 92169000000000000000 + ); // ~92 + assertEq( + ubiquityPoolFacet.getRedeemGovernanceBalance(user), + 48510485104851048510 + ); // ~48.5 } function testCollectRedemption_ShouldRevert_IfRedeemingIsPaused() public { @@ -827,6 +1027,7 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { ubiquityPoolFacet.redeemDollar( 0, // collateral index 99e18, // Dollar amount + 0, // min Governance out 90e18 // min collateral out ); @@ -870,6 +1071,7 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { ubiquityPoolFacet.redeemDollar( 0, // collateral index 99e18, // Dollar amount + 0, // min Governance out 90e18 // min collateral out ); @@ -997,6 +1199,7 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { ubiquityPoolFacet.redeemDollar( 0, // collateral index 99e18, // Dollar amount + 0, // min Governance out 90e18 // min collateral out ); From a9cd4a635e0c9286d306dc5bdd629f018532b277 Mon Sep 17 00:00:00 2001 From: rndquu Date: Sat, 6 Apr 2024 01:45:44 +0300 Subject: [PATCH 7/7] feat: add fractional redeem collection --- .../src/dollar/facets/UbiquityPoolFacet.sol | 6 +++- .../src/dollar/interfaces/IUbiquityPool.sol | 3 +- .../src/dollar/libraries/LibUbiquityPool.sol | 21 ++++++++++-- .../diamond/facets/UbiquityPoolFacet.t.sol | 33 +++++++++++++------ 4 files changed, 48 insertions(+), 15 deletions(-) diff --git a/packages/contracts/src/dollar/facets/UbiquityPoolFacet.sol b/packages/contracts/src/dollar/facets/UbiquityPoolFacet.sol index 2d191c4cd..6f9b7b058 100644 --- a/packages/contracts/src/dollar/facets/UbiquityPoolFacet.sol +++ b/packages/contracts/src/dollar/facets/UbiquityPoolFacet.sol @@ -171,7 +171,11 @@ contract UbiquityPoolFacet is IUbiquityPool, Modifiers { /// @inheritdoc IUbiquityPool function collectRedemption( uint256 collateralIndex - ) external nonReentrant returns (uint256 collateralAmount) { + ) + external + nonReentrant + returns (uint256 governanceAmount, uint256 collateralAmount) + { return LibUbiquityPool.collectRedemption(collateralIndex); } diff --git a/packages/contracts/src/dollar/interfaces/IUbiquityPool.sol b/packages/contracts/src/dollar/interfaces/IUbiquityPool.sol index 25c7f7a6f..aa41b7e29 100644 --- a/packages/contracts/src/dollar/interfaces/IUbiquityPool.sol +++ b/packages/contracts/src/dollar/interfaces/IUbiquityPool.sol @@ -178,11 +178,12 @@ interface IUbiquityPool { * @dev 2. `collectRedemption()` * @dev This is done in order to prevent someone using a flash loan of a collateral token to mint, redeem, and collect in a single transaction/block * @param collateralIndex Collateral token index being collected + * @return governanceAmount Amount of Governance tokens redeemed * @return collateralAmount Amount of collateral tokens redeemed */ function collectRedemption( uint256 collateralIndex - ) external returns (uint256 collateralAmount); + ) external returns (uint256 governanceAmount, uint256 collateralAmount); /** * @notice Updates collateral token price in USD from ChainLink price feed diff --git a/packages/contracts/src/dollar/libraries/LibUbiquityPool.sol b/packages/contracts/src/dollar/libraries/LibUbiquityPool.sol index 9fff2cf5d..ae736accf 100644 --- a/packages/contracts/src/dollar/libraries/LibUbiquityPool.sol +++ b/packages/contracts/src/dollar/libraries/LibUbiquityPool.sol @@ -698,12 +698,13 @@ library LibUbiquityPool { } /** - * @notice Used to collect collateral tokens after redeeming/burning Ubiquity Dollars + * @notice Used to collect collateral and Governance tokens after redeeming/burning Ubiquity Dollars * @dev Redeem process is split in two steps: * @dev 1. `redeemDollar()` * @dev 2. `collectRedemption()` * @dev This is done in order to prevent someone using a flash loan of a collateral token to mint, redeem, and collect in a single transaction/block * @param collateralIndex Collateral token index being collected + * @return governanceAmount Amount of Governance tokens redeemed * @return collateralAmount Amount of collateral tokens redeemed */ function collectRedemption( @@ -711,7 +712,7 @@ library LibUbiquityPool { ) internal collateralEnabled(collateralIndex) - returns (uint256 collateralAmount) + returns (uint256 governanceAmount, uint256 collateralAmount) { UbiquityPoolStorage storage poolStorage = ubiquityPoolStorage(); @@ -728,8 +729,18 @@ library LibUbiquityPool { "Too soon to collect redemption" ); + bool sendGovernance = false; bool sendCollateral = false; + if (poolStorage.redeemGovernanceBalances[msg.sender] > 0) { + governanceAmount = poolStorage.redeemGovernanceBalances[msg.sender]; + poolStorage.redeemGovernanceBalances[msg.sender] = 0; + poolStorage.unclaimedPoolGovernance = poolStorage + .unclaimedPoolGovernance + .sub(governanceAmount); + sendGovernance = true; + } + if ( poolStorage.redeemCollateralBalances[msg.sender][collateralIndex] > 0 @@ -746,7 +757,11 @@ library LibUbiquityPool { sendCollateral = true; } - // send out the tokens + // send out tokens + if (sendGovernance) { + IERC20(LibAppStorage.appStorage().governanceTokenAddress) + .safeTransfer(msg.sender, governanceAmount); + } if (sendCollateral) { IERC20(poolStorage.collateralAddresses[collateralIndex]) .safeTransfer(msg.sender, collateralAmount); diff --git a/packages/contracts/test/diamond/facets/UbiquityPoolFacet.t.sol b/packages/contracts/test/diamond/facets/UbiquityPoolFacet.t.sol index 7bc67a450..ae481d082 100644 --- a/packages/contracts/test/diamond/facets/UbiquityPoolFacet.t.sol +++ b/packages/contracts/test/diamond/facets/UbiquityPoolFacet.t.sol @@ -1011,18 +1011,22 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { 1000000 // redeem threshold ); - // user sends 100 collateral tokens and gets 99 Dollars (-1% mint fee) + // admin sets collateral ratio to 95% + vm.prank(admin); + ubiquityPoolFacet.setCollateralRatio(950_000); + + // user burns 50 Governance tokens (worth $0.1) + 95 collateral tokens and gets 99 Dollars (-1% mint fee) vm.prank(user); ubiquityPoolFacet.mintDollar( 0, // collateral index 100e18, // Dollar amount 99e18, // min amount of Dollars to mint 100e18, // max collateral to send - 0, // max Governance tokens to send + 1100e18, // max Governance tokens to send false // force 1-to-1 mint (i.e. provide only collateral without Governance tokens) ); - // user redeems 99 Dollars for collateral + // user redeems 99 Dollars for collateral and Governance tokens vm.prank(user); ubiquityPoolFacet.redeemDollar( 0, // collateral index @@ -1035,19 +1039,28 @@ contract UbiquityPoolFacetTest is DiamondTestSetup { vm.roll(block.number + 3); // balances before - assertEq(collateralToken.balanceOf(address(ubiquityPoolFacet)), 100e18); - assertEq(collateralToken.balanceOf(user), 0); + assertEq(collateralToken.balanceOf(address(ubiquityPoolFacet)), 95e18); + assertEq(collateralToken.balanceOf(user), 5e18); + assertEq( + governanceToken.balanceOf(address(ubiquityPoolFacet)), + 48510485104851048510 + ); // ~48 + assertEq(governanceToken.balanceOf(user), 1949999499994999950000); // ~1950 vm.prank(user); - uint256 collateralAmount = ubiquityPoolFacet.collectRedemption(0); - assertEq(collateralAmount, 97.02e18); // $99 - 2% redemption fee + (uint256 governanceAmount, uint256 collateralAmount) = ubiquityPoolFacet + .collectRedemption(0); + assertEq(governanceAmount, 48510485104851048510); // ~48 + assertEq(collateralAmount, 92169000000000000000); // ~92 = $95 - 2% redemption fee // balances after assertEq( collateralToken.balanceOf(address(ubiquityPoolFacet)), - 2.98e18 - ); - assertEq(collateralToken.balanceOf(user), 97.02e18); + 2.831 ether + ); // redemption fee left in the pool + assertEq(collateralToken.balanceOf(user), 97.169 ether); + assertEq(governanceToken.balanceOf(address(ubiquityPoolFacet)), 0); + assertEq(governanceToken.balanceOf(user), 1998509985099850998510); // ~1998 } function testCollectRedemption_ShouldRevert_IfCollateralDisabled() public {