From 01e8f915ffcce9e601e8198867b0394242384bd1 Mon Sep 17 00:00:00 2001 From: "Dr. GoNoGo" <83670532+drgorillamd@users.noreply.github.com> Date: Fri, 2 Aug 2024 10:14:19 +0200 Subject: [PATCH] feat(test): fuzzed and symbolic tests (#146) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: echidna and halmos bmath bnum protocol * fix: remove tests halmos cannot handle * fix: setup cow pool * chore(wip): echidna * feat: clamp util * feat(echidna): prop and tob erc20 * test(echidna): protocol prop (part) * test(echidna): swap/join prop (part) * test(echidna): all properties * test(halmos): part * feat: summary prop md and last tests * chore: reorg * feat: more bnum pain * chore: format Co-authored-by: Weißer Hase * chore: format Co-authored-by: Weißer Hase * chore: typo Co-authored-by: Weißer Hase * chore: typo Co-authored-by: Weißer Hase * test(echidna): more bnum tests * chore: unused import * chore: more assert in protocol * feat: evm version * chore: summary * fix: direct transfer then swapExactOut case * chore: fmt * feat: shanghai in summary * fix: typo * chore: typo * chore: revert consistency * chore: summary fmt * chore: fmt * Fix/fuzz tests improvements (#178) * chore: fix linter setup * chore: update smocked files * chore: add missing spdx identifier * chore: add an npm script to run echidna tests * test: ensure SpotPriceAfterBelowSpotPriceBefore is never thrown * chore: fix natspec issues * test: ensure fuzz_joinExitPool body is runnable * chore: cleaning up fuzz (#179) * feat: creating BCoWPoolForTest to avoid modifying core contracts * fix: test:echidna script * fix: safeTransfer issue with echidna * chore: update test contract licenses * test: document property 25 * chore: remove unimplemented function --------- Co-authored-by: Weißer Hase * chore: update forge snapshots * chore: update gas snapshots * chore: updating Properties document after merge * dev: improving bmath fuzz test (#184) * feat: improving bmath fuzz test * chore: updating Properties file * feat: adding min amount to test environment * test: ensure results are 0.1% from each other (#187) --------- Co-authored-by: teddy * docs: update test summary (#189) * chore: fuzz sym fixes (#190) * test: deal with previously commented code * chore: pass name and symbol to ForTest contracts * fix: work around hevm not supporting mcopy * fix: remove unprovable property and replace it with two provable ones --------- Co-authored-by: Weißer Hase Co-authored-by: teddy Co-authored-by: Weißer Hase --- .gitignore | 4 + foundry.toml | 3 +- package.json | 5 +- remappings.txt | 2 + src/contracts/BCoWPool.sol | 2 +- src/contracts/BFactory.sol | 2 +- test/SUMMARY.md | 68 +++ test/invariants/.solhint.json | 13 + test/invariants/PROPERTIES.md | 94 ++++ test/invariants/fuzz/BMath.t.sol | 91 ++++ test/invariants/fuzz/BMath.yaml | 7 + test/invariants/fuzz/BNum.t.sol | 473 +++++++++++++++++ test/invariants/fuzz/BNum.yaml | 6 + test/invariants/fuzz/BToken.t.sol | 63 +++ test/invariants/fuzz/BToken.yaml | 6 + test/invariants/fuzz/Protocol.t.sol | 495 ++++++++++++++++++ test/invariants/fuzz/Protocol.yaml | 6 + .../invariants/helpers/AdvancedTestsUtils.sol | 68 +++ .../invariants/helpers/BCoWFactoryForTest.sol | 26 + test/invariants/helpers/BCoWPoolForTest.sol | 46 ++ test/invariants/helpers/MockSettler.sol | 21 + test/invariants/symbolic/BNum.t.sol | 261 +++++++++ test/invariants/symbolic/Protocol.t.sol | 287 ++++++++++ yarn.lock | 471 ++++++++++++++++- 24 files changed, 2510 insertions(+), 10 deletions(-) create mode 100644 test/SUMMARY.md create mode 100644 test/invariants/.solhint.json create mode 100644 test/invariants/PROPERTIES.md create mode 100644 test/invariants/fuzz/BMath.t.sol create mode 100644 test/invariants/fuzz/BMath.yaml create mode 100644 test/invariants/fuzz/BNum.t.sol create mode 100644 test/invariants/fuzz/BNum.yaml create mode 100644 test/invariants/fuzz/BToken.t.sol create mode 100644 test/invariants/fuzz/BToken.yaml create mode 100644 test/invariants/fuzz/Protocol.t.sol create mode 100644 test/invariants/fuzz/Protocol.yaml create mode 100644 test/invariants/helpers/AdvancedTestsUtils.sol create mode 100644 test/invariants/helpers/BCoWFactoryForTest.sol create mode 100644 test/invariants/helpers/BCoWPoolForTest.sol create mode 100644 test/invariants/helpers/MockSettler.sol create mode 100644 test/invariants/symbolic/BNum.t.sol create mode 100644 test/invariants/symbolic/Protocol.t.sol diff --git a/.gitignore b/.gitignore index 9209344d..00955047 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,7 @@ broadcast/*/*/* # Out dir out + +# echidna corpuses +**/corpuses/* +**/crytic-export/* \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index 1a2224e0..e14b11ab 100644 --- a/foundry.toml +++ b/foundry.toml @@ -17,7 +17,8 @@ fs_permissions = [{ access = "read-write", path = ".forge-snapshots/"}] # 2018: function can be view, so far only caused by mocks # 2394: solc insists on reporting on every transient storage use # 5574, 3860: bytecode size limit, so far only caused by test contracts -ignored_error_codes = [2018, 2394, 5574, 3860] +# 1878: Some imports don't have the license identifier +ignored_error_codes = [2018, 2394, 5574, 3860, 1878] deny_warnings = true [profile.optimized] diff --git a/package.json b/package.json index b72fc12a..11f37ad1 100644 --- a/package.json +++ b/package.json @@ -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", + "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", diff --git a/remappings.txt b/remappings.txt index 08fc002a..b057bf47 100644 --- a/remappings.txt +++ b/remappings.txt @@ -7,6 +7,8 @@ cowprotocol/=node_modules/@cowprotocol/contracts/src/ @composable-cow/=node_modules/composable-cow/ @cow-amm/=node_modules/cow-amm/src lib/openzeppelin/=node_modules/@openzeppelin +halmos-cheatcodes=node_modules/halmos-cheatcodes +@crytic/=node_modules/@crytic/ contracts/=src/contracts interfaces/=src/interfaces diff --git a/src/contracts/BCoWPool.sol b/src/contracts/BCoWPool.sol index ef2a7811..aae926d8 100644 --- a/src/contracts/BCoWPool.sol +++ b/src/contracts/BCoWPool.sol @@ -145,7 +145,7 @@ contract BCoWPool is IERC1271, IBCoWPool, BPool, BCoWConst { * @dev Grants infinite approval to the vault relayer for all tokens in the * pool after the finalization of the setup. Also emits COWAMMPoolCreated() event. */ - function _afterFinalize() internal override { + function _afterFinalize() internal virtual override { uint256 tokensLength = _tokens.length; for (uint256 i; i < tokensLength; i++) { IERC20(_tokens[i]).forceApprove(VAULT_RELAYER, type(uint256).max); diff --git a/src/contracts/BFactory.sol b/src/contracts/BFactory.sol index 22a768db..b9500d02 100644 --- a/src/contracts/BFactory.sol +++ b/src/contracts/BFactory.sol @@ -42,7 +42,7 @@ contract BFactory is IBFactory { } /// @inheritdoc IBFactory - function collect(IBPool bPool) external { + function collect(IBPool bPool) external virtual { if (msg.sender != _bDao) { revert BFactory_NotBDao(); } diff --git a/test/SUMMARY.md b/test/SUMMARY.md new file mode 100644 index 00000000..5f2479b9 --- /dev/null +++ b/test/SUMMARY.md @@ -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]. +``` \ No newline at end of file diff --git a/test/invariants/.solhint.json b/test/invariants/.solhint.json new file mode 100644 index 00000000..d3749ca8 --- /dev/null +++ b/test/invariants/.solhint.json @@ -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 + }] + } +} diff --git a/test/invariants/PROPERTIES.md b/test/invariants/PROPERTIES.md new file mode 100644 index 00000000..ff3d6ed1 --- /dev/null +++ b/test/invariants/PROPERTIES.md @@ -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] | +| 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) + +
`[ ]` planed to implement and still to do +
`[x]` implemented and tested +
`:(` implemented but test not passing due to an external factor (tool limitation - eg halmos max unrolling loop, etc) +
`#` implemented but deprecated feature / property +
`` 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~~ \ No newline at end of file diff --git a/test/invariants/fuzz/BMath.t.sol b/test/invariants/fuzz/BMath.t.sol new file mode 100644 index 00000000..593d0e83 --- /dev/null +++ b/test/invariants/fuzz/BMath.t.sol @@ -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 + ); + } +} diff --git a/test/invariants/fuzz/BMath.yaml b/test/invariants/fuzz/BMath.yaml new file mode 100644 index 00000000..f937a26a --- /dev/null +++ b/test/invariants/fuzz/BMath.yaml @@ -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 diff --git a/test/invariants/fuzz/BNum.t.sol b/test/invariants/fuzz/BNum.t.sol new file mode 100644 index 00000000..79db38b3 --- /dev/null +++ b/test/invariants/fuzz/BNum.t.sol @@ -0,0 +1,473 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {EchidnaTest} from '../helpers/AdvancedTestsUtils.sol'; + +import {BNum} from 'contracts/BNum.sol'; + +import {Test} from 'forge-std/Test.sol'; + +contract FuzzBNum is BNum, EchidnaTest, Test { + function bsub_exposed(uint256 a, uint256 b) external pure returns (uint256) { + return bsub(a, b); + } + + function bdiv_exposed(uint256 _a, uint256 _b) external pure returns (uint256) { + return bdiv(_a, _b); + } + ///////////////////////////////////////////////////////////////////// + // Bnum::btoi // + ///////////////////////////////////////////////////////////////////// + + // btoi should always return the floor(a / BONE) == (a - a%BONE) / BONE + function btoi_alwaysFloor(uint256 _input) public pure { + // action + uint256 _result = btoi(_input); + + // post-conditionn + assert(_result == _input / BONE); + } + + ///////////////////////////////////////////////////////////////////// + // Bnum::bfloor // + ///////////////////////////////////////////////////////////////////// + + // btoi should always return the floor(a / BONE) == (a - a%BONE) / BONE + function bfloor_shouldAlwaysRoundDown(uint256 _input) public pure { + // action + uint256 _result = bfloor(_input); + + // post condition + assert(_result == (_input / BONE) * BONE); + } + + ///////////////////////////////////////////////////////////////////// + // Bnum::badd // + ///////////////////////////////////////////////////////////////////// + + // badd should be commutative + function baddCommut(uint256 _a, uint256 _b) public pure { + // action + uint256 _result1 = badd(_a, _b); + uint256 _result2 = badd(_b, _a); + + // post condition + assert(_result1 == _result2); + } + + // badd should be associative + function badd_assoc(uint256 _a, uint256 _b, uint256 _c) public pure { + // action + uint256 _result1 = badd(badd(_a, _b), _c); + uint256 _result2 = badd(_a, badd(_b, _c)); + + // post condition + assert(_result1 == _result2); + } + + // 0 should be identity for badd + function badd_zeroIdentity(uint256 _a) public pure { + // action + uint256 _result = badd(_a, 0); + + // post condition + assert(_result == _a); + } + + // badd result should always be gte its terms + function badd_resultGTE(uint256 _a, uint256 _b) public pure { + // action + uint256 _result = badd(_a, _b); + + // post condition + assert(_result >= _a); + assert(_result >= _b); + } + + // badd should never sum terms which have a sum gt uint max + function badd_overflow(uint256 _a, uint256 _b) public pure { + // action + uint256 _result = badd(_a, _b); + + // post condition + assert(_result == _a + _b); + } + + // badd should have bsub as reverse operation + function badd_bsub(uint256 _a, uint256 _b) public pure { + // action + uint256 _result = badd(_a, _b); + uint256 _result2 = bsub(_result, _b); + + // post condition + assert(_result2 == _a); + } + + ///////////////////////////////////////////////////////////////////// + // Bnum::bsub // + ///////////////////////////////////////////////////////////////////// + + // bsub should not be commutative + function bsub_notCommut(uint256 _a, uint256 _b) public pure { + // precondition + require(_a != _b); + + // action + uint256 _result1 = bsub(_a, _b); + uint256 _result2 = bsub(_b, _a); + + // post condition + assert(_result1 != _result2); + } + + // bsub should not be associative + function bsub_notAssoc(uint256 _a, uint256 _b, uint256 _c) public pure { + // precondition + require(_a != _b && _b != _c && _a != _c); + require(_a != 0 && _b != 0 && _c != 0); + + // action + uint256 _result1 = bsub(bsub(_a, _b), _c); + uint256 _result2 = bsub(_a, bsub(_b, _c)); + + // post condition + assert(_result1 != _result2); + } + + // bsub should have 0 as identity + function bsub_zeroIdentity(uint256 _a) public pure { + // action + uint256 _result = bsub(_a, 0); + + // post condition + assert(_result == _a); + } + + // bsub result should always be lte a (underflow reverts) + function bsub_resultLTE(uint256 _a, uint256 _b) public pure { + // action + uint256 _result = bsub(_a, _b); + + // post condition + assert(_result <= _a); + } + + // bsub should alway revert if b > a + function bsub_revert(uint256 _a, uint256 _b) public { + // Precondition + _b = clamp(_b, _a + 1, type(uint256).max); + + // Action + (bool succ,) = address(this).call(abi.encodeCall(FuzzBNum.bsub_exposed, (_a, _b))); + + // Postcondition + assert(!succ); + } + + ///////////////////////////////////////////////////////////////////// + // Bnum::bsubSign // + ///////////////////////////////////////////////////////////////////// + + // bsubSign result should always be negative if b > a + function bsubSign_negative(uint256 _a, uint256 _b) public { + // precondition + _b = clamp(_b, _a + 1, type(uint256).max); + + // action + (uint256 _result, bool _flag) = bsubSign(_a, _b); + + // post condition + assert(_result == _b - _a); + assert(_flag); + } + + // bsubSign result should always be positive if a > b + function bsubSign_positive(uint256 _a, uint256 _b) public { + // precondition + _b = clamp(_b, 0, type(uint256).max - 1); + _a = clamp(_a, _b + 1, type(uint256).max); + + // action + (uint256 _result, bool _flag) = bsubSign(_a, _b); + + // post condition + assert(_result == _a - _b); + assert(!_flag); + } + + // bsubSign result should always be 0 if a == b + function bsubSign_zero(uint256 _a) public pure { + // action + (uint256 _result, bool _flag) = bsubSign(_a, _a); + + // post condition + assert(_result == 0); + assert(!_flag); + } + + ///////////////////////////////////////////////////////////////////// + // Bnum::bmul // + ///////////////////////////////////////////////////////////////////// + + // bmul should be commutative + function bmul_commutative(uint256 _a, uint256 _b) public pure { + // action + uint256 _result1 = bmul(_a, _b); + uint256 _result2 = bmul(_b, _a); + + // post condition + assert(_result1 == _result2); + } + + // bmul should be associative + function bmul_associative(uint256 _a, uint256 _b, uint256 _c) public { + // precondition + _c = clamp(_c, BONE, 9_999_999_999_999 * BONE); + _b = clamp(_b, BONE, 9_999_999_999_999 * BONE); + _a = clamp(_a, BONE, 9_999_999_999_999 * BONE); + + // action + uint256 _result1 = bmul(bmul(_a, _b), _c); + uint256 _result2 = bmul(_a, bmul(_b, _c)); + + // post condition + assert(_result1 / BONE == _result2 / BONE); + } + + // bmul should be distributive + function bmul_distributive(uint256 _a, uint256 _b, uint256 _c) public { + _c = clamp(_c, BONE, 10_000 * BONE); + _b = clamp(_b, BONE, 10_000 * BONE); + _a = clamp(_a, BONE, 10_000 * BONE); + + uint256 _result1 = bmul(_a, badd(_b, _c)); + uint256 _result2 = badd(bmul(_a, _b), bmul(_a, _c)); + + assert(_result1 == _result2 || _result1 == _result2 - 1 || _result1 == _result2 + 1); + } + + // 1 should be identity for bmul + function bmul_identity(uint256 _a) public { + _a = clamp(_a, BONE, 9_999_999_999_999 * BONE); + + uint256 _result = bmul(_a, BONE); + + assert(_result == _a); + } + + // 0 should be absorbing for mul + function bmul_absorbing(uint256 _a) public pure { + // action + uint256 _result = bmul(_a, 0); + + // post condition + assert(_result == 0); + } + + // bmul result should always be gte a and b + function bmul_resultGTE(uint256 _a, uint256 _b) public { + // Precondition + _a = clamp(_a, BONE, type(uint256).max / BONE); + _b = clamp(_b, BONE, type(uint256).max / BONE); + + require(_a * BONE + _b / 2 < type(uint256).max); // Avoid add overflow + + // Action + uint256 _result = bmul(_a, _b); + + // Postcondition + assert(_result >= _a); + assert(_result >= _b); + } + + ///////////////////////////////////////////////////////////////////// + // Bnum::bdiv // + ///////////////////////////////////////////////////////////////////// + + // 1 should be identity for bdiv + function bdiv_identity(uint256 _a) public pure { + uint256 _result = bdiv(_a, BONE); + assert(_result == _a); + } + + // bdiv should revert if b is 0 + function bdiv_revert(uint256 _a) public { + // action + (bool succ,) = address(this).call(abi.encodeCall(FuzzBNum.bdiv_exposed, (_a, 0))); + + // post condition + assert(!succ); + } + + // bdiv result should be lte a + function bdiv_resultLTE(uint256 _a, uint256 _b) public pure { + // vm.assume(_b != 0); + // vm.assume(_a < type(uint256).max / BONE); // Avoid mul overflow + //todo: overconstrained next line? Too tightly coupled? + // vm.assume(_a * BONE + _b / 2 < type(uint256).max); // Avoid add overflow + + uint256 _result = bdiv(_a, _b); + assert(_result <= _a * BONE); + } + + // bdiv should be bmul reverse operation + function bdiv_bmul(uint256 _a, uint256 _b) public { + _a = clamp(_a, BONE + 1, 10e12 * BONE); + _b = clamp(_b, BONE, _a - 1); + + uint256 _bdivResult = bdiv(_a, _b); + uint256 _result = bmul(_bdivResult, _b); + + _result /= BONE; + _a /= BONE; + + assert(_result == _a || _result == _a - 1 || _result == _a + 1); + } + + ///////////////////////////////////////////////////////////////////// + // Bnum::bpowi // + ///////////////////////////////////////////////////////////////////// + + // bpowi should return 1 if exp is 0 + function bpowi_zeroExp(uint256 _a) public pure { + // action + uint256 _result = bpowi(_a, 0); + + // post condition + assert(_result == BONE); + } + + // 0 should be absorbing if base + function bpowi_absorbingBase(uint256 _exp) public { + _exp = clamp(_exp, 1, type(uint256).max); + + uint256 _result = bpowi(0, _exp); + assert(_result == 0); + } + + // 1 should be identity if base + function bpowi_identityBase(uint256 _exp) public pure { + uint256 _result = bpowi(BONE, _exp); + assert(_result == BONE); + } + + // 1 should be identity if exp + function bpowi_identityExp(uint256 _base) public { + _base = clamp(_base, 1, 10_000); + + uint256 _result = bpowi(_base, 1); + + assert(_result == _base); + } + + // bpowi should be distributive over mult of the same base x^a x^b == x^(a+b) + function bpowi_distributiveBase(uint256 _base, uint256 _a, uint256 _b) public { + _base = clamp(_base, 1, 10_000); + _a = clamp(_a, 1, 1000 * BONE); + _b = clamp(_b, 1, 1000 * BONE); + + uint256 _result1 = bpowi(_base, badd(_a, _b)); + uint256 _result2 = bmul(bpowi(_base, _a), bpowi(_base, _b)); + + assert(_result1 == _result2); + } + + // bpowi should be distributive over mult of the same exp a^x b^x == (ab)^x + function bpowi_distributiveExp(uint256 _a, uint256 _b, uint256 _exp) public { + _a = clamp(_a, 1, 10_000); + _b = clamp(_b, 1, 10_000); + _exp = clamp(_exp, 1, 1000 * BONE); + + uint256 _result1 = bpowi(bmul(_a, _b), _exp); + uint256 _result2 = bmul(bpowi(_a, _exp), bpowi(_b, _exp)); + + emit log_named_uint('result1', _result1); + emit log_named_uint('result2', _result2); + + assert(_result1 == _result2); + } + + // power of a power should mult the exp (x^a)^b == x^(ab) + function bpowi_powerOfPower(uint256 _base, uint256 _a, uint256 _b) public { + _base = clamp(_base, 1, 10_000); + _a = clamp(_a, BONE, 1000 * BONE); + _b = clamp(_b, BONE, 1000 * BONE); + + uint256 _result1 = bpowi(bpowi(_base, _a), _b); + uint256 _result2 = bpowi(_base, bmul(_a, _b)) / BONE; + + assert(_result1 == _result2 || _result1 == _result2 - 1 || _result1 == _result2 + 1); + } + + ///////////////////////////////////////////////////////////////////// + // Bnum::bpow // + ///////////////////////////////////////////////////////////////////// + + // bpow should return 1 if exp is 0 + function bpow_zeroExp(uint256 _a) public pure { + // action + uint256 _result = bpow(_a, 0); + + // post condition + assert(_result == BONE); + } + + // 0 should be absorbing if base + function bpow_absorbingBase(uint256 _exp) public pure { + uint256 _result = bpow(0, _exp); + assert(_result == 0); + } + + // 1 should be identity if base + function bpow_identityBase(uint256 _exp) public pure { + uint256 _result = bpow(BONE, _exp); + assert(_result == BONE); + } + + // 1 should be identity if exp + function bpow_identityExp(uint256 _base) public pure { + // action + uint256 _result = bpow(_base, BONE); + + // post condition + assert(_result == _base); + } + + // bpow should be distributive over mult of the same base x^a * x^b == x^(a+b) + function bpow_distributiveBase(uint256 _base, uint256 _a, uint256 _b) public { + _base = clamp(_base, MIN_BPOW_BASE, MAX_BPOW_BASE); + _a = clamp(_a, 1, 1000); + _b = clamp(_b, 1, 1000); + + uint256 _result1 = bpow(_base, badd(_a, _b)); + uint256 _result2 = bmul(bpow(_base, _a), bpow(_base, _b)); + + assert(_result1 == _result2 || _result1 == _result2 - 1 || _result1 == _result2 + 1); + } + + // bpow should be distributive over mult of the same exp a^x * b^x == (a*b)^x + function bpow_distributiveExp(uint256 _a, uint256 _b, uint256 _exp) public { + _exp = clamp(_exp, 1, 100); + _a = clamp(_a, MIN_BPOW_BASE, MAX_BPOW_BASE); + _b = clamp(_b, MIN_BPOW_BASE, MAX_BPOW_BASE); + + require(_a * _b < MAX_BPOW_BASE && _a * _b > MIN_BPOW_BASE); + + uint256 _result1 = bpow(bmul(_a, _b), _exp); + uint256 _result2 = bmul(bpow(_a, _exp), bpow(_b, _exp)); + + assert(_result1 == _result2 || _result1 > _result2 ? _result1 - _result2 < BONE : _result2 - _result1 < BONE); + } + + // power of a power should mult the exp (x^a)^b == x^(a*b) + function bpow_powerOfPower(uint256 _base, uint256 _a, uint256 _b) public { + _base = clamp(_base, MIN_BPOW_BASE, MAX_BPOW_BASE); + _a = clamp(_a, 1, 1000); + _b = clamp(_b, 1, 1000); + + uint256 _result1 = bpow(bpow(_base, _a), _b); + uint256 _result2 = bpow(_base, bmul(_a, _b)); + + assert(_result1 == _result2); + } +} diff --git a/test/invariants/fuzz/BNum.yaml b/test/invariants/fuzz/BNum.yaml new file mode 100644 index 00000000..85dc34ed --- /dev/null +++ b/test/invariants/fuzz/BNum.yaml @@ -0,0 +1,6 @@ +# https://github.com/crytic/echidna/blob/master/tests/solidity/basic/default.yaml for more options +testMode: assertion +corpusDir: test/invariants/fuzz/corpuses/BNum/ +coverageFormats: ["html","lcov"] +allContracts: true +testLimit: 50000 \ No newline at end of file diff --git a/test/invariants/fuzz/BToken.t.sol b/test/invariants/fuzz/BToken.t.sol new file mode 100644 index 00000000..f4eb2bd3 --- /dev/null +++ b/test/invariants/fuzz/BToken.t.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {EchidnaTest} from '../helpers/AdvancedTestsUtils.sol'; +import {CryticERC20ExternalBasicProperties} from + '@crytic/properties/contracts/ERC20/external/properties/ERC20ExternalBasicProperties.sol'; +import {ITokenMock} from '@crytic/properties/contracts/ERC20/external/util/ITokenMock.sol'; +import {PropertiesConstants} from '@crytic/properties/contracts/util/PropertiesConstants.sol'; +import {BToken} from 'contracts/BToken.sol'; + +contract FuzzBToken is CryticERC20ExternalBasicProperties, EchidnaTest { + constructor() { + // Deploy ERC20 + token = ITokenMock(address(new CryticTokenMock())); + } + + /// @custom:property-id 8 + /// @custom:property BToken increaseApproval should increase the approval of the address by the amount + function fuzz_increaseApproval(uint256 _approvalToAdd) public { + // Precondition + uint256 _approvalBefore = token.allowance(USER1, USER2); + + hevm.prank(USER1); + + // Action + BToken(address(token)).increaseApproval(USER2, _approvalToAdd); + + // Postcondition + assert(token.allowance(USER1, USER2) == _approvalBefore + _approvalToAdd); + } + /// @custom:property-id 9 + /// @custom:property BToken decreaseApproval should decrease the approval to max(old-amount, 0) + + function fuzz_decreaseApproval(uint256 _approvalToLower) public { + // Precondition + uint256 _approvalBefore = token.allowance(USER1, USER2); + + hevm.prank(USER1); + + // Action + BToken(address(token)).decreaseApproval(USER2, _approvalToLower); + + // Postcondition + assert( + token.allowance(USER1, USER2) == (_approvalBefore > _approvalToLower ? _approvalBefore - _approvalToLower : 0) + ); + } +} + +contract CryticTokenMock is BToken('Balancer Pool Token', 'BPT'), PropertiesConstants { + bool public isMintableOrBurnable; + uint256 public initialSupply; + + constructor() { + _mint(USER1, INITIAL_BALANCE); + _mint(USER2, INITIAL_BALANCE); + _mint(USER3, INITIAL_BALANCE); + _mint(msg.sender, INITIAL_BALANCE); + + initialSupply = totalSupply(); + isMintableOrBurnable = true; + } +} diff --git a/test/invariants/fuzz/BToken.yaml b/test/invariants/fuzz/BToken.yaml new file mode 100644 index 00000000..1a2e45ad --- /dev/null +++ b/test/invariants/fuzz/BToken.yaml @@ -0,0 +1,6 @@ +# https://github.com/crytic/echidna/blob/master/tests/solidity/basic/default.yaml for more options +testMode: assertion +corpusDir: test/invariants/fuzz/corpuses/BToken/ +coverageFormats: ["html","lcov"] +allContracts: true +testLimit: 50000 diff --git a/test/invariants/fuzz/Protocol.t.sol b/test/invariants/fuzz/Protocol.t.sol new file mode 100644 index 00000000..7203ccd6 --- /dev/null +++ b/test/invariants/fuzz/Protocol.t.sol @@ -0,0 +1,495 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {EchidnaTest, FuzzERC20} from '../helpers/AdvancedTestsUtils.sol'; + +import {MockBNum as BNum} from '../../manual-smock/MockBNum.sol'; +import {BCoWFactoryForTest as BCoWFactory} from '../helpers/BCoWFactoryForTest.sol'; +import {MockSettler} from '../helpers/MockSettler.sol'; + +import {BConst} from 'contracts/BConst.sol'; +import {BMath} from 'contracts/BMath.sol'; + +import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; +import {IBPool} from 'interfaces/IBPool.sol'; + +contract FuzzProtocol is EchidnaTest { + // System under test + BCoWFactory factory; + BConst bconst; + BMath bmath; + BNum bnum; + + address solutionSettler; + bytes32 appData; + + FuzzERC20[] tokens; + IBCoWPool pool; + + IBPool[] poolsToFinalize; + + uint256 ghost_bptMinted; + uint256 ghost_bptBurned; + mapping(FuzzERC20 => uint256) ghost_amountDirectlyTransfered; + + string constant ERC20_SYMBOL = 'BPT'; + string constant ERC20_NAME = 'Balancer Pool Token'; + + constructor() { + solutionSettler = address(new MockSettler()); + + factory = new BCoWFactory(solutionSettler, appData); + bconst = new BConst(); + bmath = new BMath(); + bnum = new BNum(); + + pool = IBCoWPool(address(factory.newBPool('Balancer Pool Token', 'BPT'))); + + // first 4 tokens bound to the finalized pool + for (uint256 i; i < 4; i++) { + FuzzERC20 _token = new FuzzERC20(); + _token.initialize('', '', 18); + tokens.push(_token); + + _token.mint(address(this), 10 ether); + _token.approve(address(pool), 10 ether); + + uint256 _poolWeight = bconst.MAX_WEIGHT() / 5; + + try pool.bind(address(_token), 10 ether, _poolWeight) {} + catch { + assert(false); + } + } + + // 4 other tokens to bind to pools in poolsToFinalize, since max bound token is 8 + for (uint256 i; i < 4; i++) { + FuzzERC20 _token = new FuzzERC20(); + _token.initialize('', '', 18); + tokens.push(_token); + } + + pool.finalize(); + ghost_bptMinted = bconst.INIT_POOL_SUPPLY(); + } + + // Randomly add or remove tokens to a pool + // Insure caller has enough token + // Main objective is to have an arbitrary number of tokens in the pool, peripheral objective is another + // test of min/max token bound (properties 20 and 21) + function setup_joinExitPool(bool _join, uint256 _amountBpt) public agentOrDeployer { + if (_join) { + uint256[] memory _maxAmountsIn; + + _maxAmountsIn = new uint256[](4); + + for (uint256 i; i < _maxAmountsIn.length; i++) { + uint256 _maxIn = + bnum.call_bmul(bnum.call_bdiv(_amountBpt, pool.totalSupply()), pool.getBalance(address(tokens[i]))); + _maxAmountsIn[i] = _maxIn; + + tokens[i].mint(currentCaller, _maxIn); + hevm.prank(currentCaller); + tokens[i].approve(address(pool), _maxIn); + } + + hevm.prank(currentCaller); + try pool.joinPool(_amountBpt, _maxAmountsIn) { + ghost_bptMinted += _amountBpt; + } catch { + assert( + pool.isFinalized() || pool.getCurrentTokens().length > bconst.MAX_BOUND_TOKENS() + || currentCaller != pool.getController() + ); + } + } else { + hevm.prank(currentCaller); + pool.approve(address(pool), _amountBpt); + + hevm.prank(currentCaller); + try pool.exitPool(_amountBpt, new uint256[](4)) { + ghost_bptBurned += _amountBpt; + } catch { + assert(pool.isFinalized() || pool.getCurrentTokens().length == 0 || currentCaller != pool.getController()); + } + } + } + + /// @custom:property-id 1 + /// @custom:property BFactory should always be able to deploy new pools + function fuzz_BFactoryAlwaysDeploy() public agentOrDeployer { + // Precondition + hevm.prank(currentCaller); + + // Action + try factory.newBPool(ERC20_NAME, ERC20_SYMBOL) returns (IBPool _newPool) { + // Postcondition + assert(address(_newPool).code.length > 0); + assert(factory.isBPool(address(_newPool))); + assert(!_newPool.isFinalized()); + poolsToFinalize.push(_newPool); + } catch { + assert(false); + } + } + + /// @custom:property-id 2 + /// @custom:property BFactory's BDao should always be modifiable by the current BDaos + function fuzz_BDaoAlwaysModByBDao() public agentOrDeployer { + // Precondition + address _currentBDao = factory.getBDao(); + + hevm.prank(currentCaller); + + // Action + try factory.setBDao(address(123)) { + // Postcondition + assert(_currentBDao == currentCaller); + } catch { + assert(_currentBDao != currentCaller); + } + } + + /// @custom:property-id 3 + /// @custom:property BFactory should always be able to transfer the BToken to the BDao, if called by it + function fuzz_alwaysCollect() public agentOrDeployer { + // Precondition + address _currentBDao = factory.getBDao(); + + if (address(pool) == address(0)) { + return; + } + + hevm.prank(currentCaller); + + // Action + try factory.collect(pool) { + // Postcondition + assert(_currentBDao == currentCaller); + } catch { + assert(_currentBDao != currentCaller); + } + } + + /// @custom:property-id 4 + /// @custom:property the amount received can never be less than min amount out + /// @custom:property-id 13 + /// @custom:property an exact amount in should always earn the amount out calculated in bmath + /// @custom:property-id 15 + /// @custom:property there can't be any amount out for a 0 amount in + /// @custom:property-id 19 + /// @custom:property a swap can only happen when the pool is finalized + /// @custom:property-id 25 + /// @custom:property spot price after swap is always greater than before swap + function fuzz_swapExactIn( + uint256 _minAmountOut, + uint256 _amountIn, + uint256 _tokenIn, + uint256 _tokenOut + ) public agentOrDeployer { + // Preconditions + require(pool.isFinalized()); + + _tokenIn = clamp(_tokenIn, 0, tokens.length - 1); + _tokenOut = clamp(_tokenOut, 0, tokens.length - 1); + _amountIn = clamp(_amountIn, 1 ether, 10 ether); + + tokens[_tokenIn].mint(currentCaller, _amountIn); + + hevm.prank(currentCaller); + tokens[_tokenIn].approve(address(pool), type(uint256).max); // approval isn't limiting + + uint256 _balanceOutBefore = tokens[_tokenOut].balanceOf(currentCaller); + + uint256 _outComputed = bmath.calcOutGivenIn( + tokens[_tokenIn].balanceOf(address(pool)), + pool.getDenormalizedWeight(address(tokens[_tokenIn])), + tokens[_tokenOut].balanceOf(address(pool)), + pool.getDenormalizedWeight(address(tokens[_tokenOut])), + _amountIn, + bconst.MIN_FEE() + ); + + hevm.prank(currentCaller); + + // Action + try pool.swapExactAmountIn( + address(tokens[_tokenIn]), _amountIn, address(tokens[_tokenOut]), _minAmountOut, type(uint256).max + ) { + // Postcondition + uint256 _balanceOutAfter = tokens[_tokenOut].balanceOf(currentCaller); + + // 13 + assert(_balanceOutAfter - _balanceOutBefore == _outComputed); + + // 4 + if (_amountIn != 0) assert(_balanceOutBefore <= _balanceOutAfter + _minAmountOut); + // 15 + else assert(_balanceOutBefore == _balanceOutAfter); + + // 19 + assert(pool.isFinalized()); + } catch (bytes memory errorData) { + // 25 + if (keccak256(errorData) == IBPool.BPool_SpotPriceAfterBelowSpotPriceBefore.selector) { + assert(false); + } + assert( + // above max ratio + _amountIn > bnum.call_bmul(tokens[_tokenIn].balanceOf(address(pool)), bconst.MAX_IN_RATIO()) + // below min amount out + || _outComputed < _minAmountOut + ); + } + } + + /// @custom:property-id 5 + /// @custom:property the amount spent can never be greater than max amount in + /// @custom:property-id 14 + /// @custom:property an exact amount out is earned only if the amount in calculated in bmath is transfered + /// @custom:property-id 15 + /// @custom:property there can't be any amount out for a 0 amount in + /// @custom:property-id 19 + /// @custom:property a swap can only happen when the pool is finalized + function fuzz_swapExactOut( + uint256 _maxAmountIn, + uint256 _amountOut, + uint256 _tokenIn, + uint256 _tokenOut + ) public agentOrDeployer { + // Precondition + require(pool.isFinalized()); + + _tokenIn = clamp(_tokenIn, 0, tokens.length - 1); + _tokenOut = clamp(_tokenOut, 0, tokens.length - 1); + _amountOut = clamp(_amountOut, 1 ether, 10 ether); + _maxAmountIn = clamp(_maxAmountIn, 1 ether, 10 ether); + + tokens[_tokenIn].mint(currentCaller, _maxAmountIn); + + hevm.prank(currentCaller); + tokens[_tokenIn].approve(address(pool), type(uint256).max); // approval isn't limiting + + uint256 _balanceInBefore = tokens[_tokenIn].balanceOf(currentCaller); + uint256 _balanceOutBefore = tokens[_tokenOut].balanceOf(currentCaller); + + uint256 _inComputed = bmath.calcInGivenOut( + tokens[_tokenIn].balanceOf(address(pool)), + pool.getDenormalizedWeight(address(tokens[_tokenIn])), + tokens[_tokenOut].balanceOf(address(pool)), + pool.getDenormalizedWeight(address(tokens[_tokenOut])), + _amountOut, + bconst.MIN_FEE() + ); + + hevm.prank(currentCaller); + + // Action + try pool.swapExactAmountOut( + address(tokens[_tokenIn]), _maxAmountIn, address(tokens[_tokenOut]), _amountOut, type(uint256).max + ) { + // Postcondition + uint256 _balanceInAfter = tokens[_tokenIn].balanceOf(currentCaller); + uint256 _balanceOutAfter = tokens[_tokenOut].balanceOf(currentCaller); + + // Take into account previous direct transfers (only way to get free token) + uint256 _tokenOutInExcess = ghost_amountDirectlyTransfered[tokens[_tokenOut]] > _amountOut + ? _amountOut + : ghost_amountDirectlyTransfered[tokens[_tokenOut]]; + ghost_amountDirectlyTransfered[tokens[_tokenOut]] -= _tokenOutInExcess; + + // 5 + assert(_balanceInBefore - _balanceInAfter <= _maxAmountIn); + + // 14 + if (_tokenIn != _tokenOut) assert(_balanceOutAfter - _balanceOutBefore == _amountOut); + else assert(_balanceOutAfter == _balanceOutBefore - _inComputed + _amountOut); + + // 15 + if (_balanceInBefore == _balanceInAfter) assert(_balanceOutBefore + _tokenOutInExcess == _balanceOutAfter); + + // 19 + assert(pool.isFinalized()); + } catch (bytes memory errorData) { + if (keccak256(errorData) == IBPool.BPool_SpotPriceAfterBelowSpotPriceBefore.selector) { + assert(false); + } + uint256 _spotBefore = bmath.calcSpotPrice( + tokens[_tokenIn].balanceOf(address(pool)), + pool.getDenormalizedWeight(address(tokens[_tokenIn])), + tokens[_tokenOut].balanceOf(address(pool)), + pool.getDenormalizedWeight(address(tokens[_tokenOut])), + bconst.MIN_FEE() + ); + + uint256 _outRatio = bnum.call_bmul(tokens[_tokenOut].balanceOf(address(pool)), bconst.MAX_OUT_RATIO()); + + assert( + _inComputed > _maxAmountIn // 5 + || _amountOut > _outRatio // 14 + || _spotBefore > bnum.call_bdiv(_inComputed, _amountOut) + ); + } + } + + /// @custom:property-id 6 + /// @custom:property swap fee can only be 0 (cow pool) + function fuzz_swapFeeAlwaysZero() public { + assert(pool.getSwapFee() == bconst.MIN_FEE()); // todo: check if this is the intended property (min fee == 0?) + } + + /// @custom:property-id 7 + /// @custom:property total weight can be up to 50e18 + function fuzz_totalWeightMax(uint256 _numberTokens, uint256[8] calldata _weights) public { + // Precondition + IBPool _pool = IBPool(address(factory.newBPool(ERC20_NAME, ERC20_SYMBOL))); + + _numberTokens = clamp(_numberTokens, bconst.MIN_BOUND_TOKENS(), bconst.MAX_BOUND_TOKENS()); + + uint256 _totalWeight = 0; + + for (uint256 i; i < _numberTokens; i++) { + FuzzERC20 _token = new FuzzERC20(); + _token.initialize('', '', 18); + _token.mint(address(this), 10 ether); + _token.approve(address(_pool), 10 ether); + + uint256 _poolWeight = _weights[i]; + _poolWeight = clamp(_poolWeight, bconst.MIN_WEIGHT(), bconst.MAX_WEIGHT()); + + // Action + try _pool.bind(address(_token), 10 ether, _poolWeight) { + // Postcondition + _totalWeight += _poolWeight; + + // 7 + assert(_totalWeight <= bconst.MAX_TOTAL_WEIGHT()); + } catch { + // 7 + assert(_totalWeight + _poolWeight > bconst.MAX_TOTAL_WEIGHT()); + break; + } + } + } + + /// properties 8 and 9 are tested with the BToken internal tests + + /// @custom:property-id 10 + /// @custom:property a pool can either be finalized or not finalized + /// @dev included to be exhaustive/future-proof if more states are added, as rn, it + /// basically tests the tautological (a || !a) + function fuzz_poolFinalized() public { + assert(pool.isFinalized() || !pool.isFinalized()); + } + + /// @custom:property-id 11 + /// @custom:property a finalized pool cannot switch back to non-finalized + function fuzz_poolFinalizedOnce() public { + assert(pool.isFinalized()); + } + + /// @custom:property-id 12 + /// @custom:property a non-finalized pool can only be finalized when the controller calls finalize() + function fuzz_poolFinalizedByController() public agentOrDeployer { + // Precondition + if (poolsToFinalize.length == 0) { + return; + } + + IBPool _nonFinalizedPool = poolsToFinalize[poolsToFinalize.length - 1]; + + hevm.prank(currentCaller); + + // Action + try _nonFinalizedPool.finalize() { + // Postcondition + assert(currentCaller == _nonFinalizedPool.getController()); + poolsToFinalize.pop(); + } catch { + assert( + currentCaller != _nonFinalizedPool.getController() + || _nonFinalizedPool.getCurrentTokens().length > bconst.MAX_BOUND_TOKENS() + || _nonFinalizedPool.getCurrentTokens().length < bconst.MIN_BOUND_TOKENS() + ); + } + } + + /// @custom:property-id 16 + /// @custom:property the pool btoken can only be minted/burned in the join and exit operations + function fuzz_mintBurnBPT() public { + assert(ghost_bptMinted - ghost_bptBurned == pool.totalSupply()); + } + + /// @custom:property-id 20 + /// @custom:property bounding and unbounding token can only be done on a non-finalized pool, by the controller + function fuzz_boundOnlyNotFinalized() public agentOrDeployer { + // Precondition + if (poolsToFinalize.length == 0) { + return; + } + + IBPool _nonFinalizedPool = poolsToFinalize[poolsToFinalize.length - 1]; + + for (uint256 i; i < 4; i++) { + tokens[i].mint(currentCaller, 10 ether); + + hevm.prank(currentCaller); + tokens[i].approve(address(_nonFinalizedPool), 10 ether); + + uint256 _poolWeight = bconst.MAX_WEIGHT() / 5; + + if (_nonFinalizedPool.isBound(address(tokens[i]))) { + uint256 _balanceUnboundBefore = tokens[i].balanceOf(currentCaller); + + hevm.prank(currentCaller); + // Action + try _nonFinalizedPool.unbind(address(tokens[i])) { + // Postcondition + assert(currentCaller == _nonFinalizedPool.getController()); + assert(!_nonFinalizedPool.isFinalized()); + assert(tokens[i].balanceOf(currentCaller) > _balanceUnboundBefore); + } catch { + assert(currentCaller != _nonFinalizedPool.getController() || _nonFinalizedPool.isFinalized()); + } + } else { + hevm.prank(currentCaller); + try _nonFinalizedPool.bind(address(tokens[i]), 10 ether, _poolWeight) { + // Postcondition + assert(currentCaller == _nonFinalizedPool.getController()); + assert(!_nonFinalizedPool.isFinalized()); + } catch { + assert(currentCaller != _nonFinalizedPool.getController() || _nonFinalizedPool.isFinalized()); + } + } + } + } + + /// @custom:property-id 21 + /// @custom:property there always should be between MIN_BOUND_TOKENS and MAX_BOUND_TOKENS bound in a pool + function fuzz_minMaxBoundToken() public { + assert(pool.getNumTokens() >= bconst.MIN_BOUND_TOKENS()); + assert(pool.getNumTokens() <= bconst.MAX_BOUND_TOKENS()); + + for (uint256 i; i < poolsToFinalize.length; i++) { + if (poolsToFinalize[i].isFinalized()) { + assert(poolsToFinalize[i].getNumTokens() >= bconst.MIN_BOUND_TOKENS()); + assert(poolsToFinalize[i].getNumTokens() <= bconst.MAX_BOUND_TOKENS()); + } + } + } + + /// @custom:property-id 22 + /// @custom:property only the settler can commit a hash + function fuzz_settlerCommit() public agentOrDeployer { + // Precondition + hevm.prank(currentCaller); + + // Action + try pool.commit(hex'1234') { + // Postcondition + assert(currentCaller == solutionSettler); + } catch { + assert(currentCaller != solutionSettler); + } + } +} diff --git a/test/invariants/fuzz/Protocol.yaml b/test/invariants/fuzz/Protocol.yaml new file mode 100644 index 00000000..1614810c --- /dev/null +++ b/test/invariants/fuzz/Protocol.yaml @@ -0,0 +1,6 @@ +# https://github.com/crytic/echidna/blob/master/tests/solidity/basic/default.yaml for more options +testMode: assertion +corpusDir: test/invariants/fuzz/corpuses/Protocol/ +coverageFormats: ["html","lcov"] +allContracts: true +testLimit: 50000 \ No newline at end of file diff --git a/test/invariants/helpers/AdvancedTestsUtils.sol b/test/invariants/helpers/AdvancedTestsUtils.sol new file mode 100644 index 00000000..b44ed3d3 --- /dev/null +++ b/test/invariants/helpers/AdvancedTestsUtils.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {Test} from 'forge-std/Test.sol'; +import {MockERC20} from 'forge-std/mocks/MockERC20.sol'; +import {SymTest} from 'halmos-cheatcodes/src/SymTest.sol'; + +interface IHevm { + function prank(address) external; +} + +contract FuzzERC20 is MockERC20 { + function mint(address _to, uint256 _amount) public { + _mint(_to, _amount); + } + + function burn(address _from, uint256 _amount) public { + _burn(_from, _amount); + } +} + +contract AgentsHandler { + uint256 internal agentsIndex; + address[] internal agents; + + address internal currentCaller; + + modifier agentOrDeployer() { + uint256 _currentAgentIndex = agentsIndex; + currentCaller = _currentAgentIndex == 0 ? address(this) : agents[agentsIndex]; + _; + } + + constructor(uint256 _numAgents) { + for (uint256 i = 0; i < _numAgents; i++) { + agents.push(address(bytes20(keccak256(abi.encodePacked(i))))); + } + } + + function nextAgent() public { + agentsIndex = (agentsIndex + 1) % agents.length; + } + + function getCurrentAgent() public view returns (address) { + return agents[agentsIndex]; + } +} + +contract EchidnaTest is AgentsHandler { + event AssertionFailed(); + + IHevm hevm = IHevm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + + constructor() AgentsHandler(5) {} + + function clamp(uint256 _value, uint256 _min, uint256 _max) internal pure returns (uint256) { + if (_min > _max) { + assert(false); + } + + if (_value < _min || _value > _max) { + return _min + (_value % (_max - _min + 1)); + } + return _value; + } +} + +contract HalmosTest is SymTest, Test {} diff --git a/test/invariants/helpers/BCoWFactoryForTest.sol b/test/invariants/helpers/BCoWFactoryForTest.sol new file mode 100644 index 00000000..477abf41 --- /dev/null +++ b/test/invariants/helpers/BCoWFactoryForTest.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.25; + +import {BCoWPoolForTest} from './BCoWPoolForTest.sol'; +import {BCoWFactory} from 'contracts/BCoWFactory.sol'; + +import {BFactory} from 'contracts/BFactory.sol'; +import {IBFactory} from 'interfaces/IBFactory.sol'; +import {IBPool} from 'interfaces/IBPool.sol'; + +contract BCoWFactoryForTest is BCoWFactory { + constructor(address cowSolutionSettler, bytes32 appData) BCoWFactory(cowSolutionSettler, appData) {} + + function _newBPool(string memory, string memory) internal virtual override returns (IBPool bCoWPool) { + bCoWPool = new BCoWPoolForTest(SOLUTION_SETTLER, APP_DATA); + } + + /// @dev workaround for hevm not supporting mcopy + function collect(IBPool bPool) external override(BFactory, IBFactory) { + if (msg.sender != _bDao) { + revert BFactory_NotBDao(); + } + uint256 collected = bPool.balanceOf(address(this)); + bPool.transfer(_bDao, collected); + } +} diff --git a/test/invariants/helpers/BCoWPoolForTest.sol b/test/invariants/helpers/BCoWPoolForTest.sol new file mode 100644 index 00000000..707ccc45 --- /dev/null +++ b/test/invariants/helpers/BCoWPoolForTest.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.25; + +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; + +import {BCoWPool} from 'contracts/BCoWPool.sol'; +import {IBCoWFactory} from 'interfaces/IBCoWFactory.sol'; + +contract BCoWPoolForTest is BCoWPool { + constructor(address cowSolutionSettler, bytes32 appData) BCoWPool(cowSolutionSettler, appData, 'name', 'symbol') {} + + bytes32 private _reenteringMutex; + + /// @dev workaround for hevm not supporting tload/tstore + function _setLock(bytes32 value) internal override { + _reenteringMutex = value; + } + + /// @dev workaround for hevm not supporting tload/tstore + function _getLock() internal view override returns (bytes32 value) { + value = _reenteringMutex; + } + + /// @dev workaround for hevm not supporting mcopy + function _pullUnderlying(address token, address from, uint256 amount) internal override { + IERC20(token).transferFrom(from, address(this), amount); + } + + /// @dev workaround for hevm not supporting mcopy + function _pushUnderlying(address token, address to, uint256 amount) internal override { + IERC20(token).transfer(to, amount); + } + + /// @dev workaround for hevm not supporting mcopy + function _afterFinalize() internal override { + uint256 tokensLength = _tokens.length; + for (uint256 i; i < tokensLength; i++) { + IERC20(_tokens[i]).approve(VAULT_RELAYER, type(uint256).max); + } + + try IBCoWFactory(FACTORY).logBCoWPool() {} + catch { + emit IBCoWFactory.COWAMMPoolCreated(address(this)); + } + } +} diff --git a/test/invariants/helpers/MockSettler.sol b/test/invariants/helpers/MockSettler.sol new file mode 100644 index 00000000..598f47aa --- /dev/null +++ b/test/invariants/helpers/MockSettler.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {GPv2Interaction, GPv2Trade, IERC20, ISettlement} from 'interfaces/ISettlement.sol'; + +contract MockSettler is ISettlement { + function domainSeparator() external view override returns (bytes32) { + return bytes32(hex'1234'); + } + + function vaultRelayer() external view override returns (address) { + return address(123); + } + + function settle( + IERC20[] calldata tokens, + uint256[] calldata clearingPrices, + GPv2Trade.Data[] calldata trades, + GPv2Interaction.Data[][3] calldata interactions + ) external {} +} diff --git a/test/invariants/symbolic/BNum.t.sol b/test/invariants/symbolic/BNum.t.sol new file mode 100644 index 00000000..9a4b9cd1 --- /dev/null +++ b/test/invariants/symbolic/BNum.t.sol @@ -0,0 +1,261 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {HalmosTest} from '../helpers/AdvancedTestsUtils.sol'; +import {BNum} from 'contracts/BNum.sol'; + +contract SymbolicBNum is BNum, HalmosTest { + ///////////////////////////////////////////////////////////////////// + // Bnum::btoi // + ///////////////////////////////////////////////////////////////////// + + // btoi should always return the floor(a / BONE) == (a - a%BONE) / BONE + function check_btoi_alwaysFloor(uint256 _input) public pure { + // action + uint256 _result = btoi(_input); + + // post-conditionn + assert(_result == _input / BONE); + } + + ///////////////////////////////////////////////////////////////////// + // Bnum::bfloor // + ///////////////////////////////////////////////////////////////////// + + // btoi should always return the floor(a / BONE) == (a - a%BONE) / BONE + function check_bfloor_shouldAlwaysRoundDown(uint256 _input) public pure { + // action + uint256 _result = bfloor(_input); + + // post condition + assert(_result == (_input / BONE) * BONE); + } + + ///////////////////////////////////////////////////////////////////// + // Bnum::badd // + ///////////////////////////////////////////////////////////////////// + + // badd should be commutative + function check_baddCommut(uint256 _a, uint256 _b) public pure { + // action + uint256 _result1 = badd(_a, _b); + uint256 _result2 = badd(_b, _a); + + // post condition + assert(_result1 == _result2); + } + + // badd should be associative + function check_badd_assoc(uint256 _a, uint256 _b, uint256 _c) public pure { + // action + uint256 _result1 = badd(badd(_a, _b), _c); + uint256 _result2 = badd(_a, badd(_b, _c)); + + // post condition + assert(_result1 == _result2); + } + + // 0 should be identity for badd + function check_badd_zeroIdentity(uint256 _a) public pure { + // action + uint256 _result = badd(_a, 0); + + // post condition + assert(_result == _a); + } + + // badd result should always be gte its terms + function check_badd_resultGTE(uint256 _a, uint256 _b) public pure { + // action + uint256 _result = badd(_a, _b); + + // post condition + assert(_result >= _a); + assert(_result >= _b); + } + + // badd should never sum terms which have a sum gt uint max + function check_badd_overflow(uint256 _a, uint256 _b) public pure { + // precondition + vm.assume(_a != type(uint256).max); + + // action + uint256 _result = badd(_a, _b); + + // post condition + assert(_result == _a + _b); + } + + // badd should have bsub as reverse operation + function check_badd_bsub(uint256 _a, uint256 _b) public pure { + // action + uint256 _result = badd(_a, _b); + uint256 _result2 = bsub(_result, _b); + + // post condition + assert(_result2 == _a); + } + + ///////////////////////////////////////////////////////////////////// + // Bnum::bsub // + ///////////////////////////////////////////////////////////////////// + + // bsub should not be associative + function check_bsub_notAssoc(uint256 _a, uint256 _b, uint256 _c) public pure { + // precondition + vm.assume(_a != _b && _b != _c && _a != _c); + vm.assume(_a != 0 && _b != 0 && _c != 0); + + // action + uint256 _result1 = bsub(bsub(_a, _b), _c); + uint256 _result2 = bsub(_a, bsub(_b, _c)); + + // post condition + assert(_result1 != _result2); + } + + // bsub should have 0 as identity + function check_bsub_zeroIdentity(uint256 _a) public pure { + // action + uint256 _result = bsub(_a, 0); + + // post condition + assert(_result == _a); + } + + // bsub result should always be lte a + function check_bsub_resultLTE(uint256 _a, uint256 _b) public pure { + // precondition + vm.assume(_a >= _b); // Avoid underflow + + // action + uint256 _result = bsub(_a, _b); + + // post condition + assert(_result <= _a); + } + + ///////////////////////////////////////////////////////////////////// + // Bnum::bsubSign // + ///////////////////////////////////////////////////////////////////// + + // bsubSign should be commutative value-wise + function check_bsubSign_CommutValue(uint256 _a, uint256 _b) public pure { + // precondition + vm.assume(_a != _b); + + // action + (uint256 _result1,) = bsubSign(_a, _b); + (uint256 _result2,) = bsubSign(_b, _a); + + // post condition + assert(_result1 == _result2); + } + + // bsubSign should not be commutative sign-wise + function check_bsubSign_notCommutSign(uint256 _a, uint256 _b) public pure { + // precondition + vm.assume(_a != _b); + + // action + (, bool _sign1) = bsubSign(_a, _b); + (, bool _sign2) = bsubSign(_b, _a); + + // post condition + assert(_sign1 != _sign2); + } + + // bsubSign result should always be negative if b > a + function check_bsubSign_negative(uint256 _a, uint256 _b) public pure { + // precondition + vm.assume(_b > _a); + + // action + (uint256 _result, bool _flag) = bsubSign(_a, _b); + + // post condition + assert(_result == _b - _a); + assert(_flag); + } + + // bsubSign result should always be positive if a > b + function check_bsubSign_positive(uint256 _a, uint256 _b) public pure { + // precondition + vm.assume(_a > _b); + + // action + (uint256 _result, bool _flag) = bsubSign(_a, _b); + + // post condition + assert(_result == _a - _b); + assert(!_flag); + } + + // bsubSign result should always be 0 if a == b + function check_bsubSign_zero(uint256 _a) public pure { + // action + (uint256 _result, bool _flag) = bsubSign(_a, _a); + + // post condition + assert(_result == 0); + assert(!_flag); + } + + ///////////////////////////////////////////////////////////////////// + // Bnum::bmul // + ///////////////////////////////////////////////////////////////////// + + // bmul should be commutative + function check_bmul_commutative(uint256 _a, uint256 _b) public pure { + // action + uint256 _result1 = bmul(_a, _b); + uint256 _result2 = bmul(_b, _a); + + // post condition + assert(_result1 == _result2); + } + + // 0 should be absorbing for mul + function check_bmul_absorbing(uint256 _a) public pure { + // action + uint256 _result = bmul(_a, 0); + + // post condition + assert(_result == 0); + } + + ///////////////////////////////////////////////////////////////////// + // Bnum::bpowi // + ///////////////////////////////////////////////////////////////////// + + // bpowi should return 1 if exp is 0 + function check_bpowi_zeroExp(uint256 _a) public pure { + // action + uint256 _result = bpowi(_a, 0); + + // post condition + assert(_result == BONE); + } + + ///////////////////////////////////////////////////////////////////// + // Bnum::bpow // + ///////////////////////////////////////////////////////////////////// + + // bpow should return 1 if exp is 0 + function check_bpow_zeroExp(uint256 _a) public pure { + // action + uint256 _result = bpow(_a, 0); + + // post condition + assert(_result == BONE); + } + + // 1 should be identity if exp + function check_bpow_identityExp(uint256 _base) public pure { + // action + uint256 _result = bpow(_base, BONE); + + // post condition + assert(_result == _base); + } +} diff --git a/test/invariants/symbolic/Protocol.t.sol b/test/invariants/symbolic/Protocol.t.sol new file mode 100644 index 00000000..cb0dec82 --- /dev/null +++ b/test/invariants/symbolic/Protocol.t.sol @@ -0,0 +1,287 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {FuzzERC20, HalmosTest} from '../helpers/AdvancedTestsUtils.sol'; + +import {BCoWFactoryForTest as BCoWFactory} from '../helpers/BCoWFactoryForTest.sol'; +import {MockSettler} from '../helpers/MockSettler.sol'; +import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; +import {IBPool} from 'interfaces/IBPool.sol'; + +import {BConst} from 'contracts/BConst.sol'; +import {BToken} from 'contracts/BToken.sol'; + +contract HalmosBalancer is HalmosTest { + // System under test + BCoWFactory factory; + BConst bconst; + + address solutionSettler; + bytes32 appData; + + FuzzERC20[] tokens; + IBCoWPool pool; + + address currentCaller = svm.createAddress('currentCaller'); + + constructor() { + solutionSettler = address(new MockSettler()); + factory = new BCoWFactory(solutionSettler, appData); + bconst = new BConst(); + pool = IBCoWPool(address(factory.newBPool('Balancer Pool Token', 'BPT'))); + + // max bound token is 8 + for (uint256 i; i < 5; i++) { + FuzzERC20 _token = new FuzzERC20(); + _token.initialize('', '', 18); + tokens.push(_token); + + _token.mint(address(this), 10 ether); + _token.approve(address(pool), 10 ether); + + uint256 _poolWeight = bconst.MAX_WEIGHT() / 5; + + pool.bind(address(_token), 10 ether, _poolWeight); + } + + pool.finalize(); + } + + /// @custom:property-id 0 + /// @custom:property BFactory should always be able to deploy new pools + function check_deploy() public { + assert(factory.SOLUTION_SETTLER() == solutionSettler); + assert(pool.isFinalized()); + } + + /// @custom:property-id 1 + /// @custom:property BFactory should always be able to deploy new pools + function check_BFactoryAlwaysDeploy(address _caller) public { + // Precondition + vm.assume(_caller != address(0)); + vm.prank(_caller); + + // Action + try factory.newBPool('Balancer Pool Token', 'BPT') returns (IBPool _newPool) { + // Postcondition + assert(address(_newPool).code.length > 0); + assert(factory.isBPool(address(_newPool))); + assert(!_newPool.isFinalized()); + } catch { + assert(false); + } + } + + /// @custom:property-id 2 + /// @custom:property BFactory's BDao should always be modifiable by the current BDao + function check_BDaoAlwaysModByBDao() public { + // Precondition + address _currentBDao = factory.getBDao(); + + vm.prank(currentCaller); + + // Action + try factory.setBDao(address(123)) { + // Postcondition + assert(_currentBDao == currentCaller); + } catch { + assert(_currentBDao != currentCaller); + } + } + + /// @custom:property-id 3 + /// @custom:property BFactory should always be able to transfer the BToken to the BDao, if called by it + function check_alwaysCollect() public { + // Precondition + address _currentBDao = factory.getBDao(); + + vm.prank(currentCaller); + + // Action + try factory.collect(pool) { + // Postcondition + assert(_currentBDao == currentCaller); + } catch { + assert(_currentBDao != currentCaller); + } + } + + /// @custom:property-id 7 + /// @custom:property total weight can be up to 50e18 + /// @dev Only 2 tokens are used, to avoid hitting the limit in loop unrolling + function check_totalWeightMax(uint256[2] calldata _weights) public { + // Precondition + IBCoWPool _pool = IBCoWPool(address(factory.newBPool('Balancer Pool Token', 'BPT'))); + + uint256 _totalWeight = 0; + + for (uint256 i; i < 2; i++) { + vm.assume(_weights[i] >= bconst.MIN_WEIGHT() && _weights[i] <= bconst.MAX_WEIGHT()); + } + + for (uint256 i; i < 2; i++) { + FuzzERC20 _token = new FuzzERC20(); + _token.initialize('', '', 18); + _token.mint(address(this), 10 ether); + _token.approve(address(_pool), 10 ether); + + uint256 _poolWeight = _weights[i]; + + // Action + try _pool.bind(address(_token), 10 ether, _poolWeight) { + // Postcondition + _totalWeight += _poolWeight; + + // 7 + assert(_totalWeight <= bconst.MAX_TOTAL_WEIGHT()); + } catch { + // 7 + assert(_totalWeight + _poolWeight > bconst.MAX_TOTAL_WEIGHT()); + break; + } + } + } + + /// @custom:property-id 8 + /// @custom:property BToken increaseApproval should increase the approval of the address by the amount + function check_increaseApproval(uint256 _approvalToAdd, address _owner, address _spender) public { + // Precondition + uint256 _approvalBefore = pool.allowance(_owner, _spender); + + vm.prank(_owner); + + // Action + BToken(address(pool)).increaseApproval(_spender, _approvalToAdd); + + // Postcondition + assert(pool.allowance(_owner, _spender) == _approvalBefore + _approvalToAdd); + } + /// @custom:property-id 9 + /// @custom:property BToken decreaseApproval should decrease the approval to max(old-amount, 0) + + function check_decreaseApproval(uint256 _approvalToLower, address _owner, address _spender) public { + // Precondition + uint256 _approvalBefore = pool.allowance(_owner, _spender); + + vm.prank(_owner); + + // Action + BToken(address(pool)).decreaseApproval(_spender, _approvalToLower); + + // Postcondition + assert( + pool.allowance(_owner, _spender) == (_approvalBefore > _approvalToLower ? _approvalBefore - _approvalToLower : 0) + ); + } + + /// @custom:property-id 12 + /// @custom:property a non-finalized pool can only be finalized when the controller calls finalize() + function check_poolFinalizedByController() public { + // Precondition + IBPool _nonFinalizedPool = factory.newBPool('Balancer Pool Token', 'BPT'); + + vm.prank(_nonFinalizedPool.getController()); + + for (uint256 i; i < 3; i++) { + FuzzERC20 _token = new FuzzERC20(); + + _token.initialize('', '', 18); + _token.mint(_nonFinalizedPool.getController(), 10 ether); + _token.approve(address(_nonFinalizedPool), 10 ether); + + uint256 _poolWeight = bconst.MAX_WEIGHT() / 5; + + _nonFinalizedPool.bind(address(_token), 10 ether, _poolWeight); + } + vm.stopPrank(); + + vm.prank(currentCaller); + + // Action + try _nonFinalizedPool.finalize() { + // Postcondition + assert(currentCaller == _nonFinalizedPool.getController()); + } catch {} + } + + /// @custom:property-id 20 + /// @custom:property bounding and unbounding token can only be done on a non-finalized pool, by the controller + function check_boundOnlyNotFinalized() public { + // Precondition + IBPool _nonFinalizedPool = factory.newBPool('Balancer Pool Token', 'BPT'); + + address _callerBind = svm.createAddress('callerBind'); + address _callerUnbind = svm.createAddress('callerUnbind'); + address _callerFinalize = svm.createAddress('callerFinalize'); + + // Avoid hitting the max unrolled loop limit + + // Bind 3 tokens + tokens[0].mint(_callerBind, 10 ether); + tokens[1].mint(_callerBind, 10 ether); + tokens[2].mint(_callerBind, 10 ether); + + vm.startPrank(_callerBind); + tokens[0].approve(address(_nonFinalizedPool), 10 ether); + tokens[1].approve(address(_nonFinalizedPool), 10 ether); + tokens[2].approve(address(_nonFinalizedPool), 10 ether); + + uint256 _poolWeight = bconst.MAX_WEIGHT() / 4; + uint256 _bindCount; + + try _nonFinalizedPool.bind(address(tokens[0]), 10 ether, _poolWeight) { + assert(_callerBind == _nonFinalizedPool.getController()); + _bindCount++; + } catch { + assert(_callerBind != _nonFinalizedPool.getController()); + } + + try _nonFinalizedPool.bind(address(tokens[1]), 10 ether, _poolWeight) { + assert(_callerBind == _nonFinalizedPool.getController()); + _bindCount++; + } catch { + assert(_callerBind != _nonFinalizedPool.getController()); + } + + try _nonFinalizedPool.bind(address(tokens[2]), 10 ether, _poolWeight) { + assert(_callerBind == _nonFinalizedPool.getController()); + _bindCount++; + } catch { + assert(_callerBind != _nonFinalizedPool.getController()); + } + + vm.stopPrank(); + + if (_bindCount == 3) { + vm.prank(_callerUnbind); + // Action + // Unbind one + try _nonFinalizedPool.unbind(address(tokens[0])) { + assert(_callerUnbind == _nonFinalizedPool.getController()); + } catch { + assert(_callerUnbind != _nonFinalizedPool.getController()); + } + } + + vm.prank(_callerFinalize); + // Action + try _nonFinalizedPool.finalize() { + assert(_callerFinalize == _nonFinalizedPool.getController()); + } catch { + assert(_callerFinalize != _nonFinalizedPool.getController() || _bindCount < 2); + } + } + + /// @custom:property-id 22 + /// @custom:property only the settler can commit a hash + function check_settlerCommit() public { + // Precondition + vm.prank(currentCaller); + + // Action + try pool.commit(hex'1234') { + // Postcondition + assert(currentCaller == solutionSettler); + } catch {} + } +} diff --git a/yarn.lock b/yarn.lock index bba6419e..25aee9fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -185,6 +185,16 @@ version "1.6.0" resolved "https://codeload.github.com/cowprotocol/contracts/tar.gz/a10f40788af29467e87de3dbf2196662b0a6b500" +"@crytic/properties@https://github.com/crytic/properties.git": + version "0.0.1" + resolved "https://github.com/crytic/properties.git#f1ff61b8e90ce0c9ab138dfe23e80a8afa7f5e37" + dependencies: + "@openzeppelin/contracts" "^4.7.3" + markdown-link-check "^3.11.0" + prettier "^2.8.7" + prettier-plugin-solidity "^1.1.3" + solmate "^6.6.1" + "@defi-wonderland/natspec-smells@1.1.3": version "1.1.3" resolved "https://registry.yarnpkg.com/@defi-wonderland/natspec-smells/-/natspec-smells-1.1.3.tgz#6d4c7e289b24264856170fec33e0cae0c844bd32" @@ -242,6 +252,11 @@ resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-5.0.2.tgz#b1d03075e49290d06570b2fd42154d76c2a5d210" integrity sha512-ytPc6eLGcHHnapAZ9S+5qsdomhjo6QBHTDRRBFfTxXIpsicMhVPouPgmUPebZZZGX7vt9USA+Z+0M0dSVtSUEA== +"@openzeppelin/contracts@^4.7.3": + version "4.9.6" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.9.6.tgz#2a880a24eb19b4f8b25adc2a5095f2aa27f39677" + integrity sha512-xSmezSupL+y9VkHZJGDoCBpmnB2ogM13ccaYDWqJTfS3dbuHkgjuwDFUmaFauBCboQMGB/S5UqUl2y54X99BmA== + "@scure/base@~1.1.4": version "1.1.6" resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.6.tgz#8ce5d304b436e4c84f896e0550c83e4d88cb917d" @@ -271,6 +286,16 @@ dependencies: antlr4ts "^0.5.0-alpha.4" +"@solidity-parser/parser@^0.17.0": + version "0.17.0" + resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.17.0.tgz#52a2fcc97ff609f72011014e4c5b485ec52243ef" + integrity sha512-Nko8R0/kUo391jsEHHxrGM07QFdnPGvlmox4rmH0kNiNAashItAilhy4Mv4pK5gQmW5f4sXAF58fwJbmlkGcVw== + +"@tootallnate/quickjs-emscripten@^0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz#db4ecfd499a9765ab24002c3b696d02e6d32a12c" + integrity sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA== + "@types/conventional-commits-parser@^5.0.0": version "5.0.0" resolved "https://registry.yarnpkg.com/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz#8c9d23e0b415b24b91626d07017303755d542dc8" @@ -298,6 +323,13 @@ abitype@0.7.1: resolved "https://registry.yarnpkg.com/abitype/-/abitype-0.7.1.tgz#16db20abe67de80f6183cf75f3de1ff86453b745" integrity sha512-VBkRHTDZf9Myaek/dO3yMmOzB/y2s3Zo6nVU7yaw1G+TvCHAjwaJzNGN9yo4K5D8bU/VZXKP1EJpRhFr862PlQ== +agent-base@^7.0.2, agent-base@^7.1.0, agent-base@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.1.tgz#bdbded7dfb096b751a2a087eeeb9664725b2e317" + integrity sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA== + dependencies: + debug "^4.3.4" + ajv@^6.12.6: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" @@ -377,11 +409,23 @@ ast-parents@^0.0.1: resolved "https://registry.yarnpkg.com/ast-parents/-/ast-parents-0.0.1.tgz#508fd0f05d0c48775d9eccda2e174423261e8dd3" integrity sha512-XHusKxKz3zoYk1ic8Un640joHbFMhbqneyoZfoKnEGtf2ey9Uh/IdpcQplODdO/kENaMIWsD0nJm4+wX3UNLHA== +ast-types@^0.13.4: + version "0.13.4" + resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.13.4.tgz#ee0d77b343263965ecc3fb62da16e7222b2b6782" + integrity sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w== + dependencies: + tslib "^2.0.1" + astral-regex@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== +async@^3.2.5: + version "3.2.5" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66" + integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg== + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -408,6 +452,16 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= +basic-ftp@^5.0.2: + version "5.0.5" + resolved "https://registry.yarnpkg.com/basic-ftp/-/basic-ftp-5.0.5.tgz#14a474f5fffecca1f4f406f1c26b18f800225ac0" + integrity sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg== + +boolbase@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== + brace-expansion@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" @@ -460,6 +514,31 @@ chalk@^4.1.2: ansi-styles "^4.1.0" supports-color "^7.1.0" +cheerio-select@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-2.1.0.tgz#4d8673286b8126ca2a8e42740d5e3c4884ae21b4" + integrity sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g== + dependencies: + boolbase "^1.0.0" + css-select "^5.1.0" + css-what "^6.1.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + domutils "^3.0.1" + +cheerio@^1.0.0-rc.10: + version "1.0.0-rc.12" + resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.12.tgz#788bf7466506b1c6bf5fae51d24a2c4d62e47683" + integrity sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q== + dependencies: + cheerio-select "^2.1.0" + dom-serializer "^2.0.0" + domhandler "^5.0.3" + domutils "^3.0.1" + htmlparser2 "^8.0.1" + parse5 "^7.0.0" + parse5-htmlparser2-tree-adapter "^7.0.0" + cli-cursor@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-4.0.0.tgz#3cecfe3734bf4fe02a8361cbdc0f6fe28c6a57ea" @@ -535,6 +614,11 @@ commander@^12.0.0: resolved "https://registry.yarnpkg.com/commander/-/commander-12.0.0.tgz#b929db6df8546080adfd004ab215ed48cf6f2592" integrity sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA== +commander@^12.1.0: + version "12.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3" + integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA== + commander@^8.1.0: version "8.3.0" resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" @@ -616,11 +700,39 @@ cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +css-select@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6" + integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg== + dependencies: + boolbase "^1.0.0" + css-what "^6.1.0" + domhandler "^5.0.2" + domutils "^3.0.1" + nth-check "^2.0.1" + +css-what@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" + integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== + dargs@^8.0.0: version "8.1.0" resolved "https://registry.yarnpkg.com/dargs/-/dargs-8.1.0.tgz#a34859ea509cbce45485e5aa356fef70bfcc7272" integrity sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw== +data-uri-to-buffer@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz#8a58bb67384b261a38ef18bea1810cb01badd28b" + integrity sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw== + +debug@4, debug@^4.3.4: + version "4.3.5" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" + integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== + dependencies: + ms "2.1.2" + debug@4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" @@ -642,6 +754,15 @@ define-data-property@^1.1.4: es-errors "^1.3.0" gopd "^1.0.1" +degenerator@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/degenerator/-/degenerator-5.0.1.tgz#9403bf297c6dad9a1ece409b37db27954f91f2f5" + integrity sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ== + dependencies: + ast-types "^0.13.4" + escodegen "^2.1.0" + esprima "^4.0.1" + delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -669,6 +790,36 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" +dom-serializer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" + integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.2" + entities "^4.2.0" + +domelementtype@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== + +domhandler@^5.0.2, domhandler@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" + integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== + dependencies: + domelementtype "^2.3.0" + +domutils@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e" + integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA== + dependencies: + dom-serializer "^2.0.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + dot-prop@^5.1.0: version "5.3.0" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" @@ -686,6 +837,11 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== +entities@^4.2.0, entities@^4.4.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + env-paths@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" @@ -720,6 +876,32 @@ escape-string-regexp@^1.0.5: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= +escodegen@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.1.0.tgz#ba93bbb7a43986d29d6041f99f5262da773e2e17" + integrity sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w== + dependencies: + esprima "^4.0.1" + estraverse "^5.2.0" + esutils "^2.0.2" + optionalDependencies: + source-map "~0.6.1" + +esprima@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + ethereum-cryptography@^2.0.0: version "2.1.3" resolved "https://registry.yarnpkg.com/ethereum-cryptography/-/ethereum-cryptography-2.1.3.tgz#1352270ed3b339fe25af5ceeadcf1b9c8e30768a" @@ -832,9 +1014,9 @@ for-each@^0.3.3: version "0.0.0" resolved "https://codeload.github.com/marktoda/forge-gas-snapshot/tar.gz/9161f7c0b6c6788a89081e2b3b9c67592b71e689" -"forge-std@github:foundry-rs/forge-std#5475f85": - version "1.7.6" - resolved "https://codeload.github.com/foundry-rs/forge-std/tar.gz/5475f852e3f530d7e25dfb4596aa1f9baa8ffdfc" +"forge-std@github:foundry-rs/forge-std#1.8.2": + version "1.8.2" + resolved "https://codeload.github.com/foundry-rs/forge-std/tar.gz/978ac6fadb62f5f0b723c996f64be52eddba6801" form-data@^4.0.0: version "4.0.0" @@ -895,6 +1077,16 @@ get-stream@^8.0.1: resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-8.0.1.tgz#def9dfd71742cd7754a7761ed43749a27d02eca2" integrity sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA== +get-uri@^6.0.1: + version "6.0.3" + resolved "https://registry.yarnpkg.com/get-uri/-/get-uri-6.0.3.tgz#0d26697bc13cf91092e519aa63aa60ee5b6f385a" + integrity sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw== + dependencies: + basic-ftp "^5.0.2" + data-uri-to-buffer "^6.0.2" + debug "^4.3.4" + fs-extra "^11.2.0" + git-hooks-list@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/git-hooks-list/-/git-hooks-list-3.1.0.tgz#386dc531dcc17474cf094743ff30987a3d3e70fc" @@ -982,6 +1174,10 @@ graceful-fs@^4.2.0: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== +"halmos-cheatcodes@github:a16z/halmos-cheatcodes#c0d8655": + version "0.0.0" + resolved "https://codeload.github.com/a16z/halmos-cheatcodes/tar.gz/c0d865508c0fee0a11b97732c5e90f9cad6b65a5" + handlebars@4.7.7: version "4.7.7" resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1" @@ -1042,6 +1238,39 @@ homedir-polyfill@^1.0.1: dependencies: parse-passwd "^1.0.0" +html-link-extractor@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/html-link-extractor/-/html-link-extractor-1.0.5.tgz#a4be345cb13b8c3352d82b28c8b124bb7bf5dd6f" + integrity sha512-ADd49pudM157uWHwHQPUSX4ssMsvR/yHIswOR5CUfBdK9g9ZYGMhVSE6KZVHJ6kCkR0gH4htsfzU6zECDNVwyw== + dependencies: + cheerio "^1.0.0-rc.10" + +htmlparser2@^8.0.1: + version "8.0.2" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21" + integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.3" + domutils "^3.0.1" + entities "^4.4.0" + +http-proxy-agent@^7.0.0, http-proxy-agent@^7.0.1: + version "7.0.2" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e" + integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig== + dependencies: + agent-base "^7.1.0" + debug "^4.3.4" + +https-proxy-agent@^7.0.3, https-proxy-agent@^7.0.5: + version "7.0.5" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz#9e8b5013873299e11fab6fd548405da2d6c602b2" + integrity sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw== + dependencies: + agent-base "^7.0.2" + debug "4" + human-signals@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28" @@ -1052,6 +1281,13 @@ husky@>=8: resolved "https://registry.yarnpkg.com/husky/-/husky-9.0.11.tgz#fc91df4c756050de41b3e478b2158b87c1e79af9" integrity sha512-AB6lFlbwwyIqMdHYhwPe+kjOC3Oc5P3nThEoW/AaO2BX3vJDjWPFxYLxokUZOo6RNX20He3AaT8sESs9NJcmEw== +iconv-lite@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + ignore@^5.2.4: version "5.3.1" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" @@ -1093,6 +1329,19 @@ ini@^1.3.4: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== +ip-address@^9.0.5: + version "9.0.5" + resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-9.0.5.tgz#117a960819b08780c3bd1f14ef3c1cc1d3f3ea5a" + integrity sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g== + dependencies: + jsbn "1.1.0" + sprintf-js "^1.1.3" + +is-absolute-url@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-4.0.1.tgz#16e4d487d4fded05cfe0685e53ec86804a5e94dc" + integrity sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A== + is-arguments@^1.0.4: version "1.1.1" resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" @@ -1169,6 +1418,13 @@ is-plain-obj@^4.1.0: resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0" integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg== +is-relative-url@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-relative-url/-/is-relative-url-4.0.0.tgz#4d8371999ff6033b76e4d9972fb5bf496fddfa97" + integrity sha512-PkzoL1qKAYXNFct5IKdKRH/iBQou/oCC85QhXj6WKtUQBliZ4Yfd3Zk27RHu9KQG8r6zgvAA2AQKC9p+rqTszg== + dependencies: + is-absolute-url "^4.0.1" + is-stream@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac" @@ -1193,6 +1449,13 @@ is-windows@^1.0.1: resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== +isemail@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/isemail/-/isemail-3.2.0.tgz#59310a021931a9fb06bbb51e155ce0b3f236832c" + integrity sha512-zKqkK+O+dGqevc93KNsbZ/TqTUFd46MwWjYOoMrjIMZ51eU7DtQG3Wmd9SQQT7i7RVnuTPEiYEWHU3MSbxC1Tg== + dependencies: + punycode "2.x.x" + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -1220,6 +1483,11 @@ js-yaml@^4.1.0: dependencies: argparse "^2.0.1" +jsbn@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" + integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A== + jsel@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/jsel/-/jsel-1.1.6.tgz#9257fee6c6e8ad8e75d5706503fe84f376035896" @@ -1264,6 +1532,17 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== +link-check@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/link-check/-/link-check-5.3.0.tgz#641f907a76858f18e32d457fe2811d431d1b8aec" + integrity sha512-Jhb7xueDgQgBaZzkfOtAyOZEZAIMJQIjUpYD2QY/zEB+LKTY1tWiBwZg8QIDbzQdPBOcqzg7oLQDNcES/tQmXg== + dependencies: + is-relative-url "^4.0.0" + isemail "^3.2.0" + ms "^2.1.3" + needle "^3.3.1" + proxy-agent "^6.4.0" + lint-staged@>=10: version "15.2.2" resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-15.2.2.tgz#ad7cbb5b3ab70e043fa05bff82a09ed286bc4c5f" @@ -1365,6 +1644,39 @@ log-update@^6.0.0: strip-ansi "^7.1.0" wrap-ansi "^9.0.0" +lru-cache@^7.14.1: + version "7.18.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" + integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== + +markdown-link-check@^3.11.0: + version "3.12.2" + resolved "https://registry.yarnpkg.com/markdown-link-check/-/markdown-link-check-3.12.2.tgz#05606ccfdfc14e75d28a15cf31297aa273f1f754" + integrity sha512-GWMwSvxuZn+uGGydi5yywnnDZy08SGps4I/63xqvWT7lxtH4cVLnhgZZYtEcPz/QvgPg9vbH2rvWpa29owMtHA== + dependencies: + async "^3.2.5" + chalk "^5.3.0" + commander "^12.1.0" + link-check "^5.3.0" + lodash "^4.17.21" + markdown-link-extractor "^4.0.2" + needle "^3.3.1" + progress "^2.0.3" + proxy-agent "^6.4.0" + +markdown-link-extractor@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/markdown-link-extractor/-/markdown-link-extractor-4.0.2.tgz#464ff8935d7388f75a354b877d750a7bb705ea32" + integrity sha512-5cUOu4Vwx1wenJgxaudsJ8xwLUMN7747yDJX3V/L7+gi3e4MsCm7w5nbrDQQy8nEfnl4r5NV3pDXMAjhGXYXAw== + dependencies: + html-link-extractor "^1.0.5" + marked "^12.0.1" + +marked@^12.0.1: + version "12.0.2" + resolved "https://registry.yarnpkg.com/marked/-/marked-12.0.2.tgz#b31578fe608b599944c69807b00f18edab84647e" + integrity sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q== + memorystream@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2" @@ -1432,11 +1744,29 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +needle@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/needle/-/needle-3.3.1.tgz#63f75aec580c2e77e209f3f324e2cdf3d29bd049" + integrity sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q== + dependencies: + iconv-lite "^0.6.3" + sax "^1.2.4" + neo-async@^2.6.0: version "2.6.2" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== +netmask@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/netmask/-/netmask-2.0.2.tgz#8b01a07644065d536383835823bc52004ebac5e7" + integrity sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg== + npm-run-path@^5.1.0: version "5.3.0" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-5.3.0.tgz#e23353d0ebb9317f174e93417e4a4d82d0249e9f" @@ -1444,6 +1774,13 @@ npm-run-path@^5.1.0: dependencies: path-key "^4.0.0" +nth-check@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" + integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== + dependencies: + boolbase "^1.0.0" + once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -1484,6 +1821,28 @@ p-locate@^6.0.0: dependencies: p-limit "^4.0.0" +pac-proxy-agent@^7.0.1: + version "7.0.2" + resolved "https://registry.yarnpkg.com/pac-proxy-agent/-/pac-proxy-agent-7.0.2.tgz#0fb02496bd9fb8ae7eb11cfd98386daaac442f58" + integrity sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg== + dependencies: + "@tootallnate/quickjs-emscripten" "^0.23.0" + agent-base "^7.0.2" + debug "^4.3.4" + get-uri "^6.0.1" + http-proxy-agent "^7.0.0" + https-proxy-agent "^7.0.5" + pac-resolver "^7.0.1" + socks-proxy-agent "^8.0.4" + +pac-resolver@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/pac-resolver/-/pac-resolver-7.0.1.tgz#54675558ea368b64d210fd9c92a640b5f3b8abb6" + integrity sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg== + dependencies: + degenerator "^5.0.0" + netmask "^2.0.2" + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -1506,6 +1865,21 @@ parse-passwd@^1.0.0: resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" integrity sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q== +parse5-htmlparser2-tree-adapter@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz#23c2cc233bcf09bb7beba8b8a69d46b08c62c2f1" + integrity sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g== + dependencies: + domhandler "^5.0.2" + parse5 "^7.0.0" + +parse5@^7.0.0: + version "7.1.2" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" + integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw== + dependencies: + entities "^4.4.0" + path-exists@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-5.0.0.tgz#a6aad9489200b21fab31e49cf09277e5116fb9e7" @@ -1551,16 +1925,49 @@ possible-typed-array-names@^1.0.0: resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== -prettier@^2.8.3: +prettier-plugin-solidity@^1.1.3: + version "1.3.1" + resolved "https://registry.yarnpkg.com/prettier-plugin-solidity/-/prettier-plugin-solidity-1.3.1.tgz#59944d3155b249f7f234dee29f433524b9a4abcf" + integrity sha512-MN4OP5I2gHAzHZG1wcuJl0FsLS3c4Cc5494bbg+6oQWBPuEamjwDvmGfFMZ6NFzsh3Efd9UUxeT7ImgjNH4ozA== + dependencies: + "@solidity-parser/parser" "^0.17.0" + semver "^7.5.4" + solidity-comments-extractor "^0.0.8" + +prettier@^2.8.3, prettier@^2.8.7: version "2.8.8" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== +progress@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + +proxy-agent@^6.4.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/proxy-agent/-/proxy-agent-6.4.0.tgz#b4e2dd51dee2b377748aef8d45604c2d7608652d" + integrity sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ== + dependencies: + agent-base "^7.0.2" + debug "^4.3.4" + http-proxy-agent "^7.0.1" + https-proxy-agent "^7.0.3" + lru-cache "^7.14.1" + pac-proxy-agent "^7.0.1" + proxy-from-env "^1.1.0" + socks-proxy-agent "^8.0.2" + proxy-from-env@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== +punycode@2.x.x: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + punycode@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" @@ -1624,6 +2031,16 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" +"safer-buffer@>= 2.1.2 < 3.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +sax@^1.2.4: + version "1.4.1" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.4.1.tgz#44cc8988377f126304d3b3fc1010c733b929ef0f" + integrity sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg== + semver@^5.5.0: version "5.7.2" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" @@ -1634,7 +2051,7 @@ semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.6.0: +semver@^7.5.4, semver@^7.6.0: version "7.6.2" resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== @@ -1703,6 +2120,28 @@ slice-ansi@^7.0.0: ansi-styles "^6.2.1" is-fullwidth-code-point "^5.0.0" +smart-buffer@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" + integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== + +socks-proxy-agent@^8.0.2, socks-proxy-agent@^8.0.4: + version "8.0.4" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz#9071dca17af95f483300316f4b063578fa0db08c" + integrity sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw== + dependencies: + agent-base "^7.1.1" + debug "^4.3.4" + socks "^2.8.3" + +socks@^2.8.3: + version "2.8.3" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.3.tgz#1ebd0f09c52ba95a09750afe3f3f9f724a800cb5" + integrity sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw== + dependencies: + ip-address "^9.0.5" + smart-buffer "^4.2.0" + solc-typed-ast@18.1.3: version "18.1.3" resolved "https://registry.yarnpkg.com/solc-typed-ast/-/solc-typed-ast-18.1.3.tgz#243cc0c5a4f701445ac10341224bf8c18a6ed252" @@ -1773,6 +2212,16 @@ solhint-community@4.0.0: optionalDependencies: prettier "^2.8.3" +solidity-comments-extractor@^0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/solidity-comments-extractor/-/solidity-comments-extractor-0.0.8.tgz#f6e148ab0c49f30c1abcbecb8b8df01ed8e879f8" + integrity sha512-htM7Vn6LhHreR+EglVMd2s+sZhcXAirB1Zlyrv5zBuTxieCvjfnRpd7iZk75m/u6NOlEyQ94C6TWbBn2cY7w8g== + +solmate@^6.6.1: + version "6.7.0" + resolved "https://registry.yarnpkg.com/solmate/-/solmate-6.7.0.tgz#5e07b62498babec273aceb322a3e45d0e058e042" + integrity sha512-iMPr+gKbKjXBB12a+Iz5Tua5r7T4yugHaGXDWSJbBZB4Gr3vLeUUvKeLyMxCWWqk1xlLhFDFFuAmOzeyVBuyvQ== + "solmate@github:transmissions11/solmate#c892309": version "6.2.0" resolved "https://codeload.github.com/transmissions11/solmate/tar.gz/c892309933b25c03d32b1b0d674df7ae292ba925" @@ -1796,7 +2245,7 @@ sort-package-json@2.10.0: semver "^7.6.0" sort-object-keys "^1.1.3" -source-map@^0.6.1: +source-map@^0.6.1, source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== @@ -1806,6 +2255,11 @@ split2@^4.0.0: resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== +sprintf-js@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a" + integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== + src-location@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/src-location/-/src-location-1.1.0.tgz#3f50eeb0c7169432e55b426be6e3a68ebba16218" @@ -1923,6 +2377,11 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +tslib@^2.0.1: + version "2.6.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" + integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== + uglify-js@^3.1.4: version "3.17.4" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.4.tgz#61678cf5fa3f5b7eb789bb345df29afb8257c22c"