Skip to content

Commit 01e8f91

Browse files
simon-somethingwei3erHase0xteddybear
authored
feat(test): fuzzed and symbolic tests (#146)
* 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 <[email protected]> * chore: format Co-authored-by: Weißer Hase <[email protected]> * chore: typo Co-authored-by: Weißer Hase <[email protected]> * chore: typo Co-authored-by: Weißer Hase <[email protected]> * 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 <[email protected]> * 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 <[email protected]> * 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 <[email protected]> Co-authored-by: teddy <[email protected]> Co-authored-by: Weißer Hase <[email protected]>
1 parent 67a6b41 commit 01e8f91

24 files changed

+2510
-10
lines changed

Diff for: .gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,7 @@ broadcast/*/*/*
1919

2020
# Out dir
2121
out
22+
23+
# echidna corpuses
24+
**/corpuses/*
25+
**/crytic-export/*

Diff for: foundry.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ fs_permissions = [{ access = "read-write", path = ".forge-snapshots/"}]
1717
# 2018: function can be view, so far only caused by mocks
1818
# 2394: solc insists on reporting on every transient storage use
1919
# 5574, 3860: bytecode size limit, so far only caused by test contracts
20-
ignored_error_codes = [2018, 2394, 5574, 3860]
20+
# 1878: Some imports don't have the license identifier
21+
ignored_error_codes = [2018, 2394, 5574, 3860, 1878]
2122
deny_warnings = true
2223

2324
[profile.optimized]

Diff for: package.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"script:testnet": "forge script TestnetScript -vvvvv --rpc-url $SEPOLIA_RPC --broadcast --chain sepolia --private-key $SEPOLIA_DEPLOYER_PK --verify",
3030
"smock": "smock-foundry --contracts src/contracts",
3131
"test": "yarn test:integration && yarn test:unit",
32+
"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",
3233
"test:integration": "forge test --ffi --match-path 'test/integration/**' -vvv --isolate",
3334
"test:local": "FOUNDRY_FUZZ_RUNS=100 forge test -vvv",
3435
"test:scaffold": "bulloak check --fix test/unit/**/*.tree && forge fmt",
@@ -42,6 +43,7 @@
4243
},
4344
"dependencies": {
4445
"@cowprotocol/contracts": "github:cowprotocol/contracts.git#a10f40788a",
46+
"@crytic/properties": "https://github.com/crytic/properties.git",
4547
"@openzeppelin/contracts": "5.0.2",
4648
"composable-cow": "github:cowprotocol/composable-cow.git#24d556b",
4749
"cow-amm": "github:cowprotocol/cow-amm.git#6566128",
@@ -53,7 +55,8 @@
5355
"@defi-wonderland/natspec-smells": "1.1.3",
5456
"@defi-wonderland/smock-foundry": "1.5.0",
5557
"forge-gas-snapshot": "github:marktoda/forge-gas-snapshot#9161f7c",
56-
"forge-std": "github:foundry-rs/forge-std#5475f85",
58+
"forge-std": "github:foundry-rs/forge-std#1.8.2",
59+
"halmos-cheatcodes": "github:a16z/halmos-cheatcodes#c0d8655",
5760
"husky": ">=8",
5861
"lint-staged": ">=10",
5962
"solhint-community": "4.0.0",

Diff for: remappings.txt

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ cowprotocol/=node_modules/@cowprotocol/contracts/src/
77
@composable-cow/=node_modules/composable-cow/
88
@cow-amm/=node_modules/cow-amm/src
99
lib/openzeppelin/=node_modules/@openzeppelin
10+
halmos-cheatcodes=node_modules/halmos-cheatcodes
11+
@crytic/=node_modules/@crytic/
1012

1113
contracts/=src/contracts
1214
interfaces/=src/interfaces

Diff for: src/contracts/BCoWPool.sol

+1-1
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ contract BCoWPool is IERC1271, IBCoWPool, BPool, BCoWConst {
145145
* @dev Grants infinite approval to the vault relayer for all tokens in the
146146
* pool after the finalization of the setup. Also emits COWAMMPoolCreated() event.
147147
*/
148-
function _afterFinalize() internal override {
148+
function _afterFinalize() internal virtual override {
149149
uint256 tokensLength = _tokens.length;
150150
for (uint256 i; i < tokensLength; i++) {
151151
IERC20(_tokens[i]).forceApprove(VAULT_RELAYER, type(uint256).max);

Diff for: src/contracts/BFactory.sol

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ contract BFactory is IBFactory {
4242
}
4343

4444
/// @inheritdoc IBFactory
45-
function collect(IBPool bPool) external {
45+
function collect(IBPool bPool) external virtual {
4646
if (msg.sender != _bDao) {
4747
revert BFactory_NotBDao();
4848
}

Diff for: test/SUMMARY.md

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Tests Summary
2+
3+
## Warning
4+
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)).
5+
By using `ForTest` contracts in for property tests (which avoid using transient storage and `mcopy`) we managed to make the tests run under Cancun.
6+
7+
## Unit tests
8+
Our unit tests are covering every branches, using the branched-tree technique with [Bulloak](https://github.com/alexfertel/bulloak).
9+
10+
## Integration tests
11+
Integration tests are covering various happy paths and not-so-happy paths, on a mainnet fork.
12+
13+
## Property tests
14+
We identified 24 properties. We challenged these either in a long-running fuzzing campaign or via symbolic execution (for 8 chosen properties).
15+
16+
### Fuzzing campaign
17+
18+
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).
19+
20+
#### Limitations/future improvements
21+
Currently, the swap logic are tested against the swap in/out functions (and, in a similar way, liquidity management via the join/exit function).
22+
23+
### Formal verification: Symbolic Execution
24+
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).
25+
26+
Additional properties from BNum were tested independently too (with severe limitations due to previously mentionned loop unrolling boundaries).
27+
28+
# Notes
29+
The bmath corresponding equations are:
30+
31+
**Spot price:**
32+
$$\text{spotPrice} = \frac{\text{tokenBalanceIn}/\text{tokenWeightIn}}{\text{tokenBalanceOut}/\text{tokenWeightOut}} \cdot \frac{1}{1 - \text{swapFee}}$$
33+
34+
35+
**Out given in:**
36+
$$\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)$$
37+
38+
39+
**In given out:**
40+
$$\text{tokenAmountIn} = \frac{\text{tokenBalanceIn} \cdot \left( \frac{\text{tokenBalanceOut}}{\text{tokenBalanceOut} - \text{tokenAmountOut}} \right)^{\frac{\text{tokenWeightOut}}{\text{tokenWeightIn}}} - 1}{1 - \text{swapFee}}$$
41+
42+
43+
**Pool out given single in**
44+
$$\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}$$
45+
46+
47+
**Single in given pool out**
48+
$$\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}}$$
49+
50+
51+
**Single out given pool in**
52+
$$\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)$$
53+
54+
55+
**Pool in given single out**
56+
$$\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}}$$
57+
58+
59+
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
60+
```
61+
// If n is even, then x^n = (x^2)^(n/2).
62+
// If n is odd, then x^n = x * x^(n-1),
63+
// and applying the equation for even x gives
64+
// x^n = x * (x^2)^((n-1) / 2).
65+
//
66+
// Also, EVM division is flooring and
67+
// floor[(n-1) / 2] = floor[n / 2].
68+
```

