From 9e0d5fc10f5993dda065daa29239c1079c0cadc8 Mon Sep 17 00:00:00 2001 From: green Date: Thu, 22 Aug 2024 00:47:38 +0200 Subject: [PATCH 01/17] test(UbiquityPoolFacet): add invariant tests for collateral token addition and price feed setup --- .../facets/UbiquityPoolFacet.invariant.t.sol | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol diff --git a/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol b/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol new file mode 100644 index 000000000..395af9004 --- /dev/null +++ b/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import "forge-std/Test.sol"; +import {UbiquityPoolFacet} from "../../../../src/dollar/facets/UbiquityPoolFacet.sol"; +import {LibUbiquityPool} from "../../../../src/dollar/libraries/LibUbiquityPool.sol"; +import {MockERC20} from "../../../../src/dollar/mocks/MockERC20.sol"; +import {DiamondTestSetup} from "../../../../test/diamond/DiamondTestSetup.sol"; +import {MockChainLinkFeed} from "../../../../src/dollar/mocks/MockChainLinkFeed.sol"; + +contract UbiquityPoolFacetInvariantTest is DiamondTestSetup { + MockERC20 public collateralToken; + MockChainLinkFeed public collateralTokenPriceFeed; + + function setUp() public override { + super.setUp(); + + vm.startPrank(admin); + + collateralToken = new MockERC20("COLLATERAL", "CLT", 18); + collateralTokenPriceFeed = new MockChainLinkFeed(); + + // add collateral token to the pool + uint256 poolCeiling = 50_000e18; // max 50_000 of collateral tokens is allowed + ubiquityPoolFacet.addCollateralToken( + address(collateralToken), + address(collateralTokenPriceFeed), + poolCeiling + ); + + // set collateral price initial feed mock params + collateralTokenPriceFeed.updateMockParams( + 1, // round id + 100_000_000, // answer, 100_000_000 = $1.00 (chainlink 8 decimals answer is converted to 6 decimals pool price) + 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 + address(collateralTokenPriceFeed), // price feed address + 1 days // price feed staleness threshold in seconds + ); + + ubiquityPoolFacet.toggleCollateral(0); + + vm.stopPrank(); + } + + function invariant_CollateralTokenIsEnabledAndCorrectlyAdded() public { + // Check if the collateral token is correctly added and enabled + LibUbiquityPool.CollateralInformation + memory collateralInfo = ubiquityPoolFacet.collateralInformation( + address(collateralToken) + ); + assertTrue( + collateralInfo.isEnabled, + "Collateral token should be enabled" + ); + assertEq( + collateralInfo.collateralAddress, + address(collateralToken), + "Collateral token address mismatch" + ); + assertEq( + collateralInfo.poolCeiling, + 50_000e18, + "Collateral pool ceiling mismatch" + ); + } + + function invariant_CollateralPriceFeedIsSetCorrectly() public { + // Check if the price feed for the collateral token is set correctly + LibUbiquityPool.CollateralInformation + memory collateralInfo = ubiquityPoolFacet.collateralInformation( + address(collateralToken) + ); + assertEq( + collateralInfo.collateralPriceFeedAddress, + address(collateralTokenPriceFeed), + "Collateral price feed address mismatch" + ); + assertEq( + collateralInfo.collateralPriceFeedStalenessThreshold, + 1 days, + "Collateral price feed staleness threshold mismatch" + ); + } +} From 42d739add070a2ebe56806250b3daaf8b48bb158 Mon Sep 17 00:00:00 2001 From: green Date: Sun, 25 Aug 2024 22:24:09 +0200 Subject: [PATCH 02/17] test(ubiquityPoolFacet): add invariant test for Ubiquity Dollar minting limits with fuzzing --- .../diamond/facets/PoolFacetHandler.sol | 54 ++++++ .../facets/UbiquityPoolFacet.invariant.t.sol | 181 ++++++++++++++---- 2 files changed, 203 insertions(+), 32 deletions(-) create mode 100644 packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol diff --git a/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol b/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol new file mode 100644 index 000000000..b746d31e0 --- /dev/null +++ b/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import "forge-std/Test.sol"; +import {UbiquityPoolFacet} from "../../../../src/dollar/facets/UbiquityPoolFacet.sol"; +import {MockChainLinkFeed} from "../../../../src/dollar/mocks/MockChainLinkFeed.sol"; + +contract PoolFacetHandler is Test { + MockChainLinkFeed collateralTokenPriceFeed; + UbiquityPoolFacet ubiquityPoolFacet; + + constructor( + MockChainLinkFeed _collateralTokenPriceFeed, + UbiquityPoolFacet _ubiquityPoolFacet + ) { + collateralTokenPriceFeed = _collateralTokenPriceFeed; + ubiquityPoolFacet = _ubiquityPoolFacet; + } + + function setCollateralRatio(uint256 newRatio) public { + ubiquityPoolFacet.setCollateralRatio(newRatio); + } + + function updateCollateralPrice(uint256 newPrice) public { + uint256 timestamp = block.timestamp; + + collateralTokenPriceFeed.updateMockParams( + 1, + int256(newPrice), + timestamp, + timestamp, + 1 + ); + + ubiquityPoolFacet.updateChainLinkCollateralPrice(0); + } + + function mintUbiquityDollars( + uint256 dollarAmount, + uint256 dollarOutMin, + uint256 maxCollateralIn, + uint256 maxGovernanceIn, + bool isOneToOne + ) public { + ubiquityPoolFacet.mintDollar( + 0, + dollarAmount, + dollarOutMin, + maxCollateralIn, + maxGovernanceIn, + isOneToOne + ); + } +} diff --git a/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol b/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol index 395af9004..f9471919f 100644 --- a/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol +++ b/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol @@ -7,10 +7,29 @@ import {LibUbiquityPool} from "../../../../src/dollar/libraries/LibUbiquityPool. import {MockERC20} from "../../../../src/dollar/mocks/MockERC20.sol"; import {DiamondTestSetup} from "../../../../test/diamond/DiamondTestSetup.sol"; import {MockChainLinkFeed} from "../../../../src/dollar/mocks/MockChainLinkFeed.sol"; +import {PoolFacetHandler} from "./PoolFacetHandler.sol"; +import {IERC20Ubiquity} from "../../../../src/dollar/interfaces/IERC20Ubiquity.sol"; +import {MockCurveStableSwapNG} from "../../../../src/dollar/mocks/MockCurveStableSwapNG.sol"; +import {MockCurveTwocryptoOptimized} from "../../../../src/dollar/mocks/MockCurveTwocryptoOptimized.sol"; contract UbiquityPoolFacetInvariantTest is DiamondTestSetup { - MockERC20 public collateralToken; - MockChainLinkFeed public collateralTokenPriceFeed; + PoolFacetHandler handler; + + // mock three tokens: collateral token, stable token, wrapped ETH token + MockERC20 collateralToken; + MockERC20 stableToken; + MockERC20 wethToken; + + // mock three ChainLink price feeds, one for each token + MockChainLinkFeed collateralTokenPriceFeed; + MockChainLinkFeed ethUsdPriceFeed; + MockChainLinkFeed stableUsdPriceFeed; + + // mock two curve pools Stablecoin/Dollar and Governance/WETH + MockCurveStableSwapNG curveDollarPlainPool; + MockCurveTwocryptoOptimized curveGovernanceEthPool; + + address user = address(1); function setUp() public override { super.setUp(); @@ -18,7 +37,22 @@ contract UbiquityPoolFacetInvariantTest is DiamondTestSetup { vm.startPrank(admin); collateralToken = new MockERC20("COLLATERAL", "CLT", 18); + wethToken = new MockERC20("WETH", "WETH", 18); + stableToken = new MockERC20("STABLE", "STABLE", 18); + collateralTokenPriceFeed = new MockChainLinkFeed(); + ethUsdPriceFeed = new MockChainLinkFeed(); + stableUsdPriceFeed = new MockChainLinkFeed(); + + curveDollarPlainPool = new MockCurveStableSwapNG( + address(stableToken), + address(dollarToken) + ); + + 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 @@ -37,6 +71,27 @@ contract UbiquityPoolFacetInvariantTest is DiamondTestSetup { 1 // answered in round ); + // set ETH/USD price initial feed mock params + ethUsdPriceFeed.updateMockParams( + 1, // round id + 2000_00000000, // answer, 2000_00000000 = $2000 (8 decimals) + block.timestamp, // started at + block.timestamp, // updated at + 1 // answered in round + ); + + // set stable/USD price feed initial mock params + stableUsdPriceFeed.updateMockParams( + 1, // round id + 100_000_000, // answer, 100_000_000 = $1.00 (8 decimals) + block.timestamp, // started at + block.timestamp, // updated at + 1 // answered in round + ); + + // set ETH/Governance initial price to 20k in Curve pool mock (20k GOV == 1 ETH) + curveGovernanceEthPool.updateMockParams(20_000e18); + // set price feed for collateral token ubiquityPoolFacet.setCollateralChainLinkPriceFeed( address(collateralToken), // collateral token address @@ -44,48 +99,110 @@ contract UbiquityPoolFacetInvariantTest 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 + ); + + // set price feed for stable/USD pair + ubiquityPoolFacet.setStableUsdChainLinkPriceFeed( + address(stableUsdPriceFeed), // price feed address + 1 days // price feed staleness threshold in seconds + ); + + // enable collateral at index 0 ubiquityPoolFacet.toggleCollateral(0); + // set mint and redeem initial fees + ubiquityPoolFacet.setFees( + 0, // collateral index + 10000, // 1% mint fee + 20000 // 2% redeem fee + ); + // set redemption delay to 2 blocks + 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) + ); + + // set Curve plain pool in manager facet + managerFacet.setStableSwapPlainPoolAddress( + address(curveDollarPlainPool) + ); + // stop being admin vm.stopPrank(); + + // mint 2000 Governance tokens to the user + deal(address(governanceToken), user, 2000e18); + // mint 2000 collateral tokens to the user + collateralToken.mint(address(user), 2000e18); + // user approves the pool to transfer collateral + vm.prank(user); + collateralToken.approve(address(ubiquityPoolFacet), 100e18); + + handler = new PoolFacetHandler( + collateralTokenPriceFeed, + ubiquityPoolFacet + ); + targetContract(address(handler)); } - function invariant_CollateralTokenIsEnabledAndCorrectlyAdded() public { - // Check if the collateral token is correctly added and enabled - LibUbiquityPool.CollateralInformation - memory collateralInfo = ubiquityPoolFacet.collateralInformation( - address(collateralToken) + function invariant_PoolCollateralBalanceIsConsistent() public { + uint256 expectedBalance = 0; + + for ( + uint256 i = 0; + i < ubiquityPoolFacet.allCollaterals().length; + i++ + ) { + address collateralAddress = ubiquityPoolFacet.allCollaterals()[i]; + uint256 collateralBalance = MockERC20(collateralAddress).balanceOf( + address(ubiquityPoolFacet) ); - assertTrue( - collateralInfo.isEnabled, - "Collateral token should be enabled" - ); - assertEq( - collateralInfo.collateralAddress, - address(collateralToken), - "Collateral token address mismatch" - ); - assertEq( - collateralInfo.poolCeiling, - 50_000e18, - "Collateral pool ceiling mismatch" - ); + + expectedBalance += collateralBalance; + } + + uint256 actualBalance = ubiquityPoolFacet.collateralUsdBalance(); + + assertEq(expectedBalance, actualBalance, "Collateral balance mismatch"); } - function invariant_CollateralPriceFeedIsSetCorrectly() public { - // Check if the price feed for the collateral token is set correctly + function invariant_CannotMintMoreDollarsThanCollateral() public { + uint256 fuzzedDollarPriceUsd = uint256( + bound( + uint256(keccak256(abi.encodePacked(block.timestamp))), + 90_000_000, + 110_000_000 + ) + ); + LibUbiquityPool.CollateralInformation memory collateralInfo = ubiquityPoolFacet.collateralInformation( address(collateralToken) ); - assertEq( - collateralInfo.collateralPriceFeedAddress, - address(collateralTokenPriceFeed), - "Collateral price feed address mismatch" - ); - assertEq( - collateralInfo.collateralPriceFeedStalenessThreshold, - 1 days, - "Collateral price feed staleness threshold mismatch" + + uint256 collateralBalance = ubiquityPoolFacet.freeCollateralBalance(0); + uint256 collateralPrice = collateralInfo.price; + + uint256 totalCollateralValue = collateralBalance * collateralPrice; + + uint256 totalDollarSupply = IERC20Ubiquity( + managerFacet.dollarTokenAddress() + ).totalSupply(); + + // uint256 dollarPrice = ubiquityPoolFacet.getDollarPriceUsd(); + uint256 totalDollarValue = totalDollarSupply * fuzzedDollarPriceUsd; + + assertTrue( + totalDollarValue <= totalCollateralValue, + "Minted dollars exceed collateral value" ); } } From 66899068ea4604354f9f0becbd7e10855d974cbc Mon Sep 17 00:00:00 2001 From: green Date: Tue, 27 Aug 2024 23:25:07 +0200 Subject: [PATCH 03/17] test(ubiquity-pool-facet): add collateral, ceiling, redeem, and dollar price manipulation tests --- .../diamond/facets/PoolFacetHandler.sol | 113 ++++++++++++++---- .../facets/UbiquityPoolFacet.invariant.t.sol | 5 +- 2 files changed, 92 insertions(+), 26 deletions(-) diff --git a/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol b/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol index b746d31e0..a9776214e 100644 --- a/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol +++ b/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol @@ -2,53 +2,116 @@ pragma solidity 0.8.19; import "forge-std/Test.sol"; +import "forge-std/console.sol"; import {UbiquityPoolFacet} from "../../../../src/dollar/facets/UbiquityPoolFacet.sol"; import {MockChainLinkFeed} from "../../../../src/dollar/mocks/MockChainLinkFeed.sol"; +import {MockCurveStableSwapNG} from "../../../../src/dollar/mocks/MockCurveStableSwapNG.sol"; contract PoolFacetHandler is Test { MockChainLinkFeed collateralTokenPriceFeed; UbiquityPoolFacet ubiquityPoolFacet; + address admin; + address user; + MockCurveStableSwapNG curveDollarPlainPool; + + event MintSuccess(uint256 dollarAmount); + event MintFailed(bytes reason); constructor( MockChainLinkFeed _collateralTokenPriceFeed, - UbiquityPoolFacet _ubiquityPoolFacet + UbiquityPoolFacet _ubiquityPoolFacet, + address _admin, + address _user, + MockCurveStableSwapNG _curveDollarPlainPool ) { collateralTokenPriceFeed = _collateralTokenPriceFeed; ubiquityPoolFacet = _ubiquityPoolFacet; + admin = _admin; + user = _user; + curveDollarPlainPool = _curveDollarPlainPool; } - function setCollateralRatio(uint256 newRatio) public { - ubiquityPoolFacet.setCollateralRatio(newRatio); + // Dollar price manipulations + //======================== + function setDollarPriceAboveThreshold() public { + vm.prank(admin); + curveDollarPlainPool.updateMockParams(1.02e18); } - function updateCollateralPrice(uint256 newPrice) public { - uint256 timestamp = block.timestamp; + function setDollarPriceBelowThreshold() public { + vm.prank(admin); + curveDollarPlainPool.updateMockParams(0.98e18); + } - collateralTokenPriceFeed.updateMockParams( - 1, - int256(newPrice), - timestamp, - timestamp, - 1 - ); + // Redeem manipulations + //======================== + function setMintAndRedeemFees(uint256 mintFee, uint256 redeemFee) public { + vm.prank(admin); + ubiquityPoolFacet.setFees(0, mintFee, redeemFee); + } - ubiquityPoolFacet.updateChainLinkCollateralPrice(0); + function setRedemptionDelay(uint256 delay) public { + vm.prank(admin); + ubiquityPoolFacet.setRedemptionDelayBlocks(delay); } - function mintUbiquityDollars( - uint256 dollarAmount, - uint256 dollarOutMin, - uint256 maxCollateralIn, - uint256 maxGovernanceIn, - bool isOneToOne + function collectRedemption() public { + ubiquityPoolFacet.collectRedemption(0); + } + + function redeemDollar( + uint256 _dollarAmount, + uint256 _governanceOutMin, + uint256 _collateralOutMin ) public { - ubiquityPoolFacet.mintDollar( + vm.prank(user); + ubiquityPoolFacet.redeemDollar( 0, - dollarAmount, - dollarOutMin, - maxCollateralIn, - maxGovernanceIn, - isOneToOne + _dollarAmount, + _governanceOutMin, + _collateralOutMin ); } + + // Ceiling manipulations + //======================== + function setPoolCeiling(uint256 newCeiling) public { + vm.prank(admin); + ubiquityPoolFacet.setPoolCeiling(0, newCeiling); + } + + // Collateral manipulations + //======================== + function updateCollateralRatio(uint256 newRatio) public { + vm.prank(admin); + ubiquityPoolFacet.setCollateralRatio(newRatio); + } + + function mintUbiquityDollars( + uint256 _dollarAmount, + uint256 _dollarOutMin, + uint256 _maxCollateralIn, + uint256 _maxGovernanceIn, + bool _isOneToOne + ) public { + vm.prank(user); + + uint256 dollarPrice = ubiquityPoolFacet.getDollarPriceUsd(); + console.log("::::::: DOLLAR PRICE:", dollarPrice); + + try + ubiquityPoolFacet.mintDollar( + 0, + _dollarAmount, + _dollarOutMin, + _maxCollateralIn, + _maxGovernanceIn, + _isOneToOne + ) + { + emit MintSuccess(_dollarAmount); + } catch (bytes memory reason) { + emit MintFailed(reason); + } + } } diff --git a/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol b/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol index f9471919f..6ebc324f9 100644 --- a/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol +++ b/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol @@ -148,7 +148,10 @@ contract UbiquityPoolFacetInvariantTest is DiamondTestSetup { handler = new PoolFacetHandler( collateralTokenPriceFeed, - ubiquityPoolFacet + ubiquityPoolFacet, + admin, + user, + curveDollarPlainPool ); targetContract(address(handler)); } From 142f779e91e6fe5c5cebf579a9d5b44a10dbb4ad Mon Sep 17 00:00:00 2001 From: green Date: Thu, 29 Aug 2024 21:38:06 +0200 Subject: [PATCH 04/17] test(UbiquityPoolFacetInvariantTest): add collateral price manipulation and update invariant --- .../diamond/facets/PoolFacetHandler.sol | 55 ++++++++++---- .../facets/UbiquityPoolFacet.invariant.t.sol | 71 ++++++++----------- 2 files changed, 70 insertions(+), 56 deletions(-) diff --git a/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol b/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol index a9776214e..bf25d2a2a 100644 --- a/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol +++ b/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol @@ -17,6 +17,9 @@ contract PoolFacetHandler is Test { event MintSuccess(uint256 dollarAmount); event MintFailed(bytes reason); + event redeemSuccess(uint256 dollarAmount); + event redeemFailed(bytes reason); + constructor( MockChainLinkFeed _collateralTokenPriceFeed, UbiquityPoolFacet _ubiquityPoolFacet, @@ -64,13 +67,23 @@ contract PoolFacetHandler is Test { uint256 _governanceOutMin, uint256 _collateralOutMin ) public { + vm.assume(_dollarAmount > 0 && _dollarAmount < type(uint256).max / 2); + vm.assume(_governanceOutMin >= 0 && _governanceOutMin <= _dollarAmount); + vm.assume(_collateralOutMin >= 0 && _collateralOutMin <= _dollarAmount); + vm.prank(user); - ubiquityPoolFacet.redeemDollar( - 0, - _dollarAmount, - _governanceOutMin, - _collateralOutMin - ); + try + ubiquityPoolFacet.redeemDollar( + 0, + _dollarAmount, + _governanceOutMin, + _collateralOutMin + ) + { + emit redeemSuccess(_dollarAmount); + } catch (bytes memory reason) { + emit redeemFailed(reason); + } } // Ceiling manipulations @@ -80,11 +93,20 @@ contract PoolFacetHandler is Test { ubiquityPoolFacet.setPoolCeiling(0, newCeiling); } - // Collateral manipulations + // Collateral price manipulations //======================== - function updateCollateralRatio(uint256 newRatio) public { - vm.prank(admin); - ubiquityPoolFacet.setCollateralRatio(newRatio); + function updateCollateralPrice(int256 _newPrice) public { + vm.assume(_newPrice >= 50_000_000 && _newPrice <= 200_000_000); + + collateralTokenPriceFeed.updateMockParams( + 1, // round id + _newPrice, // new price (8 decimals) + block.timestamp, // started at + block.timestamp, // updated at + 1 // answered in round + ); + + ubiquityPoolFacet.updateChainLinkCollateralPrice(0); } function mintUbiquityDollars( @@ -94,11 +116,16 @@ contract PoolFacetHandler is Test { uint256 _maxGovernanceIn, bool _isOneToOne ) public { - vm.prank(user); - - uint256 dollarPrice = ubiquityPoolFacet.getDollarPriceUsd(); - console.log("::::::: DOLLAR PRICE:", dollarPrice); + vm.assume(_dollarAmount > 0 && _dollarAmount < type(uint256).max / 2); + vm.assume(_dollarOutMin <= _dollarAmount); + vm.assume( + _maxCollateralIn > 0 && _maxCollateralIn < type(uint256).max / 2 + ); + vm.assume( + _maxGovernanceIn >= 0 && _maxGovernanceIn < type(uint256).max / 2 + ); + vm.prank(user); try ubiquityPoolFacet.mintDollar( 0, diff --git a/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol b/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol index 6ebc324f9..c2c6e72fa 100644 --- a/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol +++ b/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol @@ -156,56 +156,43 @@ contract UbiquityPoolFacetInvariantTest is DiamondTestSetup { targetContract(address(handler)); } - function invariant_PoolCollateralBalanceIsConsistent() public { - uint256 expectedBalance = 0; - - for ( - uint256 i = 0; - i < ubiquityPoolFacet.allCollaterals().length; - i++ - ) { - address collateralAddress = ubiquityPoolFacet.allCollaterals()[i]; - uint256 collateralBalance = MockERC20(collateralAddress).balanceOf( - address(ubiquityPoolFacet) - ); - - expectedBalance += collateralBalance; - } - - uint256 actualBalance = ubiquityPoolFacet.collateralUsdBalance(); - - assertEq(expectedBalance, actualBalance, "Collateral balance mismatch"); - } - function invariant_CannotMintMoreDollarsThanCollateral() public { - uint256 fuzzedDollarPriceUsd = uint256( - bound( - uint256(keccak256(abi.encodePacked(block.timestamp))), - 90_000_000, - 110_000_000 - ) - ); - - LibUbiquityPool.CollateralInformation - memory collateralInfo = ubiquityPoolFacet.collateralInformation( - address(collateralToken) - ); - - uint256 collateralBalance = ubiquityPoolFacet.freeCollateralBalance(0); - uint256 collateralPrice = collateralInfo.price; - - uint256 totalCollateralValue = collateralBalance * collateralPrice; - uint256 totalDollarSupply = IERC20Ubiquity( managerFacet.dollarTokenAddress() ).totalSupply(); - // uint256 dollarPrice = ubiquityPoolFacet.getDollarPriceUsd(); - uint256 totalDollarValue = totalDollarSupply * fuzzedDollarPriceUsd; + uint256 collateralUsdBalance = ubiquityPoolFacet.collateralUsdBalance(); + console.log( + ":::::::| UbiquityPoolFacetInvariantTest | collateralUsdBalance:", + collateralUsdBalance + ); + + vm.assume(collateralUsdBalance > 0 && totalDollarSupply > 0); + + uint256 dollarPrice = ubiquityPoolFacet.getDollarPriceUsd(); + uint256 totalDollarSupplyInUsd = (totalDollarSupply * dollarPrice) / + 1e6; + + console.log( + ":::::::| UbiquityPoolFacetInvariantTest | dollarPrice:", + dollarPrice + ); + console.log( + ":::::::| UbiquityPoolFacetInvariantTest | totalDollarSupply:", + totalDollarSupply + ); + console.log( + ":::::::| UbiquityPoolFacetInvariantTest | totalDollarSupplyInUsd:", + totalDollarSupplyInUsd + ); assertTrue( - totalDollarValue <= totalCollateralValue, + totalDollarSupplyInUsd <= collateralUsdBalance, "Minted dollars exceed collateral value" ); + + console.log( + ":::::::| UbiquityPoolFacetInvariantTest11 | Final Statement:" + ); } } From 5407774f410565e1d6a0aec9aee74ffe55911b87 Mon Sep 17 00:00:00 2001 From: green Date: Thu, 29 Aug 2024 21:41:12 +0200 Subject: [PATCH 05/17] test(UbiquityPoolFacetInvariantTest): delete irrelevant logs --- .../diamond/facets/UbiquityPoolFacet.invariant.t.sol | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol b/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol index c2c6e72fa..f1231e9f4 100644 --- a/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol +++ b/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol @@ -190,9 +190,5 @@ contract UbiquityPoolFacetInvariantTest is DiamondTestSetup { totalDollarSupplyInUsd <= collateralUsdBalance, "Minted dollars exceed collateral value" ); - - console.log( - ":::::::| UbiquityPoolFacetInvariantTest11 | Final Statement:" - ); } } From dd94c7e7748e5b26f338d7a6fddc83961f72a92f Mon Sep 17 00:00:00 2001 From: green Date: Sat, 31 Aug 2024 17:08:29 +0200 Subject: [PATCH 06/17] test(lib-ubiquity-pool): add conservative estimates for price and fees --- .../diamond/facets/PoolFacetHandler.sol | 16 +++++++++----- .../facets/UbiquityPoolFacet.invariant.t.sol | 22 +++++-------------- 2 files changed, 15 insertions(+), 23 deletions(-) diff --git a/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol b/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol index bf25d2a2a..a72019d09 100644 --- a/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol +++ b/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol @@ -38,7 +38,8 @@ contract PoolFacetHandler is Test { //======================== function setDollarPriceAboveThreshold() public { vm.prank(admin); - curveDollarPlainPool.updateMockParams(1.02e18); + curveDollarPlainPool.updateMockParams(1.01e18); + // curveDollarPlainPool.updateMockParams(1.02e18); } function setDollarPriceBelowThreshold() public { @@ -48,14 +49,17 @@ contract PoolFacetHandler is Test { // Redeem manipulations //======================== - function setMintAndRedeemFees(uint256 mintFee, uint256 redeemFee) public { + function setRedemptionDelay(uint256 delay) public { vm.prank(admin); - ubiquityPoolFacet.setFees(0, mintFee, redeemFee); + ubiquityPoolFacet.setRedemptionDelayBlocks(delay); } - function setRedemptionDelay(uint256 delay) public { + function setMintAndRedeemFees(uint256 mintFee, uint256 redeemFee) public { + vm.assume(mintFee >= 100000 && mintFee <= 200000); + vm.assume(redeemFee >= 100000 && redeemFee <= 200000); + vm.prank(admin); - ubiquityPoolFacet.setRedemptionDelayBlocks(delay); + ubiquityPoolFacet.setFees(0, mintFee, redeemFee); } function collectRedemption() public { @@ -96,7 +100,7 @@ contract PoolFacetHandler is Test { // Collateral price manipulations //======================== function updateCollateralPrice(int256 _newPrice) public { - vm.assume(_newPrice >= 50_000_000 && _newPrice <= 200_000_000); + vm.assume(_newPrice >= 100_000_000 && _newPrice <= 200_000_000); collateralTokenPriceFeed.updateMockParams( 1, // round id diff --git a/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol b/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol index f1231e9f4..150ed7d34 100644 --- a/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol +++ b/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol @@ -92,6 +92,8 @@ contract UbiquityPoolFacetInvariantTest is DiamondTestSetup { // set ETH/Governance initial price to 20k in Curve pool mock (20k GOV == 1 ETH) curveGovernanceEthPool.updateMockParams(20_000e18); + curveDollarPlainPool.updateMockParams(1.01e18); + // set price feed for collateral token ubiquityPoolFacet.setCollateralChainLinkPriceFeed( address(collateralToken), // collateral token address @@ -153,6 +155,9 @@ contract UbiquityPoolFacetInvariantTest is DiamondTestSetup { user, curveDollarPlainPool ); + + handler.mintUbiquityDollars(1e18, 0.9e18, 1e18, 0, true); + targetContract(address(handler)); } @@ -162,10 +167,6 @@ contract UbiquityPoolFacetInvariantTest is DiamondTestSetup { ).totalSupply(); uint256 collateralUsdBalance = ubiquityPoolFacet.collateralUsdBalance(); - console.log( - ":::::::| UbiquityPoolFacetInvariantTest | collateralUsdBalance:", - collateralUsdBalance - ); vm.assume(collateralUsdBalance > 0 && totalDollarSupply > 0); @@ -173,19 +174,6 @@ contract UbiquityPoolFacetInvariantTest is DiamondTestSetup { uint256 totalDollarSupplyInUsd = (totalDollarSupply * dollarPrice) / 1e6; - console.log( - ":::::::| UbiquityPoolFacetInvariantTest | dollarPrice:", - dollarPrice - ); - console.log( - ":::::::| UbiquityPoolFacetInvariantTest | totalDollarSupply:", - totalDollarSupply - ); - console.log( - ":::::::| UbiquityPoolFacetInvariantTest | totalDollarSupplyInUsd:", - totalDollarSupplyInUsd - ); - assertTrue( totalDollarSupplyInUsd <= collateralUsdBalance, "Minted dollars exceed collateral value" From 989d9c66e2a849a143de8ec3d22ce5b8ab6c697b Mon Sep 17 00:00:00 2001 From: green Date: Sat, 31 Aug 2024 19:10:32 +0200 Subject: [PATCH 07/17] test(pool-facet-handler): add fuzzed parameters to dollar price manipulation tests --- .../diamond/facets/PoolFacetHandler.sol | 31 +++++++++++++++---- .../facets/UbiquityPoolFacet.invariant.t.sol | 24 ++++++++++++++ 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol b/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol index a72019d09..6f1f187ca 100644 --- a/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol +++ b/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol @@ -6,8 +6,11 @@ import "forge-std/console.sol"; import {UbiquityPoolFacet} from "../../../../src/dollar/facets/UbiquityPoolFacet.sol"; import {MockChainLinkFeed} from "../../../../src/dollar/mocks/MockChainLinkFeed.sol"; import {MockCurveStableSwapNG} from "../../../../src/dollar/mocks/MockCurveStableSwapNG.sol"; +import {SafeMath} from "@openzeppelin/contracts/utils/math/SafeMath.sol"; contract PoolFacetHandler is Test { + using SafeMath for uint256; + MockChainLinkFeed collateralTokenPriceFeed; UbiquityPoolFacet ubiquityPoolFacet; address admin; @@ -36,15 +39,28 @@ contract PoolFacetHandler is Test { // Dollar price manipulations //======================== - function setDollarPriceAboveThreshold() public { + function setDollarPriceAboveThreshold(uint256 newDollarPrice) public { + vm.assume(newDollarPrice > 1e18 && newDollarPrice < 2e18); + vm.prank(admin); - curveDollarPlainPool.updateMockParams(1.01e18); - // curveDollarPlainPool.updateMockParams(1.02e18); + curveDollarPlainPool.updateMockParams(newDollarPrice); + + uint256 reductionFactor = newDollarPrice.sub(1e18).div(1e16); + uint256 newCollateralRatio = uint256(1e6).sub(reductionFactor); + + ubiquityPoolFacet.setCollateralRatio(newCollateralRatio); } - function setDollarPriceBelowThreshold() public { + function setDollarPriceBelowThreshold(uint256 newDollarPrice) public { + vm.assume(newDollarPrice >= 0.5e18 && newDollarPrice < 1e18); + vm.prank(admin); - curveDollarPlainPool.updateMockParams(0.98e18); + curveDollarPlainPool.updateMockParams(newDollarPrice); + + uint256 increaseFactor = uint256(1e18).sub(newDollarPrice).div(1e16); + uint256 newCollateralRatio = uint256(1e6).add(increaseFactor); + + ubiquityPoolFacet.setCollateralRatio(newCollateralRatio); } // Redeem manipulations @@ -100,7 +116,7 @@ contract PoolFacetHandler is Test { // Collateral price manipulations //======================== function updateCollateralPrice(int256 _newPrice) public { - vm.assume(_newPrice >= 100_000_000 && _newPrice <= 200_000_000); + vm.assume(_newPrice >= 50_000_000 && _newPrice <= 200_000_000); collateralTokenPriceFeed.updateMockParams( 1, // round id @@ -111,6 +127,9 @@ contract PoolFacetHandler is Test { ); ubiquityPoolFacet.updateChainLinkCollateralPrice(0); + + uint256 newCollateralRatio = uint256(1e6 * 1e8).div(uint256(_newPrice)); + ubiquityPoolFacet.setCollateralRatio(newCollateralRatio); } function mintUbiquityDollars( diff --git a/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol b/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol index 150ed7d34..d6a1148db 100644 --- a/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol +++ b/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol @@ -167,6 +167,10 @@ contract UbiquityPoolFacetInvariantTest is DiamondTestSetup { ).totalSupply(); uint256 collateralUsdBalance = ubiquityPoolFacet.collateralUsdBalance(); + console.log( + ":::::::| UbiquityPoolFacetInvariantTest | collateralUsdBalance:", + collateralUsdBalance + ); vm.assume(collateralUsdBalance > 0 && totalDollarSupply > 0); @@ -174,6 +178,26 @@ contract UbiquityPoolFacetInvariantTest is DiamondTestSetup { uint256 totalDollarSupplyInUsd = (totalDollarSupply * dollarPrice) / 1e6; + uint256 ratio = ubiquityPoolFacet.collateralRatio(); + + console.log( + ":::::::| UbiquityPoolFacetInvariantTest | collateralRatio:", + ratio + ); + + console.log( + ":::::::| UbiquityPoolFacetInvariantTest | dollarPrice:", + dollarPrice + ); + console.log( + ":::::::| UbiquityPoolFacetInvariantTest | totalDollarSupply:", + totalDollarSupply + ); + console.log( + ":::::::| UbiquityPoolFacetInvariantTest11 | totalDollarSupplyInUsd:", + totalDollarSupplyInUsd + ); + assertTrue( totalDollarSupplyInUsd <= collateralUsdBalance, "Minted dollars exceed collateral value" From 1b7f1dadefb05bb5074d525efe77504e63248170 Mon Sep 17 00:00:00 2001 From: green Date: Sat, 31 Aug 2024 20:12:08 +0200 Subject: [PATCH 08/17] test(pool-facet-handler): add usd price manipulations --- .../diamond/facets/PoolFacetHandler.sol | 24 +++++++++++++++++-- .../facets/UbiquityPoolFacet.invariant.t.sol | 16 +------------ 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol b/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol index 6f1f187ca..41f4e3bad 100644 --- a/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol +++ b/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol @@ -12,6 +12,7 @@ contract PoolFacetHandler is Test { using SafeMath for uint256; MockChainLinkFeed collateralTokenPriceFeed; + MockChainLinkFeed stableUsdPriceFeed; UbiquityPoolFacet ubiquityPoolFacet; address admin; address user; @@ -25,12 +26,14 @@ contract PoolFacetHandler is Test { constructor( MockChainLinkFeed _collateralTokenPriceFeed, + MockChainLinkFeed _stableUsdPriceFeed, UbiquityPoolFacet _ubiquityPoolFacet, address _admin, address _user, MockCurveStableSwapNG _curveDollarPlainPool ) { collateralTokenPriceFeed = _collateralTokenPriceFeed; + stableUsdPriceFeed = _stableUsdPriceFeed; ubiquityPoolFacet = _ubiquityPoolFacet; admin = _admin; user = _user; @@ -115,12 +118,12 @@ contract PoolFacetHandler is Test { // Collateral price manipulations //======================== - function updateCollateralPrice(int256 _newPrice) public { + function setCollateralPrice(int256 _newPrice) public { vm.assume(_newPrice >= 50_000_000 && _newPrice <= 200_000_000); collateralTokenPriceFeed.updateMockParams( 1, // round id - _newPrice, // new price (8 decimals) + _newPrice, block.timestamp, // started at block.timestamp, // updated at 1 // answered in round @@ -132,6 +135,23 @@ contract PoolFacetHandler is Test { ubiquityPoolFacet.setCollateralRatio(newCollateralRatio); } + // USD stablecoin price manipulations + //======================== + function setStableUsdPrice(uint256 _newPrice) public { + vm.assume(_newPrice >= 0.5e8 && _newPrice <= 1.5e8); // Assume a range for testing + + stableUsdPriceFeed.updateMockParams( + 1, // round id + int256(_newPrice), + block.timestamp, // started at + block.timestamp, // updated at + 1 // answered in round + ); + + uint256 newCollateralRatio = uint256(1e6 * 1e8).div(_newPrice); + ubiquityPoolFacet.setCollateralRatio(newCollateralRatio); + } + function mintUbiquityDollars( uint256 _dollarAmount, uint256 _dollarOutMin, diff --git a/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol b/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol index d6a1148db..113ec7949 100644 --- a/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol +++ b/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol @@ -150,6 +150,7 @@ contract UbiquityPoolFacetInvariantTest is DiamondTestSetup { handler = new PoolFacetHandler( collateralTokenPriceFeed, + stableUsdPriceFeed, ubiquityPoolFacet, admin, user, @@ -178,21 +179,6 @@ contract UbiquityPoolFacetInvariantTest is DiamondTestSetup { uint256 totalDollarSupplyInUsd = (totalDollarSupply * dollarPrice) / 1e6; - uint256 ratio = ubiquityPoolFacet.collateralRatio(); - - console.log( - ":::::::| UbiquityPoolFacetInvariantTest | collateralRatio:", - ratio - ); - - console.log( - ":::::::| UbiquityPoolFacetInvariantTest | dollarPrice:", - dollarPrice - ); - console.log( - ":::::::| UbiquityPoolFacetInvariantTest | totalDollarSupply:", - totalDollarSupply - ); console.log( ":::::::| UbiquityPoolFacetInvariantTest11 | totalDollarSupplyInUsd:", totalDollarSupplyInUsd From fd8acb602bb8819403688036788f2278b889a84b Mon Sep 17 00:00:00 2001 From: green Date: Mon, 2 Sep 2024 16:05:20 +0200 Subject: [PATCH 09/17] test(pool-facet-handler): eth/usd price manipulations --- .../diamond/facets/PoolFacetHandler.sol | 17 +++++++++++++++++ .../facets/UbiquityPoolFacet.invariant.t.sol | 1 + 2 files changed, 18 insertions(+) diff --git a/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol b/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol index 41f4e3bad..2770f18e2 100644 --- a/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol +++ b/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol @@ -13,6 +13,7 @@ contract PoolFacetHandler is Test { MockChainLinkFeed collateralTokenPriceFeed; MockChainLinkFeed stableUsdPriceFeed; + MockChainLinkFeed ethUsdPriceFeed; UbiquityPoolFacet ubiquityPoolFacet; address admin; address user; @@ -27,6 +28,7 @@ contract PoolFacetHandler is Test { constructor( MockChainLinkFeed _collateralTokenPriceFeed, MockChainLinkFeed _stableUsdPriceFeed, + MockChainLinkFeed _ethUsdPriceFeed, UbiquityPoolFacet _ubiquityPoolFacet, address _admin, address _user, @@ -34,6 +36,7 @@ contract PoolFacetHandler is Test { ) { collateralTokenPriceFeed = _collateralTokenPriceFeed; stableUsdPriceFeed = _stableUsdPriceFeed; + ethUsdPriceFeed = _ethUsdPriceFeed; ubiquityPoolFacet = _ubiquityPoolFacet; admin = _admin; user = _user; @@ -152,6 +155,20 @@ contract PoolFacetHandler is Test { ubiquityPoolFacet.setCollateralRatio(newCollateralRatio); } + // ETH/USD price manipulations + //======================== + function setEthUsdPrice(uint256 _newPrice) public { + vm.assume(_newPrice >= 1000e8 && _newPrice <= 5000e8); + + ethUsdPriceFeed.updateMockParams( + 1, // round id + int256(_newPrice), // new price + block.timestamp, // started at + block.timestamp, // updated at + 1 // answered in round + ); + } + function mintUbiquityDollars( uint256 _dollarAmount, uint256 _dollarOutMin, diff --git a/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol b/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol index 113ec7949..51055ea4b 100644 --- a/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol +++ b/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol @@ -151,6 +151,7 @@ contract UbiquityPoolFacetInvariantTest is DiamondTestSetup { handler = new PoolFacetHandler( collateralTokenPriceFeed, stableUsdPriceFeed, + ethUsdPriceFeed, ubiquityPoolFacet, admin, user, From 26abd1442a026366a0c3b3b7ed74c51e2ff280ee Mon Sep 17 00:00:00 2001 From: green Date: Mon, 2 Sep 2024 21:23:40 +0200 Subject: [PATCH 10/17] test(pool-facet-handler): add docs --- .../diamond/facets/PoolFacetHandler.sol | 175 ++++++++++++------ .../facets/UbiquityPoolFacet.invariant.t.sol | 15 +- 2 files changed, 122 insertions(+), 68 deletions(-) diff --git a/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol b/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol index 2770f18e2..9c7e94219 100644 --- a/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol +++ b/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol @@ -11,20 +11,31 @@ import {SafeMath} from "@openzeppelin/contracts/utils/math/SafeMath.sol"; contract PoolFacetHandler is Test { using SafeMath for uint256; + // mock three ChainLink price feeds, one for each token MockChainLinkFeed collateralTokenPriceFeed; MockChainLinkFeed stableUsdPriceFeed; MockChainLinkFeed ethUsdPriceFeed; + UbiquityPoolFacet ubiquityPoolFacet; + + // mock two users address admin; address user; - MockCurveStableSwapNG curveDollarPlainPool; - event MintSuccess(uint256 dollarAmount); - event MintFailed(bytes reason); - - event redeemSuccess(uint256 dollarAmount); - event redeemFailed(bytes reason); + // mock curve pool Stablecoin/Dollar + MockCurveStableSwapNG curveDollarPlainPool; + /** + * @notice Constructs the PoolFacetHandler contract by initializing the necessary mocked contracts and addresses. + * @dev This constructor sets up the initial state with provided mock price feeds, pool facet, and user addresses. + * @param _collateralTokenPriceFeed The mock price feed for collateral tokens. + * @param _stableUsdPriceFeed The mock price feed for USD stablecoin. + * @param _ethUsdPriceFeed The mock price feed for ETH/USD. + * @param _ubiquityPoolFacet The pool facet contract responsible for managing Ubiquity Dollars. + * @param _admin The address with admin privileges, used for administrative actions in the pool. + * @param _user The user address that interacts with the pool for testing purposes. + * @param _curveDollarPlainPool The mocked Curve pool contract used for dollar-based operations. + */ constructor( MockChainLinkFeed _collateralTokenPriceFeed, MockChainLinkFeed _stableUsdPriceFeed, @@ -43,8 +54,12 @@ contract PoolFacetHandler is Test { curveDollarPlainPool = _curveDollarPlainPool; } - // Dollar price manipulations - //======================== + /** + * @notice Manipulates the Ubiquity Dollar price to a value above a set threshold. + * @dev This function assumes the new dollar price is within the specified range (greater than 1e18 and less than 2e18). + * It then updates the mocked Curve pool parameters and adjusts the collateral ratio in the UbiquityPoolFacet. + * @param newDollarPrice The new price for Ubiquity Dollar, expected to be within the range of 1e18 to 2e18. + */ function setDollarPriceAboveThreshold(uint256 newDollarPrice) public { vm.assume(newDollarPrice > 1e18 && newDollarPrice < 2e18); @@ -57,6 +72,12 @@ contract PoolFacetHandler is Test { ubiquityPoolFacet.setCollateralRatio(newCollateralRatio); } + /** + * @notice Manipulates the Ubiquity Dollar price to a value below a set threshold. + * @dev This function assumes the new dollar price is within the specified range (greater than or equal to 0.5e18 and less than 1e18). + * It then updates the mocked Curve pool parameters and adjusts the collateral ratio in the UbiquityPoolFacet accordingly. + * @param newDollarPrice The new price for Ubiquity Dollar, expected to be within the range of 0.5e18 to 1e18. + */ function setDollarPriceBelowThreshold(uint256 newDollarPrice) public { vm.assume(newDollarPrice >= 0.5e18 && newDollarPrice < 1e18); @@ -69,13 +90,25 @@ contract PoolFacetHandler is Test { ubiquityPoolFacet.setCollateralRatio(newCollateralRatio); } - // Redeem manipulations - //======================== + /** + * @notice Manipulates the redemption delay in blocks for UbiquityPoolFacet. + * @dev This function allows the admin to set a delay in blocks before a redemption can be completed. + * It assumes the caller is the admin and pranks the transaction as the admin to set the redemption delay. + * @param delay The number of blocks to set as the redemption delay. + */ function setRedemptionDelay(uint256 delay) public { vm.prank(admin); ubiquityPoolFacet.setRedemptionDelayBlocks(delay); } + /** + * @notice Manipulates the minting and redemption fees for UbiquityPoolFacet. + * @dev This function allows the admin to set the fees for minting and redeeming tokens in the pool. + * It assumes the caller is the admin and pranks the transaction as the admin to set the fees. + * The function also ensures that the provided mint and redeem fees fall within the acceptable range. + * @param mintFee The fee to be set for minting, expressed in basis points (1/100 of a percent). + * @param redeemFee The fee to be set for redeeming, expressed in basis points (1/100 of a percent). + */ function setMintAndRedeemFees(uint256 mintFee, uint256 redeemFee) public { vm.assume(mintFee >= 100000 && mintFee <= 200000); vm.assume(redeemFee >= 100000 && redeemFee <= 200000); @@ -88,39 +121,25 @@ contract PoolFacetHandler is Test { ubiquityPoolFacet.collectRedemption(0); } - function redeemDollar( - uint256 _dollarAmount, - uint256 _governanceOutMin, - uint256 _collateralOutMin - ) public { - vm.assume(_dollarAmount > 0 && _dollarAmount < type(uint256).max / 2); - vm.assume(_governanceOutMin >= 0 && _governanceOutMin <= _dollarAmount); - vm.assume(_collateralOutMin >= 0 && _collateralOutMin <= _dollarAmount); - - vm.prank(user); - try - ubiquityPoolFacet.redeemDollar( - 0, - _dollarAmount, - _governanceOutMin, - _collateralOutMin - ) - { - emit redeemSuccess(_dollarAmount); - } catch (bytes memory reason) { - emit redeemFailed(reason); - } - } - - // Ceiling manipulations - //======================== + /** + * @notice Manipulates the pool ceiling for UbiquityPoolFacet. + * @dev This function allows the admin to set a new ceiling for the pool, which determines the maximum + * amount of collateral that can be utilized in the pool. The function assumes the caller is the admin + * and pranks the transaction as the admin to set the new ceiling. + * @param newCeiling The new ceiling value to be set for the pool, representing the maximum amount + * of collateral in the pool. + */ function setPoolCeiling(uint256 newCeiling) public { vm.prank(admin); ubiquityPoolFacet.setPoolCeiling(0, newCeiling); } - // Collateral price manipulations - //======================== + /** + * @notice Manipulates the collateral price and updates the corresponding collateral ratio. + * @dev This function adjusts the price of the collateral token using a mock ChainLink price feed. + * It assumes the new price is within the allowed range and updates the collateral ratio in the UbiquityPoolFacet. + * @param _newPrice The new price of the collateral, scaled by 1e8 (e.g., a price of $1 is represented as 1e8). + */ function setCollateralPrice(int256 _newPrice) public { vm.assume(_newPrice >= 50_000_000 && _newPrice <= 200_000_000); @@ -138,8 +157,12 @@ contract PoolFacetHandler is Test { ubiquityPoolFacet.setCollateralRatio(newCollateralRatio); } - // USD stablecoin price manipulations - //======================== + /** + * @notice Manipulates the stable USD price and updates the corresponding collateral ratio. + * @dev This function adjusts the price of the stable USD token using a mock ChainLink price feed. + * It assumes the new price is within the specified range and updates the collateral ratio in the UbiquityPoolFacet. + * @param _newPrice The new price of the stable USD token, scaled by 1e8 (e.g., a price of $1 is represented as 1e8). + */ function setStableUsdPrice(uint256 _newPrice) public { vm.assume(_newPrice >= 0.5e8 && _newPrice <= 1.5e8); // Assume a range for testing @@ -155,8 +178,12 @@ contract PoolFacetHandler is Test { ubiquityPoolFacet.setCollateralRatio(newCollateralRatio); } - // ETH/USD price manipulations - //======================== + /** + * @notice Manipulates the ETH/USD price using a mock ChainLink price feed. + * @dev This function assumes the new price is within the specified range and updates the ETH/USD price + * in the corresponding mock price feed. + * @param _newPrice The new ETH/USD price, scaled by 1e8 (e.g., a price of $1,000 is represented as 1000e8). + */ function setEthUsdPrice(uint256 _newPrice) public { vm.assume(_newPrice >= 1000e8 && _newPrice <= 5000e8); @@ -169,6 +196,16 @@ contract PoolFacetHandler is Test { ); } + /** + * @notice Mints Ubiquity Dollar tokens using specified collateral and governance token inputs. + * @dev Assumes that the dollar amount is within a safe range, the minimum dollar output is less than or equal to the input amount, + * and the collateral and governance inputs are valid. The function then pranks the specified user to simulate a mint transaction. + * @param _dollarAmount The amount of Ubiquity Dollars to mint. + * @param _dollarOutMin The minimum amount of Ubiquity Dollars expected to be received from the minting process. + * @param _maxCollateralIn The maximum amount of collateral tokens to use for minting. + * @param _maxGovernanceIn The maximum amount of governance tokens to use for minting. + * @param _isOneToOne A boolean flag indicating whether the minting process should be executed on a one-to-one basis with the collateral. + */ function mintUbiquityDollars( uint256 _dollarAmount, uint256 _dollarOutMin, @@ -176,29 +213,49 @@ contract PoolFacetHandler is Test { uint256 _maxGovernanceIn, bool _isOneToOne ) public { - vm.assume(_dollarAmount > 0 && _dollarAmount < type(uint256).max / 2); + vm.assume( + _dollarAmount > 0 && _dollarAmount < type(uint256).max.div(2) + ); vm.assume(_dollarOutMin <= _dollarAmount); vm.assume( - _maxCollateralIn > 0 && _maxCollateralIn < type(uint256).max / 2 + _maxCollateralIn > 0 && _maxCollateralIn < type(uint256).max.div(2) + ); + vm.assume( + _maxGovernanceIn >= 0 && _maxGovernanceIn < type(uint256).max.div(2) + ); + + vm.prank(user); + + ubiquityPoolFacet.mintDollar( + 0, + _dollarAmount, + _dollarOutMin, + _maxCollateralIn, + _maxGovernanceIn, + _isOneToOne ); + } + + /** + * @notice Redeems Ubiquity Dollar tokens for collateral and governance tokens. + * @dev Assumes the dollar amount is within a safe range, and that the minimum expected governance and collateral outputs are valid. + * The function then pranks the specified user to simulate a redemption transaction. + * @param _dollarAmount The amount of Ubiquity Dollars to redeem. + * @param _governanceOutMin The minimum amount of governance tokens expected to be received from the redemption process. + * @param _collateralOutMin The minimum amount of collateral tokens expected to be received from the redemption process. + */ + function redeemDollar( + uint256 _dollarAmount, + uint256 _governanceOutMin, + uint256 _collateralOutMin + ) public { vm.assume( - _maxGovernanceIn >= 0 && _maxGovernanceIn < type(uint256).max / 2 + _dollarAmount > 0 && _dollarAmount < type(uint256).max.div(2) ); + vm.assume(_governanceOutMin >= 0 && _governanceOutMin <= _dollarAmount); + vm.assume(_collateralOutMin >= 0 && _collateralOutMin <= _dollarAmount); vm.prank(user); - try - ubiquityPoolFacet.mintDollar( - 0, - _dollarAmount, - _dollarOutMin, - _maxCollateralIn, - _maxGovernanceIn, - _isOneToOne - ) - { - emit MintSuccess(_dollarAmount); - } catch (bytes memory reason) { - emit MintFailed(reason); - } + ubiquityPoolFacet.redeemDollar(0, 0.1e18, 0, 0); } } diff --git a/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol b/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol index 51055ea4b..7d7e15ccc 100644 --- a/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol +++ b/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol @@ -163,16 +163,18 @@ contract UbiquityPoolFacetInvariantTest is DiamondTestSetup { targetContract(address(handler)); } + /** + * @notice Ensures that the total supply of Ubiquity Dollars does not exceed the collateral value backing them. + * @dev This invariant checker calculates the total supply of Ubiquity Dollars in USD terms and compares it to the USD value of the collateral. + * The invariant asserts that the value of minted dollars does not exceed the value of the collateral. + * If the invariant is violated, it indicates that more Ubiquity Dollars have been minted than the available collateral can support. + */ function invariant_CannotMintMoreDollarsThanCollateral() public { uint256 totalDollarSupply = IERC20Ubiquity( managerFacet.dollarTokenAddress() ).totalSupply(); uint256 collateralUsdBalance = ubiquityPoolFacet.collateralUsdBalance(); - console.log( - ":::::::| UbiquityPoolFacetInvariantTest | collateralUsdBalance:", - collateralUsdBalance - ); vm.assume(collateralUsdBalance > 0 && totalDollarSupply > 0); @@ -180,11 +182,6 @@ contract UbiquityPoolFacetInvariantTest is DiamondTestSetup { uint256 totalDollarSupplyInUsd = (totalDollarSupply * dollarPrice) / 1e6; - console.log( - ":::::::| UbiquityPoolFacetInvariantTest11 | totalDollarSupplyInUsd:", - totalDollarSupplyInUsd - ); - assertTrue( totalDollarSupplyInUsd <= collateralUsdBalance, "Minted dollars exceed collateral value" From dfff4f314386ef0ec4d0fb6091367623823480e0 Mon Sep 17 00:00:00 2001 From: green Date: Mon, 2 Sep 2024 21:25:41 +0200 Subject: [PATCH 11/17] test(pool-facet-handler): deted hard params for redeemDollar --- .../test/invariant/diamond/facets/PoolFacetHandler.sol | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol b/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol index 9c7e94219..5745777c6 100644 --- a/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol +++ b/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol @@ -256,6 +256,11 @@ contract PoolFacetHandler is Test { vm.assume(_collateralOutMin >= 0 && _collateralOutMin <= _dollarAmount); vm.prank(user); - ubiquityPoolFacet.redeemDollar(0, 0.1e18, 0, 0); + ubiquityPoolFacet.redeemDollar( + 0, + _dollarAmount, + _governanceOutMin, + _collateralOutMin + ); } } From b49a8752c2fa1c27ad5d01b7070f0dc5d26d9330 Mon Sep 17 00:00:00 2001 From: green Date: Tue, 3 Sep 2024 19:34:53 +0200 Subject: [PATCH 12/17] test(pool-facet-handler): update assume statements in redeemDollar and mintUbiquityDollars --- .../diamond/facets/PoolFacetHandler.sol | 44 ++++++++++++++----- .../facets/UbiquityPoolFacet.invariant.t.sol | 4 +- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol b/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol index 5745777c6..43a92681f 100644 --- a/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol +++ b/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol @@ -6,6 +6,9 @@ import "forge-std/console.sol"; import {UbiquityPoolFacet} from "../../../../src/dollar/facets/UbiquityPoolFacet.sol"; import {MockChainLinkFeed} from "../../../../src/dollar/mocks/MockChainLinkFeed.sol"; import {MockCurveStableSwapNG} from "../../../../src/dollar/mocks/MockCurveStableSwapNG.sol"; +import {MockERC20} from "../../../../src/dollar/mocks/MockERC20.sol"; +import {IERC20Ubiquity} from "../../../../src/dollar/interfaces/IERC20Ubiquity.sol"; +import {ManagerFacet} from "../../../../src/dollar/facets/ManagerFacet.sol"; import {SafeMath} from "@openzeppelin/contracts/utils/math/SafeMath.sol"; contract PoolFacetHandler is Test { @@ -15,8 +18,13 @@ contract PoolFacetHandler is Test { MockChainLinkFeed collateralTokenPriceFeed; MockChainLinkFeed stableUsdPriceFeed; MockChainLinkFeed ethUsdPriceFeed; + MockERC20 collateralToken; + + ManagerFacet managerFacet; UbiquityPoolFacet ubiquityPoolFacet; + IERC20Ubiquity dollar; + IERC20Ubiquity governanceToken; // mock two users address admin; @@ -35,7 +43,10 @@ contract PoolFacetHandler is Test { * @param _admin The address with admin privileges, used for administrative actions in the pool. * @param _user The user address that interacts with the pool for testing purposes. * @param _curveDollarPlainPool The mocked Curve pool contract used for dollar-based operations. + * @param _managerFacet The manager facet contract, which provides access to various addresses and core components of the Ubiquity system. + * @param _collateralToken The mock ERC20 collateral token used in the pool for testing deposit and redemption functionalities. */ + constructor( MockChainLinkFeed _collateralTokenPriceFeed, MockChainLinkFeed _stableUsdPriceFeed, @@ -43,7 +54,9 @@ contract PoolFacetHandler is Test { UbiquityPoolFacet _ubiquityPoolFacet, address _admin, address _user, - MockCurveStableSwapNG _curveDollarPlainPool + MockCurveStableSwapNG _curveDollarPlainPool, + ManagerFacet _managerFacet, + MockERC20 _collateralToken ) { collateralTokenPriceFeed = _collateralTokenPriceFeed; stableUsdPriceFeed = _stableUsdPriceFeed; @@ -52,6 +65,11 @@ contract PoolFacetHandler is Test { admin = _admin; user = _user; curveDollarPlainPool = _curveDollarPlainPool; + managerFacet = _managerFacet; + collateralToken = _collateralToken; + + dollar = IERC20Ubiquity(managerFacet.dollarTokenAddress()); + governanceToken = IERC20Ubiquity(managerFacet.governanceTokenAddress()); } /** @@ -213,19 +231,17 @@ contract PoolFacetHandler is Test { uint256 _maxGovernanceIn, bool _isOneToOne ) public { - vm.assume( - _dollarAmount > 0 && _dollarAmount < type(uint256).max.div(2) - ); + uint256 maxUintHalf = type(uint256).max.div(2); + uint256 collateralTotalSupply = collateralToken.totalSupply(); + + vm.assume(_dollarAmount > 0 && _dollarAmount < maxUintHalf); vm.assume(_dollarOutMin <= _dollarAmount); vm.assume( - _maxCollateralIn > 0 && _maxCollateralIn < type(uint256).max.div(2) - ); - vm.assume( - _maxGovernanceIn >= 0 && _maxGovernanceIn < type(uint256).max.div(2) + _maxCollateralIn > 0 && _maxCollateralIn < collateralTotalSupply ); + vm.assume(_maxGovernanceIn >= 0 && _maxGovernanceIn <= maxUintHalf); vm.prank(user); - ubiquityPoolFacet.mintDollar( 0, _dollarAmount, @@ -249,11 +265,15 @@ contract PoolFacetHandler is Test { uint256 _governanceOutMin, uint256 _collateralOutMin ) public { + uint256 maxUintHalf = type(uint256).max.div(2); + uint256 dollarTotalSupply = dollar.totalSupply(); + uint256 collateralTotalSupply = collateralToken.totalSupply(); + + vm.assume(_dollarAmount > 0 && _dollarAmount < dollarTotalSupply); vm.assume( - _dollarAmount > 0 && _dollarAmount < type(uint256).max.div(2) + _collateralOutMin >= 0 && _collateralOutMin <= collateralTotalSupply ); - vm.assume(_governanceOutMin >= 0 && _governanceOutMin <= _dollarAmount); - vm.assume(_collateralOutMin >= 0 && _collateralOutMin <= _dollarAmount); + vm.assume(_governanceOutMin >= 0 && _governanceOutMin <= maxUintHalf); vm.prank(user); ubiquityPoolFacet.redeemDollar( diff --git a/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol b/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol index 7d7e15ccc..1be9313af 100644 --- a/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol +++ b/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol @@ -155,7 +155,9 @@ contract UbiquityPoolFacetInvariantTest is DiamondTestSetup { ubiquityPoolFacet, admin, user, - curveDollarPlainPool + curveDollarPlainPool, + managerFacet, + collateralToken ); handler.mintUbiquityDollars(1e18, 0.9e18, 1e18, 0, true); From 44e9bf50252f0e2b2ff1aec639f647e45c1dd4f3 Mon Sep 17 00:00:00 2001 From: green Date: Tue, 10 Sep 2024 23:36:25 +0200 Subject: [PATCH 13/17] test(pool-facet-handler): refactor the income fuzz parameters --- .../diamond/facets/PoolFacetHandler.sol | 95 ++++++------------- 1 file changed, 30 insertions(+), 65 deletions(-) diff --git a/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol b/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol index 43a92681f..2ba47b4bf 100644 --- a/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol +++ b/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol @@ -74,49 +74,16 @@ contract PoolFacetHandler is Test { /** * @notice Manipulates the Ubiquity Dollar price to a value above a set threshold. - * @dev This function assumes the new dollar price is within the specified range (greater than 1e18 and less than 2e18). - * It then updates the mocked Curve pool parameters and adjusts the collateral ratio in the UbiquityPoolFacet. - * @param newDollarPrice The new price for Ubiquity Dollar, expected to be within the range of 1e18 to 2e18. */ - function setDollarPriceAboveThreshold(uint256 newDollarPrice) public { - vm.assume(newDollarPrice > 1e18 && newDollarPrice < 2e18); - - vm.prank(admin); - curveDollarPlainPool.updateMockParams(newDollarPrice); - - uint256 reductionFactor = newDollarPrice.sub(1e18).div(1e16); - uint256 newCollateralRatio = uint256(1e6).sub(reductionFactor); - - ubiquityPoolFacet.setCollateralRatio(newCollateralRatio); + function setDollarPriceAboveThreshold() public { + curveDollarPlainPool.updateMockParams(1.01e18); } /** * @notice Manipulates the Ubiquity Dollar price to a value below a set threshold. - * @dev This function assumes the new dollar price is within the specified range (greater than or equal to 0.5e18 and less than 1e18). - * It then updates the mocked Curve pool parameters and adjusts the collateral ratio in the UbiquityPoolFacet accordingly. - * @param newDollarPrice The new price for Ubiquity Dollar, expected to be within the range of 0.5e18 to 1e18. - */ - function setDollarPriceBelowThreshold(uint256 newDollarPrice) public { - vm.assume(newDollarPrice >= 0.5e18 && newDollarPrice < 1e18); - - vm.prank(admin); - curveDollarPlainPool.updateMockParams(newDollarPrice); - - uint256 increaseFactor = uint256(1e18).sub(newDollarPrice).div(1e16); - uint256 newCollateralRatio = uint256(1e6).add(increaseFactor); - - ubiquityPoolFacet.setCollateralRatio(newCollateralRatio); - } - - /** - * @notice Manipulates the redemption delay in blocks for UbiquityPoolFacet. - * @dev This function allows the admin to set a delay in blocks before a redemption can be completed. - * It assumes the caller is the admin and pranks the transaction as the admin to set the redemption delay. - * @param delay The number of blocks to set as the redemption delay. */ - function setRedemptionDelay(uint256 delay) public { - vm.prank(admin); - ubiquityPoolFacet.setRedemptionDelayBlocks(delay); + function setDollarPriceBelowThreshold() public { + curveDollarPlainPool.updateMockParams(0.99e18); } /** @@ -148,52 +115,50 @@ contract PoolFacetHandler is Test { * of collateral in the pool. */ function setPoolCeiling(uint256 newCeiling) public { + uint256 maxUint = type(uint128).max; + vm.assume(newCeiling <= maxUint); + vm.prank(admin); ubiquityPoolFacet.setPoolCeiling(0, newCeiling); } /** - * @notice Manipulates the collateral price and updates the corresponding collateral ratio. - * @dev This function adjusts the price of the collateral token using a mock ChainLink price feed. - * It assumes the new price is within the allowed range and updates the collateral ratio in the UbiquityPoolFacet. - * @param _newPrice The new price of the collateral, scaled by 1e8 (e.g., a price of $1 is represented as 1e8). + * @notice Manipulates the stable USD price and updates the corresponding collateral ratio. + * @dev This function adjusts the price of the stable USD token using a mock ChainLink price feed. + * It assumes the new price is within the specified range and updates the collateral ratio in the UbiquityPoolFacet. + * @param _newPrice The new price of the stable USD token, scaled by 1e8 (e.g., a price of $1 is represented as 1e8). */ - function setCollateralPrice(int256 _newPrice) public { - vm.assume(_newPrice >= 50_000_000 && _newPrice <= 200_000_000); + function setStableUsdPrice(uint256 _newPrice) public { + // Assume the price is between $0.70 and $1.00 (0.7e8 <= _newPrice <= 1e8) + vm.assume(_newPrice >= 0.7e8 && _newPrice <= 1e8); - collateralTokenPriceFeed.updateMockParams( + stableUsdPriceFeed.updateMockParams( 1, // round id - _newPrice, + int256(_newPrice), // Set the new price dynamically based on _newPrice block.timestamp, // started at block.timestamp, // updated at 1 // answered in round ); - - ubiquityPoolFacet.updateChainLinkCollateralPrice(0); - - uint256 newCollateralRatio = uint256(1e6 * 1e8).div(uint256(_newPrice)); - ubiquityPoolFacet.setCollateralRatio(newCollateralRatio); } /** - * @notice Manipulates the stable USD price and updates the corresponding collateral ratio. - * @dev This function adjusts the price of the stable USD token using a mock ChainLink price feed. - * It assumes the new price is within the specified range and updates the collateral ratio in the UbiquityPoolFacet. - * @param _newPrice The new price of the stable USD token, scaled by 1e8 (e.g., a price of $1 is represented as 1e8). + * @notice Manipulates the collateral price and updates the corresponding collateral ratio. + * @dev This function adjusts the price of the collateral token using a mock ChainLink price feed. + * It assumes the new price is within the allowed range and updates the collateral ratio in the UbiquityPoolFacet. + * @param _newPrice The new price of the collateral, scaled by 1e8 (e.g., a price of $1 is represented as 1e8). */ - function setStableUsdPrice(uint256 _newPrice) public { - vm.assume(_newPrice >= 0.5e8 && _newPrice <= 1.5e8); // Assume a range for testing + function setCollateralPrice(int256 _newPrice) public { + vm.assume(_newPrice >= 100_000_000 && _newPrice <= 150_000_000); - stableUsdPriceFeed.updateMockParams( + collateralTokenPriceFeed.updateMockParams( 1, // round id - int256(_newPrice), + _newPrice, block.timestamp, // started at block.timestamp, // updated at 1 // answered in round ); - uint256 newCollateralRatio = uint256(1e6 * 1e8).div(_newPrice); - ubiquityPoolFacet.setCollateralRatio(newCollateralRatio); + ubiquityPoolFacet.updateChainLinkCollateralPrice(0); } /** @@ -231,15 +196,15 @@ contract PoolFacetHandler is Test { uint256 _maxGovernanceIn, bool _isOneToOne ) public { - uint256 maxUintHalf = type(uint256).max.div(2); + uint256 maxUint = type(uint128).max; uint256 collateralTotalSupply = collateralToken.totalSupply(); - vm.assume(_dollarAmount > 0 && _dollarAmount < maxUintHalf); + vm.assume(_dollarAmount > 0 && _dollarAmount < maxUint); vm.assume(_dollarOutMin <= _dollarAmount); vm.assume( _maxCollateralIn > 0 && _maxCollateralIn < collateralTotalSupply ); - vm.assume(_maxGovernanceIn >= 0 && _maxGovernanceIn <= maxUintHalf); + vm.assume(_maxGovernanceIn >= 0 && _maxGovernanceIn <= maxUint); vm.prank(user); ubiquityPoolFacet.mintDollar( @@ -265,7 +230,7 @@ contract PoolFacetHandler is Test { uint256 _governanceOutMin, uint256 _collateralOutMin ) public { - uint256 maxUintHalf = type(uint256).max.div(2); + uint256 maxUint = type(uint128).max; uint256 dollarTotalSupply = dollar.totalSupply(); uint256 collateralTotalSupply = collateralToken.totalSupply(); @@ -273,7 +238,7 @@ contract PoolFacetHandler is Test { vm.assume( _collateralOutMin >= 0 && _collateralOutMin <= collateralTotalSupply ); - vm.assume(_governanceOutMin >= 0 && _governanceOutMin <= maxUintHalf); + vm.assume(_governanceOutMin >= 0 && _governanceOutMin <= maxUint); vm.prank(user); ubiquityPoolFacet.redeemDollar( From 316d5576142d839621701c9d2e44685884901412 Mon Sep 17 00:00:00 2001 From: green Date: Wed, 11 Sep 2024 18:39:45 +0200 Subject: [PATCH 14/17] test(pool-facet-invariant): add new invariant checks for minting and redemption --- .../diamond/facets/PoolFacetHandler.sol | 18 +++++- .../facets/UbiquityPoolFacet.invariant.t.sol | 56 ++++++++++++++++--- 2 files changed, 64 insertions(+), 10 deletions(-) diff --git a/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol b/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol index 2ba47b4bf..651b46e5c 100644 --- a/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol +++ b/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol @@ -102,7 +102,23 @@ contract PoolFacetHandler is Test { ubiquityPoolFacet.setFees(0, mintFee, redeemFee); } - function collectRedemption() public { + /** + * @notice Simulates the process of a user collecting redeemed collateral and governance tokens after advancing a number of blocks. + * @dev This function first advances the block number by the specified amount and then simulates the user calling the `collectRedemption` function. + * @param _blocksToAdvance The number of blocks to advance before allowing redemption, constrained by an upper limit. + */ + function collectRedemption(uint256 _blocksToAdvance) public { + uint256 currentBlock = block.number; + uint256 maxBlockAdvance = 100; + + // Ensure the number of blocks to advance is within a reasonable range + vm.assume(_blocksToAdvance > 0 && _blocksToAdvance <= maxBlockAdvance); + + // Advance the block number by the specified amount + vm.roll(currentBlock + _blocksToAdvance); + + // Simulate the `user` performing the redemption + vm.prank(user); ubiquityPoolFacet.collectRedemption(0); } diff --git a/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol b/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol index 1be9313af..87b9ab74d 100644 --- a/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol +++ b/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol @@ -11,8 +11,11 @@ import {PoolFacetHandler} from "./PoolFacetHandler.sol"; import {IERC20Ubiquity} from "../../../../src/dollar/interfaces/IERC20Ubiquity.sol"; import {MockCurveStableSwapNG} from "../../../../src/dollar/mocks/MockCurveStableSwapNG.sol"; import {MockCurveTwocryptoOptimized} from "../../../../src/dollar/mocks/MockCurveTwocryptoOptimized.sol"; +import {SafeMath} from "@openzeppelin/contracts/utils/math/SafeMath.sol"; contract UbiquityPoolFacetInvariantTest is DiamondTestSetup { + using SafeMath for uint256; + PoolFacetHandler handler; // mock three tokens: collateral token, stable token, wrapped ETH token @@ -172,21 +175,56 @@ contract UbiquityPoolFacetInvariantTest is DiamondTestSetup { * If the invariant is violated, it indicates that more Ubiquity Dollars have been minted than the available collateral can support. */ function invariant_CannotMintMoreDollarsThanCollateral() public { + ( + uint256 totalDollarSupplyInUsd, + uint256 collateralUsdBalance + ) = getDollarSupplyAndCollateralBalance(); + + assertTrue( + totalDollarSupplyInUsd <= collateralUsdBalance, + "Minted dollars exceed collateral value" + ); + } + + /** + * @notice Ensures that users cannot redeem more collateral than the value of the Dollar tokens provided. + * @dev This invariant checker calculates the total supply of Ubiquity Dollars in USD terms and compares it to the USD value of the collateral. + * The invariant asserts that the value of collateral redeemed does not exceed the value of the Dollar tokens burned. + * If the invariant is violated, it indicates that more collateral has been redeemed than the Ubiquity Dollars can support. + */ + function invariant_CannotRedeemMoreCollateralThanDollarValue() public { + ( + uint256 totalDollarSupplyInUsd, + uint256 collateralUsdBalance + ) = getDollarSupplyAndCollateralBalance(); + + assertTrue( + collateralUsdBalance >= totalDollarSupplyInUsd, + "Redeemed collateral exceeds provided Dollar tokens" + ); + } + + /** + * @notice Helper function to get the USD value of total Dollar supply and the collateral USD balance. + * @dev This function returns the current total supply of Ubiquity Dollars in USD and the USD value of the collateral. + * @return totalDollarSupplyInUsd The total supply of Ubiquity Dollars in USD (6 decimals). + * @return collateralUsdBalance The total USD value of collateral backing the Ubiquity Dollars (18 decimals). + */ + function getDollarSupplyAndCollateralBalance() + public + view + returns (uint256 totalDollarSupplyInUsd, uint256 collateralUsdBalance) + { uint256 totalDollarSupply = IERC20Ubiquity( managerFacet.dollarTokenAddress() ).totalSupply(); - uint256 collateralUsdBalance = ubiquityPoolFacet.collateralUsdBalance(); + collateralUsdBalance = ubiquityPoolFacet.collateralUsdBalance(); - vm.assume(collateralUsdBalance > 0 && totalDollarSupply > 0); + require(collateralUsdBalance > 0, "Collateral balance is zero"); + require(totalDollarSupply > 0, "Dollar supply is zero"); uint256 dollarPrice = ubiquityPoolFacet.getDollarPriceUsd(); - uint256 totalDollarSupplyInUsd = (totalDollarSupply * dollarPrice) / - 1e6; - - assertTrue( - totalDollarSupplyInUsd <= collateralUsdBalance, - "Minted dollars exceed collateral value" - ); + totalDollarSupplyInUsd = totalDollarSupply.mul(dollarPrice).div(1e6); } } From 16de33c8617a51d7c450402060f0207d81fb7403 Mon Sep 17 00:00:00 2001 From: green Date: Wed, 11 Sep 2024 22:19:53 +0200 Subject: [PATCH 15/17] test(setting-up-ci): add deep-invariant ci sttings --- .github/workflows/deep-fuzz-and-invariant.yml | 31 +++++++++++++++++++ .github/workflows/deep-invariant.yml | 31 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 .github/workflows/deep-fuzz-and-invariant.yml create mode 100644 .github/workflows/deep-invariant.yml diff --git a/.github/workflows/deep-fuzz-and-invariant.yml b/.github/workflows/deep-fuzz-and-invariant.yml new file mode 100644 index 000000000..b68e0f9ec --- /dev/null +++ b/.github/workflows/deep-fuzz-and-invariant.yml @@ -0,0 +1,31 @@ +name: Deep Fuzz and Invariant Tests +on: + push: + branches: + - development + paths: + - '**.sol' + +jobs: + deep-fuzz-and-invariant: + name: Run Deep Fuzz and Invariant Tests + runs-on: ubuntu-22.04 + env: + FOUNDRY_PROFILE: intense + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Setup Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly-5be158ba6dc7c798a6f032026fe60fc01686b33b + + - name: Forge install + working-directory: packages/contracts + run: forge install + + - name: Run Deep Fuzz and Invariant Tests + working-directory: packages/contracts + run: forge test diff --git a/.github/workflows/deep-invariant.yml b/.github/workflows/deep-invariant.yml new file mode 100644 index 000000000..db5579007 --- /dev/null +++ b/.github/workflows/deep-invariant.yml @@ -0,0 +1,31 @@ +name: Invariant Tests +on: + push: + branches: + - development + paths: + - '**.sol' + +jobs: + invariant-tests: + name: Run Invariant Tests + runs-on: ubuntu-22.04 + env: + FOUNDRY_PROFILE: intense + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Setup Foundry + uses: foundry-rs/toolchain@v1 + with: + version: nightly-5be158ba6dc7c79a8f6302026fe60fc01606b33b + + - name: Forge install + working-directory: packages/contracts + run: forge install + + - name: Run Invariant Tests + working-directory: packages/contracts + run: forge test From ae26dfbe0f8de710ecb4c3a32f1f192cd9f66cd6 Mon Sep 17 00:00:00 2001 From: green Date: Fri, 13 Sep 2024 22:08:43 +0200 Subject: [PATCH 16/17] test(ci-settings): update fuzz and invariant ci settings --- .github/workflows/deep-fuzz-and-invariant.yml | 31 ------------------- .github/workflows/deep-fuzz.yml | 2 +- .github/workflows/deep-invariant.yml | 6 ++-- 3 files changed, 4 insertions(+), 35 deletions(-) delete mode 100644 .github/workflows/deep-fuzz-and-invariant.yml diff --git a/.github/workflows/deep-fuzz-and-invariant.yml b/.github/workflows/deep-fuzz-and-invariant.yml deleted file mode 100644 index b68e0f9ec..000000000 --- a/.github/workflows/deep-fuzz-and-invariant.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Deep Fuzz and Invariant Tests -on: - push: - branches: - - development - paths: - - '**.sol' - -jobs: - deep-fuzz-and-invariant: - name: Run Deep Fuzz and Invariant Tests - runs-on: ubuntu-22.04 - env: - FOUNDRY_PROFILE: intense - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Setup Foundry - uses: foundry-rs/foundry-toolchain@v1 - with: - version: nightly-5be158ba6dc7c798a6f032026fe60fc01686b33b - - - name: Forge install - working-directory: packages/contracts - run: forge install - - - name: Run Deep Fuzz and Invariant Tests - working-directory: packages/contracts - run: forge test diff --git a/.github/workflows/deep-fuzz.yml b/.github/workflows/deep-fuzz.yml index d0329cb12..17eacb88b 100644 --- a/.github/workflows/deep-fuzz.yml +++ b/.github/workflows/deep-fuzz.yml @@ -28,4 +28,4 @@ jobs: - name: Deep Fuzz Solidity Contracts working-directory: packages/contracts - run: forge test + run: forge test --match-path "test/fuzz/**/*" diff --git a/.github/workflows/deep-invariant.yml b/.github/workflows/deep-invariant.yml index db5579007..b6a96911e 100644 --- a/.github/workflows/deep-invariant.yml +++ b/.github/workflows/deep-invariant.yml @@ -18,7 +18,7 @@ jobs: uses: actions/checkout@v3 - name: Setup Foundry - uses: foundry-rs/toolchain@v1 + uses: foundry-rs/foundry-toolchain@v1 with: version: nightly-5be158ba6dc7c79a8f6302026fe60fc01606b33b @@ -26,6 +26,6 @@ jobs: working-directory: packages/contracts run: forge install - - name: Run Invariant Tests + - name: Run Deep Invariant Tests working-directory: packages/contracts - run: forge test + run: forge test --match-path "test/invariant/**/*" From e5c687e4a6ebdcb3cc5e2e3b3bb5df6c53c29346 Mon Sep 17 00:00:00 2001 From: green Date: Wed, 18 Sep 2024 15:59:30 +0200 Subject: [PATCH 17/17] test: add an invariant profile to the foundry configuration --- packages/contracts/foundry.toml | 3 +++ .../diamond/facets/PoolFacetHandler.sol | 16 ++++++++++------ .../facets/UbiquityPoolFacet.invariant.t.sol | 2 +- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/contracts/foundry.toml b/packages/contracts/foundry.toml index 8b516b440..c1c0e91ca 100644 --- a/packages/contracts/foundry.toml +++ b/packages/contracts/foundry.toml @@ -43,3 +43,6 @@ src = 'src/dollar' [profile.intense.fuzz] runs = 100000 max_test_rejects = 900000 + +[profile.intense.invariant] +runs = 50000 # ~1 hour \ No newline at end of file diff --git a/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol b/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol index 651b46e5c..1617d5c0b 100644 --- a/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol +++ b/packages/contracts/test/invariant/diamond/facets/PoolFacetHandler.sol @@ -73,14 +73,18 @@ contract PoolFacetHandler is Test { } /** - * @notice Manipulates the Ubiquity Dollar price to a value above a set threshold. + * @notice Simulates setting the Ubiquity Dollar price to a value of the minting threshold. + * @dev This function updates the mocked Curve pool parameters to simulate a Ubiquity Dollar price of $1.01. + * The threshold of $1.01 is significant because it represents the upper bound at which new Ubiquity Dollars can be minted. */ function setDollarPriceAboveThreshold() public { curveDollarPlainPool.updateMockParams(1.01e18); } /** - * @notice Manipulates the Ubiquity Dollar price to a value below a set threshold. + * @notice Simulates setting the Ubiquity Dollar price to a value of the redemption threshold. + * @dev This function updates the mocked Curve pool parameters to simulate a Ubiquity Dollar price of $0.99. + * The threshold of $0.99 is significant because it represents the lower bound at which Ubiquity Dollars can be redeemed. */ function setDollarPriceBelowThreshold() public { curveDollarPlainPool.updateMockParams(0.99e18); @@ -139,9 +143,9 @@ contract PoolFacetHandler is Test { } /** - * @notice Manipulates the stable USD price and updates the corresponding collateral ratio. + * @notice Manipulates the stable USD price. * @dev This function adjusts the price of the stable USD token using a mock ChainLink price feed. - * It assumes the new price is within the specified range and updates the collateral ratio in the UbiquityPoolFacet. + * It assumes the new price is within the specified range. * @param _newPrice The new price of the stable USD token, scaled by 1e8 (e.g., a price of $1 is represented as 1e8). */ function setStableUsdPrice(uint256 _newPrice) public { @@ -158,9 +162,9 @@ contract PoolFacetHandler is Test { } /** - * @notice Manipulates the collateral price and updates the corresponding collateral ratio. + * @notice Manipulates the collateral price. * @dev This function adjusts the price of the collateral token using a mock ChainLink price feed. - * It assumes the new price is within the allowed range and updates the collateral ratio in the UbiquityPoolFacet. + * It assumes the new price is within the allowed range. * @param _newPrice The new price of the collateral, scaled by 1e8 (e.g., a price of $1 is represented as 1e8). */ function setCollateralPrice(int256 _newPrice) public { diff --git a/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol b/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol index 87b9ab74d..114d646a1 100644 --- a/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol +++ b/packages/contracts/test/invariant/diamond/facets/UbiquityPoolFacet.invariant.t.sol @@ -207,7 +207,7 @@ contract UbiquityPoolFacetInvariantTest is DiamondTestSetup { /** * @notice Helper function to get the USD value of total Dollar supply and the collateral USD balance. * @dev This function returns the current total supply of Ubiquity Dollars in USD and the USD value of the collateral. - * @return totalDollarSupplyInUsd The total supply of Ubiquity Dollars in USD (6 decimals). + * @return totalDollarSupplyInUsd The total supply of Ubiquity Dollars in USD (18 decimals). * @return collateralUsdBalance The total USD value of collateral backing the Ubiquity Dollars (18 decimals). */ function getDollarSupplyAndCollateralBalance()