Client contracts for running Enso Shortcuts.
Requires Foundry.
$ forge install
$ pnpm install
$ pnpm foundry:update
$ forge build
$ forge test
Bulloak is a Solidity test generator based on the Branching Tree Technique
(BTT).
See the Bulloak repo for full
documentation, examples, and advanced usage.
- Bulloak (install with
cargo install bulloak
) - (Optional) VSCode Ascii Tree Generator extension for easier tree editing
-
Create a Tree File
Create a
<ContractName>.tree
file (e.g.,ERC4337CloneFactory.tree
).
Each tree describes the branching logic of a function or contract using ASCII art.Example:
ERC4337CloneFactory_DelegateDeploy # when EnsoReceiver does not exist ## it should emit CloneDeployed event ## it should deploy clone ## it should initialize clone # when EnsoReceiver already exists ## it should revert
-
Convert to Tree Structure
Use ASCII tree tools or an LLM to convert your outline to a tree with
├
,└
, and│
characters.
You can group multiple trees in a single file, but only one can be scaffolded at a time (comment out others).Example:
// ERC4337CloneFactory_Deploy // ├── when EnsoReceiver does not exist // │ ├── it should emit CloneDeployed event // │ ├── it should deploy clone // │ └── it should initialize clone // └── when EnsoReceiver already exists // └── it should revert ERC4337CloneFactory_DelegateDeploy ├── when EnsoReceiver does not exist │ ├── it should emit CloneDeployed event │ ├── it should deploy clone │ └── it should initialize clone └── when EnsoReceiver already exists └── it should revert
-
Scaffold the Solidity Test
Run:
bulloak scaffold path/to/YourContract.tree
This generates a
.t.sol
test file with the contract and test stubs. -
Review and Finalize
- Set the correct SPDX license and pragma.
- Add necessary imports.
- Update the contract name and inheritance as needed.
- Naming pattern suggestion:
<ContractName>_<Method>[_When<Condition>][_As<Role>][_Unit|Int|Fork][_Concrete|Fuzz]_Test
Mutation testing helps you measure the effectiveness of your test suite by introducing small changes ("mutants") to your code and checking if your tests catch them.
- Certora Gambit (for mutation generation)
- Node.js (for running the mutation test script)
- A working
gambit.config.json
(see below)
-
Create or update your Gambit config
Make sure you have a
gambit.config.json
in your project root.
See Certora Gambit docs for config options. -
Generate Mutations
gambit mutate --json gambit.config.json
This will create mutated versions of your contracts in
gambit_out/mutants/
. -
Run Mutation Tests
You can run mutation tests using the provided script:
node ./mutationTest.mjs --matchContract 'EnsoReceiver_.*_Unit_Concrete_Test'
- Use the
--matchContract
flag with a regex to select which test contracts to run against each mutant. - You can also use
--matchMutant
to filter which mutants to test (by contract name or pattern).
Example:
node ./mutationTest.mjs --matchContract 'EnsoReceiver_.*_Unit_Concrete_Test' --matchMutant EnsoReceiver
This will run all test contracts matching the pattern against all mutants of
EnsoReceiver
.Tip: You can use more complex regexes to match multiple contracts, e.g.
--matchContract 'EnsoReceiver_.*_(Unit|Fork)_Concrete_Test'
- Use the
-
(Optional) Automate with a Script
For more complex or repeated runs, you can create a JS script in
scripts/
, e.g.
scripts/runEnsoCheckoutMutationTests.mjs
.
The mutationTest.mjs
script is based on
ibourn/gambit-mutation-testing.
For
a full list of available options and advanced usage, see the
original script documentation.
Some useful options include:
--matchContract "<pattern>"
– Only run tests for contracts matching the regex.--noMatchContract "<pattern>"
– Exclude contracts matching the regex.--matchTest "<pattern>"
– Only run test functions matching the regex.--noMatchTest "<pattern>"
– Exclude test functions matching the regex.--matchMutant "<pattern>"
– Only test mutants for source files matching the pattern.--verbose true
– Show detailed output in the console.--debug true
– Save detailed logs to thetestLogs
folder.
- The script will:
- Backup the original contract file (in
src/
) - Replace it with each mutant, one at a time
- Run your Foundry tests with the specified contract filter
- Restore the original file after each mutant
- Log results and mutation score
- Backup the original contract file (in
- Do not stage or commit mutated files during or after an interrupted test run. Always ensure the original contract files are restored before committing.
- Always use a regex for
--matchContract
if you want to match multiple contracts.
Example:--matchContract 'EnsoReceiver_.*_Unit_Concrete_Test'
- Do not stage or commit files in
gambit_out/
ortempBackup/
—these are generated and temporary. - If you see errors about missing files (e.g.,
ENOENT: no such file or directory, lstat 'delegate/EnsoReceiver.sol'
), make sure your source files are insrc/
and the script is up to date (see #Path Issues below). - Check your test coverage: Surviving mutants indicate untested or weakly tested code paths.
- If you reorganize or rename your contracts, ensure the mutation config and script is updated accordingly.
For more details and advanced script options, see the gambit-mutation-testing script documentation.
Copy .env.example
to .env
, fill out required values.
$ forge script Deployer --broadcast --fork-url <network>
Example of how manually verifying a contract with constructor args after deployment:
forge verify-contract \
--watch \
--chain polygon \
0xDb5b96dC4CE3E0E44d30279583F926363eFaE29f \
src/helpers/FeeSplitter.sol:FeeSplitter \
--verifier etherscan \
--etherscan-api-key <string:etherscan-api-ke> \
--constructor-args $(cast abi-encode "constructor(address,address[],uint16[])" "0x6AA68C46eD86161eB318b1396F7b79E386e88676" "[0xBfC330020E3267Cea008718f1712f1dA7F0d32A9,0x6AA68C46eD86161eB318b1396F7b79E386e88676]" "[1,1]")