Client contracts for running Enso Shortcuts.
Requires Foundry.
$ forge install
$ pnpm install
$ pnpm foundry:update
$ forge build$ forge testBulloak 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>.treefile (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.soltest 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.jsonin 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
--matchContractflag with a regex to select which test contracts to run against each mutant. - You can also use
--matchMutantto filter which mutants to test (by contract name or pattern).
Example:
node ./mutationTest.mjs --matchContract 'EnsoReceiver_.*_Unit_Concrete_Test' --matchMutant EnsoReceiverThis 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
test/scripts/, e.g.test/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 thetestLogsfolder.
- 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
--matchContractif 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.
This repo never reads a raw private key from .env. Deployer scripts
broadcast keylessly (vm.startBroadcast() with no argument) and the signer
comes from an encrypted
Foundry keystore,
picked by name with --account.
cast wallet import enso-deployer --interactive # create one (hidden prompt)
cast wallet list # which ones exist
cast wallet address --account enso-deployer # its public addressHeads up: anything that resolves a keystore signer unlocks it, so --account
asks for the password even when nothing is broadcast. No tty, no run.
Then copy .env.example to .env and fill in RPC URLs and verifier keys.
Owners are not read from .env; they come from
ChainOwner.ownerFor(chainId). The standalone solver deployers read an OWNER
variable instead.
Preferred — the wrapper picks the right per-network flags and uses the
enso-deployer keystore (override with DEPLOYER_ACCOUNT):
$ ./.bash/deploy.sh <Deployer.s.sol> <network> broadcast <verifier><verifier> is one of etherscan (Etherscan v2 chains, key from
ETHEREUM_BLOCKSCAN_KEY), blockscout (per-network URL in the wrapper),
routescan, tempo, or sourcify (keyless, any chain Sourcify lists; the only
route for Arc mainnet, whose Blockscout is behind Cloudflare Access). Leave it
out to deploy without verifying.
Or invoke forge script directly:
$ forge script Deployer --account enso-deployer --broadcast --rpc-url <network>Drop the broadcast argument and the script only simulates. Nothing is sent.
$ DEPLOYER_ADDRESS=<public address> ./.bash/deploy.sh <Deployer.s.sol> <network>DEPLOYER_ADDRESS makes the wrapper simulate with --sender, so there is no
password prompt and no tty needed. CI can run it.
Worth knowing:
- Deploys go through the CREATE2 factory, so the sender does not change the resulting addresses. A dry run predicts exactly what a broadcast produces.
- Check those addresses against the ones your integrations already expect. A mismatch means the bytecode moved.
- It cannot prove the sender may transact on a permissioned chain. Gas estimation succeeds for any address.
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-key> \
--constructor-args $(cast abi-encode "constructor(address,address[],uint16[])" "0x6AA68C46eD86161eB318b1396F7b79E386e88676" "[0xBfC330020E3267Cea008718f1712f1dA7F0d32A9,0x6AA68C46eD86161eB318b1396F7b79E386e88676]" "[1,1]")