Diff for: test/invariants/.solhint.json

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"rules": {
3+
"custom-errors": "off",
4+
"no-empty-blocks":"off",
5+
"reason-string": "off",
6+
"reentrancy": "off",
7+
"style-guide-casing": [ "warn", {
8+
"ignoreVariables": true,
9+
"ignorePublicFunctions": true,
10+
"ignoreExternalFunctions": true
11+
}]
12+
}
13+
}

Diff for: test/invariants/PROPERTIES.md

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
| Properties | Type | Id | Halmos | Echidna |
2+
| ------------------------------------------------------------------------------------------- | ------------------- | --- | ------ | ------- |
3+
| BFactory should always be able to deploy new pools | Unit | 1 | [x] | [x] |
4+
| BFactory's BDao should always be modifiable by the current BDao | Unit | 2 | [x] | [x] |
5+
| BFactory should always be able to transfer the BToken to the BDao, if called by it | Unit | 3 | [x] | [x] |
6+
| the amount received can never be less than min amount out | Unit | 4 | :( | [x] |
7+
| the amount spent can never be greater than max amount in | Unit | 5 | :( | [x] |
8+
| swap fee can only be 0 (cow pool) | Valid state | 6 | | [x] |
9+
| total weight can be up to 50e18 | Variable transition | 7 | [x] | [x] |
10+
| BToken increaseApproval should increase the approval of the address by the amount* | Variable transition | 8 | | [x] |
11+
| BToken decreaseApproval should decrease the approval to max(old-amount, 0)* | Variable transition | 9 | | [x] |
12+
| a pool can either be finalized or not finalized | Valid state | 10 | | [x] |
13+
| a finalized pool cannot switch back to non-finalized | State transition | 11 | | [x] |
14+
| a non-finalized pool can only be finalized when the controller calls finalize() | State transition | 12 | [x] | [x] |
15+
| an exact amount in should always earn the amount out calculated in bmath | High level | 13 | :( | [x] |
16+
| an exact amount out is earned only if the amount in calculated in bmath is transfered | High level | 14 | :( | [x] |
17+
| there can't be any amount out for a 0 amount in | High level | 15 | :( | [x] |
18+
| the pool btoken can only be minted/burned in the join and exit operations | High level | 16 | | [x] |
19+
| ~~a direct token transfer can never reduce the underlying amount of a given token per BPT~~ | High level | 17 | :( | # |
20+
| ~~the amount of underlying token when exiting should always be the amount calculated in bmath~~ | High level | 18 | :( | # |
21+
| a swap can only happen when the pool is finalized | High level | 19 | | [x] |
22+
| bounding and unbounding token can only be done on a non-finalized pool, by the controller | High level | 20 | [x] | [x] |
23+
| there always should be between MIN_BOUND_TOKENS and MAX_BOUND_TOKENS bound in a pool | High level | 21 | | [x] |
24+
| only the settler can commit a hash | High level | 22 | [x] | [x] |
25+
| when a hash has been commited, only this order can be settled | High level | 23 | [ ] | [ ] |
26+
| BToken should not break the ToB ERC20 properties** | High level | 24 | | [x] |
27+
| Spot price after swap is always greater than before swap | High level | 25 | | [x] |
28+
29+
> (*) Bundled with 24
30+
31+
> (**) [Trail of Bits ERC20 properties](https://github.com/crytic/properties?tab=readme-ov-file#erc20-tests)
32+
33+
<br>`[ ]` planed to implement and still to do
34+
<br>`[x]` implemented and tested
35+
<br>`:(` implemented but test not passing due to an external factor (tool limitation - eg halmos max unrolling loop, etc)
36+
<br>`#` implemented but deprecated feature / property
37+
<br>`` empty not implemented and will not be (design, etc)
38+
39+
# Unit-test properties for the math libs (BNum and BMath):
40+
41+
btoi should always return the floor(a / BONE) == (a - a%BONE) / BONE
42+
43+
bfloor should always return (a - a % BONE)
44+
45+
badd should be commutative
46+
badd should be associative
47+
badd should have 0 as identity
48+
badd result should always be gte its terms
49+
badd should never sum terms which have a sum gt uint max
50+
badd should have bsub as reverse operation
51+
52+
bsub should not be associative
53+
bsub should have 0 as identity
54+
bsub result should always be lte its terms
55+
bsub should alway revert if b > a (duplicate with previous tho)
56+
57+
bsubSign should not be commutative sign-wise
58+
bsubSign should be commutative value-wise
59+
bsubSign result should always be negative if b > a
60+
bsubSign result should always be positive if a > b
61+
bsubSign result should always be 0 if a == b
62+
63+
bmul should be commutative
64+
bmul should be associative
65+
bmul should be distributive
66+
bmul should have 1 as identity
67+
bmul should have 0 as absorving
68+
bmul result should always be gte a and b
69+
70+
bdiv should be bmul reverse operation // <-- unsolved
71+
bdiv should have 1 as identity
72+
bdiv should revert if b is 0 // <-- impl with wrapper to have low lvl call
73+
bdiv result should be lte a
74+
75+
bpowi should return 1 if exp is 0
76+
0 should be absorbing if base
77+
1 should be identity if base
78+
1 should be identity if exp
79+
bpowi should be distributive over mult of the same base x^a * x^b == x^(a+b)
80+
bpowi should be distributive over mult of the same exp a^x * b^x == (a*b)^x
81+
power of a power should mult the exp (x^a)^b == x^(a*b)
82+
83+
bpow should return 1 if exp is 0
84+
0 should be absorbing if base
85+
1 should be identity if base
86+
1 should be identity if exp
87+
bpow should be distributive over mult of the same base x^a * x^b == x^(a+b)
88+
bpow should be distributive over mult of the same exp a^x * b^x == (a*b)^x
89+
power of a power should mult the exp (x^a)^b == x^(a*b)
90+
91+
calcOutGivenIn should be inv with calcInGivenOut
92+
calcInGivenOut should be inv with calcOutGivenIn
93+
~~calcPoolOutGivenSingleIn should be inv with calcSingleInGivenPoolOut~~
94+
~~calcSingleOutGivenPoolIn should be inv with calcPoolInGivenSingleOut~~

Diff for: test/invariants/fuzz/BMath.t.sol

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.8.25;
3+
4+
import {EchidnaTest} from '../helpers/AdvancedTestsUtils.sol';
5+
import {BMath} from 'contracts/BMath.sol';
6+
7+
contract FuzzBMath is EchidnaTest {
8+
BMath bMath;
9+
10+
uint256 immutable MIN_WEIGHT;
11+
uint256 immutable MAX_WEIGHT;
12+
uint256 immutable MIN_FEE;
13+
uint256 immutable MAX_FEE;
14+
15+
/**
16+
* NOTE: These values were chosen to pass the fuzzing tests
17+
* @dev Reducing BPOW_PRECISION may allow broader range of values increasing the gas cost
18+
*/
19+
uint256 constant MAX_BALANCE = 1_000_000e18;
20+
uint256 constant MIN_BALANCE = 100e18;
21+
uint256 constant MIN_AMOUNT = 1e12;
22+
uint256 constant TOLERANCE_PRECISION = 1e18;
23+
uint256 constant MAX_TOLERANCE = 1e18 + 1e15; //0.1%
24+
25+
constructor() {
26+
bMath = new BMath();
27+
28+
MIN_WEIGHT = bMath.MIN_WEIGHT();
29+
MAX_WEIGHT = bMath.MAX_WEIGHT();
30+
MIN_FEE = bMath.MIN_FEE();
31+
MAX_FEE = bMath.MAX_FEE();
32+
}
33+
34+
// calcOutGivenIn should be inverse of calcInGivenOut
35+
function testCalcInGivenOut_InvCalcInGivenOut(
36+
uint256 tokenBalanceIn,
37+
uint256 tokenWeightIn,
38+
uint256 tokenBalanceOut,
39+
uint256 tokenWeightOut,
40+
uint256 tokenAmountIn,
41+
uint256 swapFee
42+
) public view {
43+
tokenWeightIn = clamp(tokenWeightIn, MIN_WEIGHT, MAX_WEIGHT);
44+
tokenWeightOut = clamp(tokenWeightOut, MIN_WEIGHT, MAX_WEIGHT);
45+
tokenAmountIn = clamp(tokenAmountIn, MIN_AMOUNT, MAX_BALANCE);
46+
tokenBalanceOut = clamp(tokenBalanceOut, MIN_BALANCE, MAX_BALANCE);
47+
tokenBalanceIn = clamp(tokenBalanceIn, MIN_BALANCE, MAX_BALANCE);
48+
swapFee = clamp(swapFee, MIN_FEE, MAX_FEE);
49+
50+
uint256 calc_tokenAmountOut =
51+
bMath.calcOutGivenIn(tokenBalanceIn, tokenWeightIn, tokenBalanceOut, tokenWeightOut, tokenAmountIn, swapFee);
52+
53+
uint256 calc_tokenAmountIn =
54+
bMath.calcInGivenOut(tokenBalanceIn, tokenWeightIn, tokenBalanceOut, tokenWeightOut, calc_tokenAmountOut, swapFee);
55+
56+
assert(
57+
tokenAmountIn >= calc_tokenAmountIn
58+
? (tokenAmountIn * TOLERANCE_PRECISION / calc_tokenAmountIn) <= MAX_TOLERANCE
59+
: (calc_tokenAmountIn * TOLERANCE_PRECISION / tokenAmountIn) <= MAX_TOLERANCE
60+
);
61+
}
62+
63+
// calcInGivenOut should be inverse of calcOutGivenIn
64+
function testCalcOutGivenIn_InvCalcOutGivenIn(
65+
uint256 tokenBalanceIn,
66+
uint256 tokenWeightIn,
67+
uint256 tokenBalanceOut,
68+
uint256 tokenWeightOut,
69+
uint256 tokenAmountOut,
70+
uint256 swapFee
71+
) public view {
72+
tokenWeightIn = clamp(tokenWeightIn, MIN_WEIGHT, MAX_WEIGHT);
73+
tokenWeightOut = clamp(tokenWeightOut, MIN_WEIGHT, MAX_WEIGHT);
74+
tokenAmountOut = clamp(tokenAmountOut, MIN_AMOUNT, MAX_BALANCE);
75+
tokenBalanceOut = clamp(tokenBalanceOut, MIN_BALANCE, MAX_BALANCE);
76+
tokenBalanceIn = clamp(tokenBalanceIn, MIN_BALANCE, MAX_BALANCE);
77+
swapFee = clamp(swapFee, MIN_FEE, MAX_FEE);
78+
79+
uint256 calc_tokenAmountIn =
80+
bMath.calcInGivenOut(tokenBalanceOut, tokenWeightOut, tokenBalanceIn, tokenWeightIn, tokenAmountOut, swapFee);
81+
82+
uint256 calc_tokenAmountOut =
83+
bMath.calcOutGivenIn(tokenBalanceOut, tokenWeightOut, tokenBalanceIn, tokenWeightIn, calc_tokenAmountIn, swapFee);
84+
85+
assert(
86+
tokenAmountOut >= calc_tokenAmountOut
87+
? (tokenAmountOut * TOLERANCE_PRECISION / calc_tokenAmountOut) <= MAX_TOLERANCE
88+
: (calc_tokenAmountOut * TOLERANCE_PRECISION / tokenAmountOut) <= MAX_TOLERANCE
89+
);
90+
}
91+
}

Diff for: test/invariants/fuzz/BMath.yaml

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# https://github.com/crytic/echidna/blob/master/tests/solidity/basic/default.yaml for more options
2+
testMode: assertion
3+
corpusDir: test/invariants/fuzz/corpuses/BMath/
4+
coverageFormats: ["html","lcov"]
5+
allContracts: false
6+
testLimit: 50000
7+
seqLen: 1

0 commit comments

Comments
 (0)