-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
test: port SP invariant test from v1
- Loading branch information
1 parent
54105e9
commit 757e0ab
Showing
7 changed files
with
293 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Submodule forge-std
updated
31 files
+6 −12 | .github/workflows/ci.yml | |
+3 −1 | .github/workflows/sync.yml | |
+0 −3 | .gitmodules | |
+1 −1 | README.md | |
+3 −3 | foundry.toml | |
+0 −1 | lib/ds-test | |
+1 −1 | package.json | |
+17 −48 | scripts/vm.py | |
+518 −225 | src/StdAssertions.sol | |
+10 −8 | src/StdChains.sol | |
+7 −11 | src/StdJson.sol | |
+201 −106 | src/StdStorage.sol | |
+179 −0 | src/StdToml.sol | |
+1 −1 | src/StdUtils.sol | |
+4 −4 | src/Test.sol | |
+550 −2 | src/Vm.sol | |
+51 −33 | src/mocks/MockERC20.sol | |
+46 −32 | src/mocks/MockERC721.sol | |
+39 −909 | test/StdAssertions.t.sol | |
+31 −26 | test/StdChains.t.sol | |
+19 −11 | test/StdCheats.t.sol | |
+49 −0 | test/StdJson.t.sol | |
+8 −8 | test/StdMath.t.sol | |
+159 −11 | test/StdStorage.t.sol | |
+49 −0 | test/StdToml.t.sol | |
+18 −18 | test/StdUtils.t.sol | |
+3 −3 | test/Vm.t.sol | |
+8 −0 | test/fixtures/test.json | |
+6 −0 | test/fixtures/test.toml | |
+1 −1 | test/mocks/MockERC20.t.sol | |
+1 −1 | test/mocks/MockERC721.t.sol |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
168
contracts/src/test/TestContracts/SPInvariantsTestHandler.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |