Skip to content

Commit

Permalink
Merge pull request #176 from overlay-market/feat/add-invariants
Browse files Browse the repository at this point in the history
Add invariant tests and fix dead oi shares bug
  • Loading branch information
TomasCImach authored May 20, 2024
2 parents 94fa241 + 128b081 commit 71a3187
Show file tree
Hide file tree
Showing 7 changed files with 350 additions and 34 deletions.
36 changes: 36 additions & 0 deletions .github/workflows/test-echidna.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Echidna Test

on:
push:
branches: [ main, staging ]
pull_request:
branches: [ main, staging ]

env:
FOUNDRY_PROFILE: ci

jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly

- name: Compile contracts
run: |
forge build --build-info
- name: Run Echidna
uses: crytic/echidna-action@v2
with:
files: .
contract: MarketEchidnaAdvanced
config: tests/invariants/MarketEchidnaAdvanced.yaml
crytic-args: --ignore-compile --foundry-out-directory forge-out
5 changes: 5 additions & 0 deletions contracts/OverlayV1Market.sol
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,11 @@ contract OverlayV1Market is IOverlayV1Market, Pausable {
// oiShares and fraction remaining of initial position
pos.oiShares -= uint240(pos.oiSharesCurrent(fraction));
pos.fractionRemaining = pos.updatedFractionRemaining(fraction);
// ensure there are no dead shares left
if (pos.fractionRemaining == 0 && pos.oiShares > 0) {
_reduceOIAndOIShares(pos, ONE);
pos.oiShares = 0;
}
positions.set(msg.sender, positionId, pos);
}

