Skip to content

Commit

Permalink
test: port SP invariant test from v1
Browse files Browse the repository at this point in the history
  • Loading branch information
danielattilasimon committed Apr 25, 2024
1 parent 54105e9 commit 757e0ab
Show file tree
Hide file tree
Showing 7 changed files with 293 additions and 2 deletions.
7 changes: 7 additions & 0 deletions contracts/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,11 @@ src = "src"
out = "out"
libs = ["lib"]

[invariant]
call_override = false
fail_on_revert = true
runs = 500
depth = 20
# shrink_run_limit = 0

# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
6 changes: 6 additions & 0 deletions contracts/src/Interfaces/IStabilityPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,12 @@ interface IStabilityPool is ILiquityBase {
*/
function getCompoundedBoldDeposit(address _depositor) external view returns (uint256);

// Public state variable getters
function P() external view returns (uint256);
function currentScale() external view returns (uint128);
function currentEpoch() external view returns (uint128);
function epochToScaleToSum(uint128 epoch, uint128 scale) external view returns (uint256);

/*
* Only callable by Active Pool, it pulls ETH and accounts for ETH received
*/
Expand Down
1 change: 0 additions & 1 deletion contracts/src/StabilityPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,6 @@ contract StabilityPool is LiquityBase, Ownable, CheckContract, IStabilityPool {
struct Snapshots {
uint256 S;
uint256 P;
uint256 G;
uint128 scale;
uint128 epoch;
}
Expand Down
104 changes: 104 additions & 0 deletions contracts/src/test/SPInvariants.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;

import {Test} from "forge-std/Test.sol";
import {IStabilityPool} from "../Interfaces/IStabilityPool.sol";
import {LiquityContracts, _deployAndConnectContracts} from "../deployment.sol";
import {SPInvariantsTestHandler} from "./TestContracts/SPInvariantsTestHandler.sol";

contract SPInvariantsTest is Test {
string[] actorLabels = ["Adam", "Barb", "Carl", "Dana", "Eric", "Fran", "Gabe", "Hope"];
address[] actors;

IStabilityPool stabilityPool;
SPInvariantsTestHandler handler;

function setUp() external {
LiquityContracts memory contracts = _deployAndConnectContracts();
stabilityPool = contracts.stabilityPool;

handler = new SPInvariantsTestHandler(
SPInvariantsTestHandler.ContractAddresses({
boldToken: contracts.boldToken,
borrowerOperations: contracts.borrowerOperations,
collateralToken: contracts.WETH,
priceFeed: contracts.priceFeed,
stabilityPool: contracts.stabilityPool,
troveManager: contracts.troveManager
})
);

targetContract(address(handler));

for (uint160 i = 0; i < actorLabels.length; ++i) {
address actor = address((i + 1) * uint160(0x1111111111111111111111111111111111111111));
vm.label(actor, actorLabels[i]);
targetSender(actor);
actors.push(actor);
}

assert(actors.length == actorLabels.length);
}

function invariant_allFundsClaimable() external {
uint256 stabilityPoolEth = stabilityPool.getETHBalance();
uint256 stabilityPoolBold = stabilityPool.getTotalBoldDeposits();
uint256 claimableEth = 0;
uint256 claimableBold = 0;

for (uint256 i = 0; i < actors.length; ++i) {
claimableEth += stabilityPool.getDepositorETHGain(actors[i]);
claimableBold += stabilityPool.getCompoundedBoldDeposit(actors[i]);
}

assertApproxEqAbsDecimal(stabilityPoolEth, claimableEth, 0.00001 ether, 18, "SP ETH !~ claimable ETH");
assertApproxEqAbsDecimal(stabilityPoolBold, claimableBold, 0.001 ether, 18, "SP BOLD !~ claimable BOLD");
}

function test_Issue_LossOfFundsAfterAnyTwoLiquidationsFollowingTinyP() external {
address adam = actors[0];
address barb = actors[1];

vm.prank(adam);
handler.openTrove(100_000 ether); // used as funds

vm.prank(barb); // can't use startPrank because of the handler's internal pranking
uint256 debt = handler.openTrove(2_000 ether);
vm.prank(barb);
handler.provideToSp(debt + debt / 1 ether + 1, false);
vm.prank(barb);
handler.liquidateMe();

vm.prank(barb);
debt = handler.openTrove(2_000 ether);
vm.prank(barb);
handler.provideToSp(debt, false);
vm.prank(barb);
handler.liquidateMe();

this.invariant_allFundsClaimable();

vm.prank(adam);
handler.provideToSp(80_000 ether, false);

this.invariant_allFundsClaimable();

vm.prank(barb);
debt = handler.openTrove(2_000 ether);
vm.prank(barb);
handler.liquidateMe();

this.invariant_allFundsClaimable();

vm.prank(barb);
debt = handler.openTrove(2_000 ether);
vm.prank(barb);
handler.liquidateMe();

vm.expectRevert(); // SP LUSD !~ claimable LUSD: ...
this.invariant_allFundsClaimable();

// Adam's wrekt
assertEq(stabilityPool.getCompoundedBoldDeposit(adam), 0);
}
}
168 changes: 168 additions & 0 deletions contracts/src/test/TestContracts/SPInvariantsTestHandler.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;

import {Test} from "forge-std/Test.sol";
import {console2 as console} from "forge-std/console2.sol";
import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {IBorrowerOperations} from "../../Interfaces/IBorrowerOperations.sol";
import {IBoldToken} from "../../Interfaces/IBoldToken.sol";
import {IStabilityPool} from "../../Interfaces/IStabilityPool.sol";
import {ITroveManager} from "../../Interfaces/ITroveManager.sol";
import {IPriceFeedTestnet} from "./Interfaces/IPriceFeedTestnet.sol";
import {mulDivCeil} from "../Utils/math.sol";

using {mulDivCeil} for uint256;

// Test parameters
uint256 constant OPEN_TROVE_BORROWED_MIN = 2_000 ether;
uint256 constant OPEN_TROVE_BORROWED_MAX = 100_000 ether;
uint256 constant OPEN_TROVE_ICR = 1.5 ether;
uint256 constant LIQUIDATION_ICR = 1.09 ether;

// Universal constants
uint256 constant ONE = 1 ether;
uint256 constant MCR = 1.1 ether;
uint256 constant CCR = 1.5 ether;
uint256 constant GAS_COMPENSATION = 200 ether;
uint256 constant COLL_GAS_COMPENSATION_DIVISOR = 200;

contract SPInvariantsTestHandler is Test {
struct ContractAddresses {
IBoldToken boldToken;
IBorrowerOperations borrowerOperations;
IERC20 collateralToken;
IPriceFeedTestnet priceFeed;
IStabilityPool stabilityPool;
ITroveManager troveManager;
}

IBoldToken immutable boldToken;
IBorrowerOperations immutable borrowerOperations;
IERC20 collateralToken;
IPriceFeedTestnet immutable priceFeed;
IStabilityPool immutable stabilityPool;
ITroveManager immutable troveManager;

uint256 immutable initialPrice;

// Ghost variables
uint256 myBold = 0;
uint256 spBold = 0;
uint256 spEth = 0;

// Fixtures
uint256[] fixtureDeposited;

constructor(ContractAddresses memory contracts) {
boldToken = contracts.boldToken;
borrowerOperations = contracts.borrowerOperations;
collateralToken = contracts.collateralToken;
priceFeed = contracts.priceFeed;
stabilityPool = contracts.stabilityPool;
troveManager = contracts.troveManager;

initialPrice = priceFeed.getPrice();
}

// Let us receive ETH gas compensation from liquidations
receive() external payable {}

function _getTroveId(address owner, uint256 i) internal pure returns (uint256) {
return uint256(keccak256(abi.encode(owner, i)));
}

function openTrove(uint256 borrowed) external returns (uint256 debt) {
uint256 i = troveManager.balanceOf(msg.sender);
vm.assume(troveManager.getTroveStatus(_getTroveId(msg.sender, i)) != 1);

borrowed = _bound(borrowed, OPEN_TROVE_BORROWED_MIN, OPEN_TROVE_BORROWED_MAX);
uint256 price = priceFeed.getPrice();
debt = borrowed + GAS_COMPENSATION;
uint256 coll = debt.mulDivCeil(OPEN_TROVE_ICR, price);
assertEqDecimal(coll * price / debt, OPEN_TROVE_ICR, 18, "Wrong ICR");

console.log(vm.getLabel(msg.sender), "> openTrove ", debt);
deal(address(collateralToken), msg.sender, coll);
vm.prank(msg.sender);
collateralToken.approve(address(borrowerOperations), coll);
vm.prank(msg.sender);
uint256 troveId = borrowerOperations.openTrove(msg.sender, i + 1, 0.005 ether, coll, borrowed, 0, 0, 0);
(uint256 actualDebt,,,,) = troveManager.getEntireDebtAndColl(troveId);
assertEqDecimal(debt, actualDebt, 18, "Wrong debt");

// Sweep funds
vm.prank(msg.sender);
boldToken.transfer(address(this), borrowed);
assertEqDecimal(boldToken.balanceOf(msg.sender), 0, 18, "Incomplete BOLD sweep");
myBold += borrowed;

// Use these interesting values as SP deposit amounts later
fixtureDeposited.push(debt);
fixtureDeposited.push(debt + debt / ONE + 1); // See https://github.com/liquity/dev/security/advisories/GHSA-m9f3-hrx8-x2g3
}

function provideToSp(uint256 deposited, bool useFixture) external {
vm.assume(myBold > 0);

uint256 ethBefore = collateralToken.balanceOf(msg.sender);
uint256 ethGain = stabilityPool.getDepositorETHGain(msg.sender);

// Poor man's fixturing, because Foundry's fixtures don't seem to work under invariant testing
if (useFixture && fixtureDeposited.length > 0) {
deposited = fixtureDeposited[_bound(deposited, 0, fixtureDeposited.length - 1)];
vm.assume(deposited <= myBold);
} else {
deposited = _bound(deposited, 1, myBold);
}

console.log(vm.getLabel(msg.sender), "> provideToSp ", deposited);
boldToken.transfer(msg.sender, deposited);
vm.prank(msg.sender);
stabilityPool.provideToSP(deposited);
emit log_named_decimal_uint(" spBold ", stabilityPool.getTotalBoldDeposits(), 18);

uint256 ethAfter = collateralToken.balanceOf(msg.sender);
assertEqDecimal(ethAfter, ethBefore + ethGain, 18, "Wrong ETH gain");

myBold -= deposited;
spBold += deposited;
spEth -= ethGain;

assertEqDecimal(spBold, stabilityPool.getTotalBoldDeposits(), 18, "Wrong SP BOLD balance");
assertEqDecimal(spEth, stabilityPool.getETHBalance(), 18, "Wrong SP ETH balance");
}

function liquidateMe() external {
uint256 troveId = _getTroveId(msg.sender, troveManager.balanceOf(msg.sender));
vm.assume(troveManager.getTroveStatus(troveId) == 1);

(uint256 debt, uint256 coll,,,) = troveManager.getEntireDebtAndColl(troveId);
vm.assume(debt <= spBold); // only interested in SP offset, no redistribution

uint256 ethBefore = collateralToken.balanceOf(address(this));
uint256 ethCompensation = coll / COLL_GAS_COMPENSATION_DIVISOR;

console.log("XXXX liquidate ", vm.getLabel(msg.sender));
priceFeed.setPrice(initialPrice * LIQUIDATION_ICR / OPEN_TROVE_ICR);

try troveManager.liquidate(troveId) {}
catch (bytes memory reason) {
// XXX ignore assertion failure inside liquidation (due to P = 0)
assertEq(reason.length, 0, "Unexpected revert in liquidate()");
vm.assume(false);
}

priceFeed.setPrice(initialPrice);
emit log_named_decimal_uint(" spBold ", stabilityPool.getTotalBoldDeposits(), 18);
emit log_named_decimal_uint(" P ", stabilityPool.P(), 18);

uint256 ethAfter = collateralToken.balanceOf(address(this));
assertEqDecimal(ethAfter, ethBefore + ethCompensation, 18, "Wrong ETH compensation");

spBold -= debt;
spEth += coll - ethCompensation;

assertEqDecimal(spBold, stabilityPool.getTotalBoldDeposits(), 18, "Wrong SP BOLD balance");
assertEqDecimal(spEth, stabilityPool.getETHBalance(), 18, "Wrong SP ETH balance");
}
}
7 changes: 7 additions & 0 deletions contracts/src/test/Utils/math.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;

function mulDivCeil(uint256 x, uint256 multiplier, uint256 divider) pure returns (uint256) {
assert(divider != 0);
return x == 0 ? 0 : (x * multiplier + divider - 1) / divider;
}

0 comments on commit 757e0ab

Please sign in to comment.