Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Invariant tests for UbiquityPoolFacet #953

Merged
merged 17 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
9e0d5fc
test(UbiquityPoolFacet): add invariant tests for collateral token add…
alexandr-masl Aug 21, 2024
42d739a
test(ubiquityPoolFacet): add invariant test for Ubiquity Dollar minti…
alexandr-masl Aug 25, 2024
6689906
test(ubiquity-pool-facet): add collateral, ceiling, redeem, and dolla…
alexandr-masl Aug 27, 2024
142f779
test(UbiquityPoolFacetInvariantTest): add collateral price manipulati…
alexandr-masl Aug 29, 2024
5407774
test(UbiquityPoolFacetInvariantTest): delete irrelevant logs
alexandr-masl Aug 29, 2024
dd94c7e
test(lib-ubiquity-pool): add conservative estimates for price and fees
alexandr-masl Aug 31, 2024
989d9c6
test(pool-facet-handler): add fuzzed parameters to dollar price manip…
alexandr-masl Aug 31, 2024
1b7f1da
test(pool-facet-handler): add usd price manipulations
alexandr-masl Aug 31, 2024
fd8acb6
test(pool-facet-handler): eth/usd price manipulations
alexandr-masl Sep 2, 2024
26abd14
test(pool-facet-handler): add docs
alexandr-masl Sep 2, 2024
dfff4f3
test(pool-facet-handler): deted hard params for redeemDollar
alexandr-masl Sep 2, 2024
b49a875
test(pool-facet-handler): update assume statements in redeemDollar an…
alexandr-masl Sep 3, 2024
44e9bf5
test(pool-facet-handler): refactor the income fuzz parameters
alexandr-masl Sep 10, 2024
316d557
test(pool-facet-invariant): add new invariant checks for minting and …
alexandr-masl Sep 11, 2024
16de33c
test(setting-up-ci): add deep-invariant ci sttings
alexandr-masl Sep 11, 2024
ae26dfb
test(ci-settings): update fuzz and invariant ci settings
alexandr-masl Sep 13, 2024
e5c687e
test: add an invariant profile to the foundry configuration
alexandr-masl Sep 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// SPDX-License-Identifier: MIT
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,
address _admin,
address _user,
MockCurveStableSwapNG _curveDollarPlainPool
) {
collateralTokenPriceFeed = _collateralTokenPriceFeed;
ubiquityPoolFacet = _ubiquityPoolFacet;
admin = _admin;
user = _user;
curveDollarPlainPool = _curveDollarPlainPool;
}

// Dollar price manipulations
//========================
function setDollarPriceAboveThreshold() public {
vm.prank(admin);
curveDollarPlainPool.updateMockParams(1.02e18);
}

function setDollarPriceBelowThreshold() public {
vm.prank(admin);
curveDollarPlainPool.updateMockParams(0.98e18);
}

// Redeem manipulations
//========================
function setMintAndRedeemFees(uint256 mintFee, uint256 redeemFee) public {
vm.prank(admin);
ubiquityPoolFacet.setFees(0, mintFee, redeemFee);
}

function setRedemptionDelay(uint256 delay) public {
vm.prank(admin);
ubiquityPoolFacet.setRedemptionDelayBlocks(delay);
}

function collectRedemption() public {
ubiquityPoolFacet.collectRedemption(0);
alexandr-masl marked this conversation as resolved.
Show resolved Hide resolved
}

function redeemDollar(
uint256 _dollarAmount,
uint256 _governanceOutMin,
uint256 _collateralOutMin
) public {
vm.prank(user);
ubiquityPoolFacet.redeemDollar(
0,
_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(
alexandr-masl marked this conversation as resolved.
Show resolved Hide resolved
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);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
// 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";
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 {
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();

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
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 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
address(collateralTokenPriceFeed), // price feed address
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,
admin,
user,
curveDollarPlainPool
);
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;

assertTrue(
totalDollarValue <= totalCollateralValue,
"Minted dollars exceed collateral value"
);
}
}
Loading