Expand Down
77 changes: 77 additions & 0 deletions medusa.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
{
"fuzzing": {
"workers": 10,
"workerResetLimit": 50,
"timeout": 0,
"testLimit": 0,
"callSequenceLength": 100,
"corpusDirectory": "",
"coverageEnabled": true,
"deploymentOrder": ["MarketEchidnaAdvanced"],
"constructorArgs": {},
"deployerAddress": "0x30000",
"senderAddresses": [
"0x1000000000000000000000000000000000000000",
"0x2000000000000000000000000000000000000000"
],
"blockNumberDelayMax": 60480,
"blockTimestampDelayMax": 604800,
"blockGasLimit": 125000000,
"transactionGasLimit": 12500000,
"testing": {
"stopOnFailedTest": true,
"stopOnFailedContractMatching": true,
"stopOnNoTests": true,
"testAllContracts": false,
"traceAll": false,
"assertionTesting": {
"enabled": true,
"testViewMethods": false,
"assertionModes": {
"failOnCompilerInsertedPanic": false,
"failOnAssertion": true,
"failOnArithmeticUnderflow": false,
"failOnDivideByZero": false,
"failOnEnumTypeConversionOutOfBounds": false,
"failOnIncorrectStorageAccess": false,
"failOnPopEmptyArray": false,
"failOnOutOfBoundsArrayAccess": false,
"failOnAllocateTooMuchMemory": false,
"failOnCallUninitializedVariable": false
}
},
"propertyTesting": {
"enabled": false,
"testPrefixes": [
"fuzz_"
]
},
"optimizationTesting": {
"enabled": false,
"testPrefixes": [
"optimize_"
]
}
},
"chainConfig": {
"codeSizeCheckDisabled": true,
"cheatCodes": {
"cheatCodesEnabled": true,
"enableFFI": false
}
}
},
"compilation": {
"platform": "crytic-compile",
"platformConfig": {
"target": "tests/invariants/MarketEchidnaAdvanced.t.sol",
"solcVersion": "",
"exportDirectory": "",
"args": ["--foundry-out-directory", "forge-out"]
}
},
"logging": {
"level": "info",
"logDirectory": ""
}
}
49 changes: 25 additions & 24 deletions tests/invariants/MarketEchidna.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {OverlayV1Factory} from "../../contracts/OverlayV1Factory.sol";
import {OverlayV1Market} from "../../contracts/OverlayV1Market.sol";
import {OverlayV1Token} from "../../contracts/OverlayV1Token.sol";
import {OverlayV1FeedFactoryMock} from "../../contracts/mocks/OverlayV1FeedFactoryMock.sol";
import {OverlayV1FeedMock} from "../../contracts/mocks/OverlayV1FeedMock.sol";
import {AggregatorMock} from "../../contracts/mocks/AggregatorMock.sol";
import {GOVERNOR_ROLE, MINTER_ROLE} from "../../contracts/interfaces/IOverlayV1Token.sol";
import {TestUtils} from "./TestUtils.sol";
Expand All @@ -18,22 +19,26 @@ interface IHevm {
// solc-select install 0.8.10
// solc-select use 0.8.10
//
// run from base project directory with:
// run with echidna from base project directory with:
// echidna tests/invariants/MarketEchidna.t.sol --contract MarketEchidna --config tests/invariants/MarketEchidna.yaml
//
// run with medusa from base project directory with:
// medusa fuzz
//
// Reference: https://github.com/overlay-market/v1-core/blob/main/tests/markets/conftest.py
contract MarketEchidna {
IHevm hevm = IHevm(address(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D));
IHevm hevm = IHevm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);

// contracts required for test
OverlayV1Factory factory;
OverlayV1Market market;
OverlayV1Token ovl;
OverlayV1FeedMock feed;
AggregatorMock sequencer_oracle;

// make these constant to match Echidna config
address public constant ALICE = address(0x1000000000000000000000000000000000000000);
address public constant BOB = address(0x2000000000000000000000000000000000000000);
address constant ALICE = address(0x1000000000000000000000000000000000000000);
address constant BOB = address(0x2000000000000000000000000000000000000000);

uint256 constant MIN_COLLATERAL = 1e14;
uint256 constant CAP_NOTIONAL = 8e23;
Expand All @@ -46,13 +51,10 @@ contract MarketEchidna {
factory = new OverlayV1Factory(address(ovl), address(0x111), address(sequencer_oracle), 0);
// market will be later deployed by factory

// ovl config
uint256 ovlSupply = 8_000_000e18;
// ovl config; fund test accounts
ovl.grantRole(MINTER_ROLE, address(this));
ovl.mint(address(this), ovlSupply);
ovl.renounceRole(MINTER_ROLE, address(this));
ovl.transfer(ALICE, ovlSupply / 2);
ovl.transfer(BOB, ovlSupply / 2);
ovl.mint(ALICE, 4_000_000e18);
ovl.mint(BOB, 4_000_000e18);

// factory config
ovl.grantRole(GOVERNOR_ROLE, address(this));
Expand All @@ -62,7 +64,7 @@ contract MarketEchidna {
factory.addFeedFactory(address(feedFactory));

// market config and deployment
address feed = feedFactory.deployFeed({price: 1e29, reserve: 2_000_000e18});
feed = OverlayV1FeedMock(feedFactory.deployFeed({price: 1e25, reserve: 2_000_000e18}));
uint256[15] memory params = [
uint256(122000000000), // k
500000000000000000, // lmbda
Expand All @@ -78,22 +80,21 @@ contract MarketEchidna {
750000000000000, // tradingFeeRate
MIN_COLLATERAL, // minCollateral
25000000000000, // priceDriftUpperLimit
250 // averageBlockTime // FIXME: this will be different in Arbitrum
250 // averageBlockTime
];
market = OverlayV1Market(factory.deployMarket(address(feedFactory), feed, params));
market = OverlayV1Market(factory.deployMarket(address(feedFactory), address(feed), params));
hevm.prank(ALICE);
ovl.approve(address(market), ovlSupply / 2);
ovl.approve(address(market), type(uint256).max);
hevm.prank(BOB);
ovl.approve(address(market), ovlSupply / 2);
ovl.approve(address(market), type(uint256).max);
}

// Invariant 1) `oiOverweight * oiUnderweight` remains constant after funding payments

// event to raise if the invariant is broken
event OiAfterFunding(uint256 oiProductBefore, uint256 oiProductAfter);
event OiAfterFunding(uint256 oiProductBefore, uint256 oiProductAfter, uint256 price);

function invariant_oi_product_after_funding() public returns (bool) {
uint256 lastUpdate = market.timestampUpdateLast();
function check_oi_product_after_funding(uint256 timeElapsed) public {
uint256 oiLong = market.oiLong();
uint256 oiShort = market.oiShort();
uint256 oiOverweightBefore = oiLong > oiShort ? oiLong : oiShort;
Expand All @@ -104,17 +105,17 @@ contract MarketEchidna {
(uint256 oiOverweightAfter, uint256 oiUnderweightAfter) = market.oiAfterFunding({
oiOverweight: oiOverweightBefore,
oiUnderweight: oiUnderweightBefore,
// FIXME: block.timestamp is always 0 in the test
// timeElapsed: block.timestamp - lastUpdate
timeElapsed: 1
timeElapsed: timeElapsed
});

uint256 oiProductAfter = oiOverweightAfter * oiUnderweightAfter;

// only visible when invariant fails
emit OiAfterFunding(oiProductBefore, oiProductAfter);
emit OiAfterFunding(oiProductBefore, oiProductAfter, feed.price());

// 10% tolerance
assert(TestUtils.isApproxEqRel(oiProductBefore, oiProductAfter, 10e16));

// 0.5% tolerance
return (TestUtils.isApproxEqRel(oiProductBefore, oiProductAfter, 0.5e16));
revert(); // revert state changes during the invariant check
}
}
4 changes: 2 additions & 2 deletions tests/invariants/MarketEchidna.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ sender: ["0x1000000000000000000000000000000000000000", "0x2000000000000000000000
# fuzzer executes
corpusDir: "./tests/invariants/coverage-echidna"

# use same prefix as Foundry invariant tests
prefix: "invariant_"
# use assertion mode to include dynamic parameters in the invariant checks
testMode: assertion
Loading

0 comments on commit 71a3187

Please sign in to comment.