-
Notifications
You must be signed in to change notification settings - Fork 1
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
feat(test): fuzzed and symbolic tests #146
Changes from all commits
21bc497
5b1182b
bec1de2
1d0d05d
013f72f
388c657
caf3c62
b01b4e6
3753371
d12abd4
8cde0c0
0d0ca16
87a9acb
e946f09
59c2686
462c38e
68a6b59
fdd4617
cad698a
c143808
a65a9c0
350c39d
c60f5fa
c5c263f
09d332d
888ebe9
0c0e1c4
00cd7d1
ea280d9
5555afc
637aa83
42d2a04
302463c
23a8264
b3f2cf6
5abef51
a793e9d
61a0930
df49eb9
7bbb6cc
9d976ff
b101648
cd1ef62
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,3 +19,7 @@ broadcast/*/*/* | |
|
||
# Out dir | ||
out | ||
|
||
# echidna corpuses | ||
**/corpuses/* | ||
**/crytic-export/* |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,6 +29,7 @@ | |
"script:testnet": "forge script TestnetScript -vvvvv --rpc-url $SEPOLIA_RPC --broadcast --chain sepolia --private-key $SEPOLIA_DEPLOYER_PK --verify", | ||
"smock": "smock-foundry --contracts src/contracts", | ||
"test": "yarn test:integration && yarn test:unit", | ||
"test:echidna": "find test/invariants/fuzz -regex '.*\\.t\\.sol$' |cut -d '/' -f 4 | cut -d . -f 1 |xargs -I{} echidna test/invariants/fuzz/{}.t.sol --contract Fuzz{} --config test/invariants/fuzz/{}.yaml", | ||
"test:integration": "forge test --ffi --match-path 'test/integration/**' -vvv --isolate", | ||
"test:local": "FOUNDRY_FUZZ_RUNS=100 forge test -vvv", | ||
"test:scaffold": "bulloak check --fix test/unit/**/*.tree && forge fmt", | ||
|
@@ -42,6 +43,7 @@ | |
}, | ||
"dependencies": { | ||
"@cowprotocol/contracts": "github:cowprotocol/contracts.git#a10f40788a", | ||
"@crytic/properties": "https://github.com/crytic/properties.git", | ||
"@openzeppelin/contracts": "5.0.2", | ||
"composable-cow": "github:cowprotocol/composable-cow.git#24d556b", | ||
"cow-amm": "github:cowprotocol/cow-amm.git#6566128", | ||
|
@@ -53,7 +55,8 @@ | |
"@defi-wonderland/natspec-smells": "1.1.3", | ||
"@defi-wonderland/smock-foundry": "1.5.0", | ||
"forge-gas-snapshot": "github:marktoda/forge-gas-snapshot#9161f7c", | ||
"forge-std": "github:foundry-rs/forge-std#5475f85", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this will change the deployed build... |
||
"forge-std": "github:foundry-rs/forge-std#1.8.2", | ||
"halmos-cheatcodes": "github:a16z/halmos-cheatcodes#c0d8655", | ||
"husky": ">=8", | ||
"lint-staged": ">=10", | ||
"solhint-community": "4.0.0", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
# Tests Summary | ||
|
||
## Warning | ||
The repo is using solc 0.8.25, which compiles to the Cancun EVM version by default. Unfortunately, the hevm has no implementation of this EVM version ([or not yet](https://github.com/ethereum/hevm/issues/469#issuecomment-2220677206)). | ||
By using `ForTest` contracts in for property tests (which avoid using transient storage and `mcopy`) we managed to make the tests run under Cancun. | ||
|
||
## Unit tests | ||
Our unit tests are covering every branches, using the branched-tree technique with [Bulloak](https://github.com/alexfertel/bulloak). | ||
|
||
## Integration tests | ||
Integration tests are covering various happy paths and not-so-happy paths, on a mainnet fork. | ||
|
||
## Property tests | ||
We identified 24 properties. We challenged these either in a long-running fuzzing campaign or via symbolic execution (for 8 chosen properties). | ||
|
||
### Fuzzing campaign | ||
|
||
We used echidna to test these 23 properties. In addition to these, another fuzzing campaign as been led against the mathematical contracts (BNum and BMath). | ||
|
||
#### Limitations/future improvements | ||
Currently, the swap logic are tested against the swap in/out functions (and, in a similar way, liquidity management via the join/exit function). | ||
|
||
### Formal verification: Symbolic Execution | ||
We managed to test 10 properties out of the 23. Properties not tested are either not easily challenged with symbolic execution (statefullness needed) or limited by Halmos itself (hitting loop unrolling boundaries in the implementation for instance). | ||
|
||
Additional properties from BNum were tested independently too (with severe limitations due to previously mentionned loop unrolling boundaries). | ||
|
||
# Notes | ||
The bmath corresponding equations are: | ||
|
||
**Spot price:** | ||
$$\text{spotPrice} = \frac{\text{tokenBalanceIn}/\text{tokenWeightIn}}{\text{tokenBalanceOut}/\text{tokenWeightOut}} \cdot \frac{1}{1 - \text{swapFee}}$$ | ||
|
||
|
||
**Out given in:** | ||
$$\text{tokenAmountOut} = \text{tokenBalanceOut} \cdot \left( 1 - \left( \frac{\text{tokenBalanceIn}}{\text{tokenBalanceIn} + \left( \text{tokenAmountIn} \cdot \left(1 - \text{swapFee}\right)\right)} \right)^{\frac{\text{tokenWeightIn}}{\text{tokenWeightOut}}} \right)$$ | ||
|
||
|
||
**In given out:** | ||
$$\text{tokenAmountIn} = \frac{\text{tokenBalanceIn} \cdot \left( \frac{\text{tokenBalanceOut}}{\text{tokenBalanceOut} - \text{tokenAmountOut}} \right)^{\frac{\text{tokenWeightOut}}{\text{tokenWeightIn}}} - 1}{1 - \text{swapFee}}$$ | ||
|
||
|
||
**Pool out given single in** | ||
$$\text{poolAmountOut} = \left(\frac{\text{tokenAmountIn} \cdot \left(1 - \left(1 - \frac{\text{tokenWeightIn}}{\text{totalWeight}}\right) \cdot \text{swapFee}\right) + \text{tokenBalanceIn}}{\text{tokenBalanceIn}}\right)^{\frac{\text{tokenWeightIn}}{\text{totalWeight}}} \cdot \text{poolSupply} - \text{poolSupply}$$ | ||
|
||
|
||
**Single in given pool out** | ||
$$\text{tokenAmountIn} = \frac{\left(\frac{\text{poolSupply} + \text{poolAmountOut}}{\text{poolSupply}}\right)^{\frac{1}{\frac{\text{weightIn}}{\text{totalWeight}}}} \cdot \text{balanceIn} - \text{balanceIn}}{\left(1 - \frac{\text{weightIn}}{\text{totalWeight}}\right) \cdot \text{swapFee}}$$ | ||
|
||
|
||
**Single out given pool in** | ||
$$\text{tokenAmountOut} = \left( \text{tokenBalanceOut} - \left( \frac{\text{poolSupply} - \left(\text{poolAmountIn} \cdot \left(1 - \text{exitFee}\right)\right)}{\text{poolSupply}} \right)^{\frac{1}{\frac{\text{tokenWeightOut}}{\text{totalWeight}}}} \cdot \text{tokenBalanceOut} \right) \cdot \left(1 - \left(1 - \frac{\text{tokenWeightOut}}{\text{totalWeight}}\right) \cdot \text{swapFee}\right)$$ | ||
|
||
|
||
**Pool in given single out** | ||
$$\text{poolAmountIn} = \frac{\text{poolSupply} - \left( \frac{\text{tokenBalanceOut} - \frac{\text{tokenAmountOut}}{1 - \left(1 - \frac{\text{tokenWeightOut}}{\text{totalWeight}}\right) \cdot \text{swapFee}}}{\text{tokenBalanceOut}} \right)^{\frac{\text{tokenWeightOut}}{\text{totalWeight}}} \cdot \text{poolSupply}}{1 - \text{exitFee}}$$ | ||
|
||
|
||
BNum bpow is based on exponentiation by squaring and hold true because (see dapphub dsmath): https://github.com/dapphub/ds-math/blob/e70a364787804c1ded9801ed6c27b440a86ebd32/src/math.sol#L62 | ||
``` | ||
// If n is even, then x^n = (x^2)^(n/2). | ||
// If n is odd, then x^n = x * x^(n-1), | ||
// and applying the equation for even x gives | ||
// x^n = x * (x^2)^((n-1) / 2). | ||
// | ||
// Also, EVM division is flooring and | ||
// floor[(n-1) / 2] = floor[n / 2]. | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
{ | ||
"rules": { | ||
"custom-errors": "off", | ||
"no-empty-blocks":"off", | ||
"reason-string": "off", | ||
"reentrancy": "off", | ||
"style-guide-casing": [ "warn", { | ||
"ignoreVariables": true, | ||
"ignorePublicFunctions": true, | ||
"ignoreExternalFunctions": true | ||
}] | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
| Properties | Type | Id | Halmos | Echidna | | ||
| ------------------------------------------------------------------------------------------- | ------------------- | --- | ------ | ------- | | ||
| BFactory should always be able to deploy new pools | Unit | 1 | [x] | [x] | | ||
| BFactory's BDao should always be modifiable by the current BDao | Unit | 2 | [x] | [x] | | ||
| BFactory should always be able to transfer the BToken to the BDao, if called by it | Unit | 3 | [x] | [x] | | ||
| the amount received can never be less than min amount out | Unit | 4 | :( | [x] | | ||
| the amount spent can never be greater than max amount in | Unit | 5 | :( | [x] | | ||
| swap fee can only be 0 (cow pool) | Valid state | 6 | | [x] | | ||
| total weight can be up to 50e18 | Variable transition | 7 | [x] | [x] | | ||
| BToken increaseApproval should increase the approval of the address by the amount* | Variable transition | 8 | | [x] | | ||
| BToken decreaseApproval should decrease the approval to max(old-amount, 0)* | Variable transition | 9 | | [x] | | ||
| a pool can either be finalized or not finalized | Valid state | 10 | | [x] | | ||
| a finalized pool cannot switch back to non-finalized | State transition | 11 | | [x] | | ||
| a non-finalized pool can only be finalized when the controller calls finalize() | State transition | 12 | [x] | [x] | | ||
| an exact amount in should always earn the amount out calculated in bmath | High level | 13 | :( | [x] | | ||
| an exact amount out is earned only if the amount in calculated in bmath is transfered | High level | 14 | :( | [x] | | ||
| there can't be any amount out for a 0 amount in | High level | 15 | :( | [x] | | ||
| the pool btoken can only be minted/burned in the join and exit operations | High level | 16 | | [x] | | ||
| ~~a direct token transfer can never reduce the underlying amount of a given token per BPT~~ | High level | 17 | :( | # | | ||
| ~~the amount of underlying token when exiting should always be the amount calculated in bmath~~ | High level | 18 | :( | # | | ||
| a swap can only happen when the pool is finalized | High level | 19 | | [x] | | ||
| bounding and unbounding token can only be done on a non-finalized pool, by the controller | High level | 20 | [x] | [x] | | ||
| there always should be between MIN_BOUND_TOKENS and MAX_BOUND_TOKENS bound in a pool | High level | 21 | | [x] | | ||
| only the settler can commit a hash | High level | 22 | [x] | [x] | | ||
| when a hash has been commited, only this order can be settled | High level | 23 | [ ] | [ ] | | ||
| BToken should not break the ToB ERC20 properties** | High level | 24 | | [x] | | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. wanna add it? I guess that should be fairly easy in the swap post-conditions |
||
| Spot price after swap is always greater than before swap | High level | 25 | | [x] | | ||
|
||
> (*) Bundled with 24 | ||
|
||
> (**) [Trail of Bits ERC20 properties](https://github.com/crytic/properties?tab=readme-ov-file#erc20-tests) | ||
|
||
<br>`[ ]` planed to implement and still to do | ||
<br>`[x]` implemented and tested | ||
<br>`:(` implemented but test not passing due to an external factor (tool limitation - eg halmos max unrolling loop, etc) | ||
<br>`#` implemented but deprecated feature / property | ||
<br>`` empty not implemented and will not be (design, etc) | ||
|
||
# Unit-test properties for the math libs (BNum and BMath): | ||
|
||
btoi should always return the floor(a / BONE) == (a - a%BONE) / BONE | ||
|
||
bfloor should always return (a - a % BONE) | ||
|
||
badd should be commutative | ||
badd should be associative | ||
badd should have 0 as identity | ||
badd result should always be gte its terms | ||
badd should never sum terms which have a sum gt uint max | ||
badd should have bsub as reverse operation | ||
|
||
bsub should not be associative | ||
bsub should have 0 as identity | ||
bsub result should always be lte its terms | ||
bsub should alway revert if b > a (duplicate with previous tho) | ||
|
||
bsubSign should not be commutative sign-wise | ||
bsubSign should be commutative value-wise | ||
bsubSign result should always be negative if b > a | ||
bsubSign result should always be positive if a > b | ||
bsubSign result should always be 0 if a == b | ||
|
||
bmul should be commutative | ||
bmul should be associative | ||
bmul should be distributive | ||
bmul should have 1 as identity | ||
bmul should have 0 as absorving | ||
bmul result should always be gte a and b | ||
|
||
bdiv should be bmul reverse operation // <-- unsolved | ||
bdiv should have 1 as identity | ||
bdiv should revert if b is 0 // <-- impl with wrapper to have low lvl call | ||
bdiv result should be lte a | ||
|
||
bpowi should return 1 if exp is 0 | ||
0 should be absorbing if base | ||
1 should be identity if base | ||
1 should be identity if exp | ||
bpowi should be distributive over mult of the same base x^a * x^b == x^(a+b) | ||
bpowi should be distributive over mult of the same exp a^x * b^x == (a*b)^x | ||
power of a power should mult the exp (x^a)^b == x^(a*b) | ||
|
||
bpow should return 1 if exp is 0 | ||
0 should be absorbing if base | ||
1 should be identity if base | ||
1 should be identity if exp | ||
bpow should be distributive over mult of the same base x^a * x^b == x^(a+b) | ||
bpow should be distributive over mult of the same exp a^x * b^x == (a*b)^x | ||
power of a power should mult the exp (x^a)^b == x^(a*b) | ||
|
||
calcOutGivenIn should be inv with calcInGivenOut | ||
calcInGivenOut should be inv with calcOutGivenIn | ||
~~calcPoolOutGivenSingleIn should be inv with calcSingleInGivenPoolOut~~ | ||
~~calcSingleOutGivenPoolIn should be inv with calcPoolInGivenSingleOut~~ |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity 0.8.25; | ||
|
||
import {EchidnaTest} from '../helpers/AdvancedTestsUtils.sol'; | ||
import {BMath} from 'contracts/BMath.sol'; | ||
|
||
contract FuzzBMath is EchidnaTest { | ||
BMath bMath; | ||
|
||
uint256 immutable MIN_WEIGHT; | ||
uint256 immutable MAX_WEIGHT; | ||
uint256 immutable MIN_FEE; | ||
uint256 immutable MAX_FEE; | ||
|
||
/** | ||
* NOTE: These values were chosen to pass the fuzzing tests | ||
* @dev Reducing BPOW_PRECISION may allow broader range of values increasing the gas cost | ||
*/ | ||
uint256 constant MAX_BALANCE = 1_000_000e18; | ||
uint256 constant MIN_BALANCE = 100e18; | ||
uint256 constant MIN_AMOUNT = 1e12; | ||
uint256 constant TOLERANCE_PRECISION = 1e18; | ||
uint256 constant MAX_TOLERANCE = 1e18 + 1e15; //0.1% | ||
|
||
constructor() { | ||
bMath = new BMath(); | ||
|
||
MIN_WEIGHT = bMath.MIN_WEIGHT(); | ||
MAX_WEIGHT = bMath.MAX_WEIGHT(); | ||
MIN_FEE = bMath.MIN_FEE(); | ||
MAX_FEE = bMath.MAX_FEE(); | ||
} | ||
|
||
// calcOutGivenIn should be inverse of calcInGivenOut | ||
function testCalcInGivenOut_InvCalcInGivenOut( | ||
uint256 tokenBalanceIn, | ||
uint256 tokenWeightIn, | ||
uint256 tokenBalanceOut, | ||
uint256 tokenWeightOut, | ||
uint256 tokenAmountIn, | ||
uint256 swapFee | ||
) public view { | ||
tokenWeightIn = clamp(tokenWeightIn, MIN_WEIGHT, MAX_WEIGHT); | ||
tokenWeightOut = clamp(tokenWeightOut, MIN_WEIGHT, MAX_WEIGHT); | ||
tokenAmountIn = clamp(tokenAmountIn, MIN_AMOUNT, MAX_BALANCE); | ||
tokenBalanceOut = clamp(tokenBalanceOut, MIN_BALANCE, MAX_BALANCE); | ||
tokenBalanceIn = clamp(tokenBalanceIn, MIN_BALANCE, MAX_BALANCE); | ||
swapFee = clamp(swapFee, MIN_FEE, MAX_FEE); | ||
|
||
uint256 calc_tokenAmountOut = | ||
bMath.calcOutGivenIn(tokenBalanceIn, tokenWeightIn, tokenBalanceOut, tokenWeightOut, tokenAmountIn, swapFee); | ||
|
||
uint256 calc_tokenAmountIn = | ||
bMath.calcInGivenOut(tokenBalanceIn, tokenWeightIn, tokenBalanceOut, tokenWeightOut, calc_tokenAmountOut, swapFee); | ||
|
||
assert( | ||
tokenAmountIn >= calc_tokenAmountIn | ||
? (tokenAmountIn * TOLERANCE_PRECISION / calc_tokenAmountIn) <= MAX_TOLERANCE | ||
: (calc_tokenAmountIn * TOLERANCE_PRECISION / tokenAmountIn) <= MAX_TOLERANCE | ||
); | ||
} | ||
|
||
// calcInGivenOut should be inverse of calcOutGivenIn | ||
function testCalcOutGivenIn_InvCalcOutGivenIn( | ||
uint256 tokenBalanceIn, | ||
uint256 tokenWeightIn, | ||
uint256 tokenBalanceOut, | ||
uint256 tokenWeightOut, | ||
uint256 tokenAmountOut, | ||
uint256 swapFee | ||
) public view { | ||
tokenWeightIn = clamp(tokenWeightIn, MIN_WEIGHT, MAX_WEIGHT); | ||
tokenWeightOut = clamp(tokenWeightOut, MIN_WEIGHT, MAX_WEIGHT); | ||
tokenAmountOut = clamp(tokenAmountOut, MIN_AMOUNT, MAX_BALANCE); | ||
tokenBalanceOut = clamp(tokenBalanceOut, MIN_BALANCE, MAX_BALANCE); | ||
tokenBalanceIn = clamp(tokenBalanceIn, MIN_BALANCE, MAX_BALANCE); | ||
swapFee = clamp(swapFee, MIN_FEE, MAX_FEE); | ||
|
||
uint256 calc_tokenAmountIn = | ||
bMath.calcInGivenOut(tokenBalanceOut, tokenWeightOut, tokenBalanceIn, tokenWeightIn, tokenAmountOut, swapFee); | ||
|
||
uint256 calc_tokenAmountOut = | ||
bMath.calcOutGivenIn(tokenBalanceOut, tokenWeightOut, tokenBalanceIn, tokenWeightIn, calc_tokenAmountIn, swapFee); | ||
|
||
assert( | ||
tokenAmountOut >= calc_tokenAmountOut | ||
? (tokenAmountOut * TOLERANCE_PRECISION / calc_tokenAmountOut) <= MAX_TOLERANCE | ||
: (calc_tokenAmountOut * TOLERANCE_PRECISION / tokenAmountOut) <= MAX_TOLERANCE | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
# https://github.com/crytic/echidna/blob/master/tests/solidity/basic/default.yaml for more options | ||
testMode: assertion | ||
corpusDir: test/invariants/fuzz/corpuses/BMath/ | ||
coverageFormats: ["html","lcov"] | ||
allContracts: false | ||
testLimit: 50000 | ||
seqLen: 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this a halmos/echidna thing? we should issue them
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
crytic/properties#59