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 Fork Testing - Latest #811

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,6 @@ cache

# ngrok config is individual to each dev
ngrok.config.yml
.idea/
.idea/

packages/contracts/fuzzTests/corpus
1 change: 1 addition & 0 deletions packages/contracts/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
MAINNET_RPC_URL=""
16 changes: 14 additions & 2 deletions packages/contracts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,22 @@
Use this guide to install foundry:
https://book.getfoundry.sh/getting-started/installation

## Running foundry tests
## Running Foundry tests
- Simply `cd` into target test package:
- `cd packages/contracts`
- Run `forge test`

## Running Foundry fork tests for broken property reproducers
To run Foundry fork tests for reproducers:
- Add the reproducer test for the broken property to the `ForkToFoundry` contract
- Change the block number in the call to `vm.createSelectFork` to the block number in the coverage report that gets dynamically replaced in the [`Setup`](https://github.com/ebtc-protocol/ebtc/blob/925073f04bdfe5a6b594898d6491950087eee23b/packages/contracts/contracts/TestContracts/invariants/Setup.sol#L348) contract.
- The block number shown in the coverage report for the run will be the block at which Echidna forked mainnet from for your run.
- Rename the `.env.example` file to `.env` and add your rpc url.
- `cd` into the target test package:
- `cd packages/contracts`
- Run `forge test --mt <broken_property_reproducer_test>`


## Remappings:
Foundry test configuration is using existing hardhat dependencies, such as @openzeppelin etc.
They are declated in `remappings.txt`.
Expand All @@ -26,4 +37,5 @@ Then from root of the project run:
yarn install
```

And everything should work
And everything should work

1 change: 1 addition & 0 deletions packages/contracts/contracts/TestContracts/CRLens.sol
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ contract CRLens {
}

function getRealStake(bytes32 cdpId) external returns (uint256) {
hevm.prank(address(borrowerOperations));
cdpManager.syncAccounting(cdpId);
uint256 collShares = cdpManager.getCdpCollShares(cdpId);
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ abstract contract BeforeAfter is BaseStorageVariables {
}

function _before(bytes32 _cdpId) internal {
vars.priceBefore = priceFeedMock.fetchPrice();
vars.priceBefore = priceFeedMock.lastGoodPrice();

address ownerToCheck = sortedCdps.getOwnerAddress(_cdpId);
vars.userSurplusBefore = collSurplusPool.getSurplusCollShares(ownerToCheck);
Expand All @@ -101,6 +101,7 @@ abstract contract BeforeAfter is BaseStorageVariables {
vars.icrBefore = _cdpId != bytes32(0)
? cdpManager.getCachedICR(_cdpId, vars.priceBefore)
: 0;

vars.cdpCollBefore = _cdpId != bytes32(0) ? collBefore : 0;
vars.cdpDebtBefore = _cdpId != bytes32(0) ? debtBefore : 0;
vars.cdpStakeBefore = _cdpId != bytes32(0) ? crLens.getRealStake(_cdpId) : 0;
Expand All @@ -117,6 +118,7 @@ abstract contract BeforeAfter is BaseStorageVariables {
cdpManager.stEthIndex()
)
: (0, 0, 0);

vars.feeRecipientTotalCollBefore = collateral.balanceOf(activePool.feeRecipientAddress());
vars.feeRecipientCollSharesBefore = activePool.getFeeRecipientClaimableCollShares();
vars.feeRecipientCollSharesBalBefore = collateral.sharesOf(activePool.feeRecipientAddress());
Expand Down Expand Up @@ -161,7 +163,7 @@ abstract contract BeforeAfter is BaseStorageVariables {
address ownerToCheck = sortedCdps.getOwnerAddress(_cdpId);
vars.userSurplusAfter = collSurplusPool.getSurplusCollShares(ownerToCheck);

vars.priceAfter = priceFeedMock.fetchPrice();
vars.priceAfter = priceFeedMock.lastGoodPrice();

(, uint256 collAfter) = cdpManager.getSyncedDebtAndCollShares(_cdpId);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ abstract contract Properties is BeforeAfter, PropertiesDescriptions, Asserts, Pr
PriceFeedTestnet priceFeedMock
) internal returns (bool) {
bytes32 _first = sortedCdps.getFirst();
uint256 _price = priceFeedMock.fetchPrice();
uint256 _price = priceFeedMock.lastGoodPrice();
uint256 _firstICR = cdpManager.getCachedICR(_first, _price);
uint256 _TCR = cdpManager.getCachedTCR(_price);

Expand All @@ -214,7 +214,7 @@ abstract contract Properties is BeforeAfter, PropertiesDescriptions, Asserts, Pr
) internal returns (bool) {
bytes32 currentCdp = sortedCdps.getFirst();

uint256 _price = priceFeedMock.fetchPrice();
uint256 _price = priceFeedMock.lastGoodPrice();
if (_price == 0) return true;

while (currentCdp != bytes32(0)) {
Expand Down Expand Up @@ -272,13 +272,13 @@ abstract contract Properties is BeforeAfter, PropertiesDescriptions, Asserts, Pr
// TODO how to calculate "the dollar value of eBTC"?
// TODO how do we take into account underlying/shares into this calculation?
return
cdpManager.getCachedTCR(priceFeedMock.fetchPrice()) > 1e18
cdpManager.getCachedTCR(priceFeedMock.lastGoodPrice()) > 1e18
? (collateral.getPooledEthByShares(cdpManager.getSystemCollShares()) *
priceFeedMock.fetchPrice()) /
priceFeedMock.lastGoodPrice()) /
1e18 >=
eBTCToken.totalSupply()
: (collateral.getPooledEthByShares(cdpManager.getSystemCollShares()) *
priceFeedMock.fetchPrice()) /
priceFeedMock.lastGoodPrice()) /
1e18 <
eBTCToken.totalSupply();
}
Expand Down Expand Up @@ -414,7 +414,7 @@ abstract contract Properties is BeforeAfter, PropertiesDescriptions, Asserts, Pr
PriceFeedTestnet priceFeedTestnet,
ICollateralToken collateral
) internal returns (bool) {
uint256 curentPrice = priceFeedTestnet.fetchPrice();
uint256 curentPrice = priceFeedTestnet.lastGoodPrice();

bytes32 currentCdp = sortedCdps.getFirst();

Expand Down Expand Up @@ -466,8 +466,8 @@ abstract contract Properties is BeforeAfter, PropertiesDescriptions, Asserts, Pr
PriceFeedTestnet priceFeedMock,
CRLens crLens
) internal returns (bool) {
uint256 curentPrice = priceFeedMock.fetchPrice();
return crLens.quoteRealTCR() == cdpManager.getSyncedTCR(curentPrice);
uint256 currentPrice = priceFeedMock.fetchPrice();
return crLens.quoteRealTCR() == cdpManager.getSyncedTCR(currentPrice);
}

function invariant_GENERAL_13(
Expand Down Expand Up @@ -532,7 +532,7 @@ abstract contract Properties is BeforeAfter, PropertiesDescriptions, Asserts, Pr
uint256 n = cdpManager.getActiveCdpsCount();

// Get
uint256 price = priceFeedTestnet.fetchPrice();
uint256 price = priceFeedTestnet.lastGoodPrice();

// Get lists
bytes32[] memory cdpsFromCurrent = ls.sequenceLiqToBatchLiqWithPrice(n, price);
Expand All @@ -556,7 +556,7 @@ abstract contract Properties is BeforeAfter, PropertiesDescriptions, Asserts, Pr
}

function invariant_DUMMY_01(PriceFeedTestnet priceFeedTestnet) internal returns (bool) {
return priceFeedTestnet.fetchPrice() > 0;
return priceFeedTestnet.lastGoodPrice() > 0;
}

function invariant_BO_09(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import "./Properties.sol";
import "./Actor.sol";
import "../BaseStorageVariables.sol";

abstract contract TargetContractSetup is BaseStorageVariables, PropertiesConstants {
abstract contract Setup is BaseStorageVariables, PropertiesConstants {
using SafeMath for uint;

bytes4 internal constant BURN_SIG = bytes4(keccak256(bytes("burn(address,uint256)")));
Expand Down Expand Up @@ -340,7 +340,15 @@ abstract contract TargetContractSetup is BaseStorageVariables, PropertiesConstan

event Log(string);

// This is a fix to allow facilitate dynamic replacement that searches for the `vm.roll` statements.

function _setUpFork() internal {
IHevm vm = IHevm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);

// Add timestamp and block for Recon dynamic replacement
vm.roll(20996709);
vm.warp(1729305851);

// NOTE: Addresses from: https://gist.github.com/GalloDaSballo/75d77f8d0837821156fe061d0d8687e1
defaultGovernance = address(0xaDDeE229Bd103bb5B10C3CdB595A01c425dd3264);
ebtcDeployer = EBTCDeployer(0x5c42faC7eEa7e724986bB5e4F3B12912F046120a);
Expand Down Expand Up @@ -441,6 +449,9 @@ abstract contract TargetContractSetup is BaseStorageVariables, PropertiesConstan
actorsArray[i] = actors[addresses[i]];
}
simulator = new Simulator(actorsArray, cdpManager, sortedCdps, borrowerOperations);

// Make sure there is always an actor for any actor related calls in setup to be successful
actor = actors[addresses[0]];
}

// Simple canaries for fork health
Expand All @@ -462,7 +473,7 @@ abstract contract TargetContractSetup is BaseStorageVariables, PropertiesConstan
Actor actor = actors[USER3]; // USER3 is the whale CDP holder
uint256 _col = INITIAL_COLL_BALANCE / 2; // 50% of their initial collateral balance

uint256 price = priceFeedMock.getPrice();
uint256 price = priceFeedMock.lastGoodPrice();
uint256 _EBTCAmount = (_col * price) / cdpManager.CCR();

(success, ) = actor.proxy(
Expand Down Expand Up @@ -500,4 +511,34 @@ abstract contract TargetContractSetup is BaseStorageVariables, PropertiesConstan
assert(success);
}
}

function _setUpCdpFork() internal {
bool success;
bytes memory returnData;

(success, ) = actor.proxy(
address(collateral),
abi.encodeWithSelector(
CollateralTokenTester.approve.selector,
address(borrowerOperations),
18e18
)
);

assert(success);

// @audit Note the bias here, we assume that 18 ETH is enough to overcollateralize a 0.4EBTC position
(success, returnData) = actor.proxy(
address(borrowerOperations),
abi.encodeWithSelector(
BorrowerOperations.openCdp.selector,
4e17,
bytes32(0),
bytes32(0),
18e18
)
);

assert(eBTCToken.balanceOf(address(actor)) > 0);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import "../../EBTCDeployer.sol";
import "./Properties.sol";
import "./Actor.sol";
import "./BeforeAfter.sol";
import "./TargetContractSetup.sol";
import "./Setup.sol";
import "./Asserts.sol";
import "../BaseStorageVariables.sol";

Expand All @@ -41,7 +41,7 @@ abstract contract TargetFunctions is Properties {
uint256 ans;
bytes32 currentCdp = sortedCdps.getFirst();

uint256 _price = priceFeedMock.fetchPrice();
uint256 _price = priceFeedMock.lastGoodPrice();

while (currentCdp != bytes32(0)) {
if (cdpManager.getCachedICR(currentCdp, _price) < cdpManager.MCR()) {
Expand All @@ -59,7 +59,7 @@ abstract contract TargetFunctions is Properties {
uint256 i = 0;
bytes32 currentCdp = sortedCdps.getFirst();

uint256 _price = priceFeedMock.fetchPrice();
uint256 _price = priceFeedMock.lastGoodPrice();

while (currentCdp != bytes32(0)) {
uint256 _currentCdpDebt = cdpManager.getSyncedCdpDebt(currentCdp);
Expand Down Expand Up @@ -179,7 +179,7 @@ abstract contract TargetFunctions is Properties {
// Find the first cdp with ICR >= MCR
while (
currentBorrower != address(0) &&
cdpManager.getCachedICR(_cId, priceFeedMock.fetchPrice()) < cdpManager.MCR()
cdpManager.getCachedICR(_cId, priceFeedMock.lastGoodPrice()) < cdpManager.MCR()
) {
_cId = sortedCdps.getPrev(_cId);
currentBorrower = sortedCdps.getOwnerAddress(_cId);
Expand Down Expand Up @@ -225,7 +225,7 @@ abstract contract TargetFunctions is Properties {

_before(_cdpId);

uint256 _icrToLiq = cdpManager.getSyncedICR(_cdpId, priceFeedMock.getPrice());
uint256 _icrToLiq = cdpManager.getSyncedICR(_cdpId, priceFeedMock.lastGoodPrice());

(success, returnData) = actor.proxy(
address(cdpManager),
Expand Down Expand Up @@ -604,9 +604,8 @@ abstract contract TargetFunctions is Properties {
bytes32 _cdpId = _getFirstCdpWithIcrGteMcr();

_before(_cdpId);

{
uint price = priceFeedMock.fetchPrice();
uint price = priceFeedMock.lastGoodPrice();

(bytes32 firstRedemptionHintVal, uint256 partialRedemptionHintNICR, , ) = hintHelpers
.getRedemptionHints(_EBTCAmount, price, _maxIterations);
Expand Down Expand Up @@ -803,7 +802,7 @@ abstract contract TargetFunctions is Properties {
bytes memory returnData;

// we pass in CCR instead of MCR in case it's the first one
uint price = priceFeedMock.fetchPrice();
uint price = priceFeedMock.lastGoodPrice();

uint256 requiredCollAmount = (_EBTCAmount * cdpManager.CCR()) / (price);
uint256 minCollAmount = max(
Expand Down Expand Up @@ -878,7 +877,7 @@ abstract contract TargetFunctions is Properties {

gte(_EBTCAmount, borrowerOperations.MIN_CHANGE(), GENERAL_16);
gte(vars.cdpDebtAfter, borrowerOperations.MIN_CHANGE(), GENERAL_15);
require(invariant_BO_09(cdpManager, priceFeedMock.getPrice(), _cdpId), BO_09);
require(invariant_BO_09(cdpManager, priceFeedMock.lastGoodPrice(), _cdpId), BO_09);

_checkStakeInvariants();
} else {
Expand Down Expand Up @@ -1402,7 +1401,7 @@ abstract contract TargetFunctions is Properties {
///////////////////////////////////////////////////////

function setPrice(uint256 _newPrice) public virtual {
uint256 currentPrice = priceFeedMock.fetchPrice();
uint256 currentPrice = priceFeedMock.lastGoodPrice();
_newPrice = between(
_newPrice,
(currentPrice * 1e18) / MAX_PRICE_CHANGE_PERCENT,
Expand Down
Loading
Loading