Repository navigation
Conversation
CATValidatorV1_1 subclasses CATValidator and commits an execution target and payload hash in the signed constraint, and publishes pulled(token) and entryId() to contracts called during the entry. CATValidatorV1_1Tron is the Tron variant. Adds the sources next to CATValidator, tests, the ABI and sca-parity vectors with a viem spec, a bytecode pin check in CI, a deploy script and a design doc. CATValidator is unchanged. Signed-off-by: georgi <georgi@li.finance>
The forge job installed the toolchain's default, stable, which now resolves to v1.8.x. v1.8 makes --isolate the default and formats test/libs/KeyedOwnable.t.sol differently, so the job no longer matched the repository's last green toolchain. The Bun job already pins v1.7.1. Signed-off-by: georgi <georgi@li.finance>
The entry-id tests made several top-level calls and expected the transient counter to carry across them. Under forge test --isolate, the default since Foundry v1.8, every top-level call is its own transaction, the counter restarts and the tests failed. They now send their entries through SameTxHarness.runAll, a single external call, so they hold with and without isolation. Signed-off-by: georgi <georgi@li.finance>
- NatSpec: @author LIFI on the new contracts, @PARAM for every argument of the nine-argument entry, NatSpec on ICATValidatorV1_1's functions, and PULLED_SEED documented as an arbitrary fixed seed. Comments only: the creation bytecode of CATValidatorV1_1 and CATValidatorV1_1Tron is unchanged; the source blob pins are refreshed. - Design doc: pulled is written once, after the last transfer of the loop, as the code does. The entry id rollback, the domain version and deployV1_1 on a chain without EIP-1153 are described as they behave. - Tests: the base validator is called base (BaseLoop, BASE_GAS_DELTA, test_gas_entry_delta_base_to_v1_1) and the Catapultar account is called account. Names follow the base suite (snake_case, test_fuzz_, test_revert_), the probes' at() helper is seenAt() because at is a future keyword, and section banners are single lines. - sca-parity fixture: schema sca-parity-v1_1/v1, vector receipt-outcome and field account replace the old names. Every hash and address in it is unchanged. - The CATValidatorV1_1Test gas snapshot drops by 22 gas per entry: those entries use the test contract as their ERC-1271 signer, and its dispatcher changed with the test renames. - READMEs: the coverage command's excluded test name, the deployV1_1 EIP-1153 note, and catValidatorV1_1Abi in the TypeScript ABI list. Signed-off-by: georgi <georgi@li.finance>
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. |
4 of 8 tasks
foundry-toolchain v1.7.0 runs the foundryup installer with bash, which fails now that foundryup ships as a compiled binary. Drops the forge-test version pin added on this branch, so that job installs stable again; the bun-test job keeps its v1.7.1 pin. Signed-off-by: georgi <georgi@li.finance>
forge fmt 1.8 braces a nested if/else; the dangling else keeps its binding to the inner if. The file also passes forge fmt 1.7.1. Signed-off-by: georgi <georgi@li.finance>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #44 +/- ##
==========================================
- Coverage 94.01% 91.54% -2.48%
==========================================
Files 43 47 +4
Lines 3260 3715 +455
Branches 54 69 +15
==========================================
+ Hits 3065 3401 +336
- Misses 195 314 +119 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
reednaa
reviewed
Nov 3, 2026
Signed-off-by: georgi <georgi@li.finance>
Signed-off-by: georgi <georgi@li.finance>
CATValidator marks entry, _domainNameAndVersion and _call virtual, the same three hunks as #43; check-v1-bytecode.sh pins its creation hashes, which are unchanged. CATValidatorV1_1 returns version "1.1" from _domainNameAndVersion instead of re-implementing Solady's EIP712, overrides _call with value 0, and disables the base's 7-argument entry with V1EntryDisabled. Signed-off-by: georgi <georgi@li.finance>
Signed-off-by: georgi <georgi@li.finance>
The 9-argument entry no longer takes the base's storage nonReentrant guard: a non-zero entry id means an entry is running, so a nested entry reverts Reentrancy() before any state change. Steady-state entry: 74,754 to 69,739 gas of execution (the storage guard also earned a ~2,800 refund). Signed-off-by: georgi <georgi@li.finance>
The Tron _transfer override's safeTransferETH branch had no test. Signed-off-by: georgi <georgi@li.finance>
…e transient lock entryId() no longer claims ids are never reused, and a zero spend is described as a zero transferFrom. The design doc and README describe the transient lock and the base's three virtual hooks, and the design doc notes the unconstrained-payload case on nonce 0. The standalone reference now uses the same transient lock as the subclass, so the subclass gas test compares like with like (65,810 vs 65,794). Creation bytecode is unchanged; the source blob id is refreshed. Signed-off-by: georgi <georgi@li.finance>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds
CATValidatorV1_1andCATValidatorV1_1Tron, subclasses ofCATValidatoronmain(the branch is merged with85f700d).CATValidatorgainsvirtualonentry,_domainNameAndVersionand_call, the same three hunks as #43;script/check-v1-bytecode.shpins its creation bytecode, which is unchanged.CallProxy,CatapultarandLibExecutionConstraintare not modified.CATValidatoris a hold-and-forward validator. The executor delivers the outcome tokens to the validator during the call. After the call, the validator checks its own balance of each outcome token againstOutcome.amountand forwards that whole balance to the outcome's destination.CATValidatorV1_1keeps this settlement (the inherited_validatePayment) and adds:entrywhose signed constraint also commitsexecutionTargetandpayloadHash. Zero means unconstrained; a zeroexecutionTargeton the reusable nonce 0 revertsZeroDestination.pulled(token), the amount the running entry moved out of the signer, andentryId(), the entry's id within the transaction. Both live in transient storage (EIP-1153) and are meant for contracts called during the entry.entry, so a force-fed native balance cannot block a non-payable target."1.1"(the base is"1", feat(validator): CATValidatorV2 on top of the audited CATValidator #43's V2 is"2")._transferthat skips zero payouts, so{token, 0, refund}outcomes settle on tokens that reject zero transfers.entrydoes not use the base's storagenonReentrant.Design doc:
solidity/design/cat-validator-v1_1.md.Affected packages:
solidityandtypescript.Why
LI.FI settles non-custodial bridge fills through this validator. The signer's execute constraint pulls the deposit to the execution target and requires one unit of a receipt token at a commit address:
{ token: attester, amount: 1, destination: commit }. During the call, the execution target bridges the pulled funds through an attester-gated bridge entrypoint, which reports the exact bridge call to the attester. The attester readspulled(token)andentryId()here, checks that the bridge spent what this entry pulled, and credits one receipt unit to the validator for that entry. After the call,_validatePaymentfinds the receipt and forwards it tocommit. Without an attested bridge there is no receipt, the entry reverts and the deposit stays with the signer.The refund constraint signs nonce 0, the refund address as
executionTargetandkeccak256("")aspayloadHash, so the executor cannot redirect a refund even though its outcome floors are zero.What an attester can rely on:
pulled(token)is non-zero only inside the nine-argumententry. It is written once, after the last allowance transfer, and cleared before the outcomes are paid, so token hooks during the pull and during the payout read zero.entryId()is 1, 2, 3, ... for the nine-argument entries of a transaction and zero elsewhere, so a receipt can be tagged with the entry that earned it.entryforwards no native value to the call.The bridge side is lifinance/contracts#2583:
AttestedBridgeFacet.startBridgeAttestedruns an allow-listed bridge entrypoint of the LI.FI Diamond and reports the exact call to the attester in the same transaction.What an attester must check itself: that the call comes from the bridge entrypoint it trusts, that
pulled(token)is non-zero, that the reported bridge call spends whatpulledreports for the currententryId(), and that it credits only the validator.Next to #43.
CATValidatorV2(#43) also commits a target and a payload hash, but it checks after settlement: it runs a signed validation program over what moved through the validator, and it disables the seven-argumententryby making the base'sentryvirtual. An attester that runs during the call needs what this entry pulled while the call runs. V1.1 publishes that and has no program. It uses the same three base hooks as #43:_domainNameAndVersionreturns"1.1",_callforwards no value, and the seven-argumententryrevertsV1EntryDisabled. It also overrides_transferfor the zero-skipping payout. The two are independent subclasses.Layout
solidity/src/CATValidatorV1_1.sol,solidity/src/CATValidatorV1_1.tron.sol,solidity/src/interfaces/ICATValidatorV1_1.sol,solidity/src/libs/LibExecutionConstraintV1_1.solsolidity/test/CATValidatorV1_1{,.AllowanceParity,.differential,.equivalence}.t.sol,solidity/test/Tron/CATValidatorV1_1.tron.t.sol,solidity/test/reference/CATValidatorV1_1Standalone.sol,solidity/test/vc/ScaParity.t.soland its fixturetypescript/src/abi/CATValidatorV1_1.ts(exported ascatValidatorV1_1Abi),typescript/src/protocol/validatorV1_1.spec.ts,typescript/test/fixtures/sca-parity.v1_1.vectors.json, an opt-in--step CATValidatorV1_1Tronintypescript/scripts/tron/deploy-catapultar.tssolidity/script/deployV1_1.s.solsolidity/script/check-v1-bytecode.shandsolidity/snapshots/v1-bytecode.jsonpin the base's creation hashes (step "Check audited v1 bytecode is unchanged");solidity/script/check-v1-1-bytecode.shandsolidity/snapshots/v1-1-bytecode.jsonpin both V1.1 creation hashes and the four source blob ids; the coverage step gets--ir-minimum; the Foundry install steps followmain(Foundry v1.9.0);typescript/.oxfmtrc.jsonkeeps the mirroredsca-parityvectors out of oxfmt, so the copy stays byte-identical to the Solidity fixtureTypeScript scope. The ABI export, the vectors and a viem spec that recomputes them. The SDK's
ConstrainedAssetTransactiondoes not build V1.1 constraints yet; that path is not ported in this PR.Deploying
deployV1_1is a separate script, so the defaultdeployrun is unchanged. It deploys through the deterministic deployer0x4e59b44847b379578588920cA78FbF26c0B4956Cat salt 0. At the default profile (solc 0.8.35, via-IR,bytecode_hash = 'none') the creation hashes areCATValidatorV1_10x684da2d47165c41001592a4294aabdd835b1d0d9a86184b17070be6221ecfa0candCATValidatorV1_1Tron0xbb496cd2485983c4cfa50de55eb54a3b718af59418d454a51668f35aa274d86a, andCATValidatorV1_1lands at0xA1d38c83e842ebA863238845Dc4fF14F5668041A. Another compiler gives another address. Thetronprofile gives the same bytes.The constructor needs EIP-1153 and reverts without it.
deployV1_1does not check the chain first. On a chain without EIP-1153, forge's local simulation passes and the broadcast deployment reverts on chain: nothing is deployed and the gas is spent (reproduced on an anvil at--hardfork shanghai). Once #43 lands,deployV1_1adopts itsrequireCancun()probe; that is a script-only change.Things to know when reviewing
entry. The seven-argumententryrevertsV1EntryDisabled, so the nine-argumententryis the only way in.ScaParityderives the addresses ofdeployWithDigestaccounts withCatapultarFactory._saltas it is onmain. The fixture's digests, struct hashes and domain do not depend on the factory;saltWithDigestandaccountdo, so accounts created through an older factory must not reuse those two fields.test_reference_is_pinnedpins the creation hash of the standalone reference, so it moves with the compiler. It andtest_gas_entry_delta_base_to_v1_1fail under the unoptimizedforge coverage --ir-minimumbuild, so the coverage step excludes them.entrydoes not compile under the non-IR pipelineforge coverageuses (stack too deep), which is also why feat(validator): CATValidatorV2 on top of the audited CATValidator #43 adds--ir-minimum. forge coverage does not attribute inline-assembly lines, which run on every entry, so the local lcov forCATValidatorV1_1.solreads lines 60/72, branches 12/16 and functions 15/15. The four branches it reports missed are exercised: the constructor probe bytest_revert_constructor_without_tstore(Shanghai EVM) and_call's revert path bytest_entry_bubbles_long_revert_data_verbatim.SameTxHarness.runAll), so they pass with and without--isolate, the default since Foundry 1.8.entrywith one allowance and no call payload uses 20,345 less frame gas than the base'sentry(65,810 vs 86,155), mostly because the base's frame pays the first write of its storagenonReentrantguard.test_gas_entry_delta_base_to_v1_1fails if the delta grows more than 200 gas above that. These are frame numbers under--isolate(the default since Foundry 1.8); steady-state numbers are under Tests.PULLED_SEEDand the constructorrequireare part of the pinned creation code and stay as they are. The seed is an arbitrary fixed constant for the transientpulledslots; its NatSpec says so.CATValidatorV1_1Tronbytes (TCoRpXBWyvyXm7sCDSAKHh4yeyT2HnzVwA) passed: an attested fill that held and forwarded its receipt, a refused fill, a nonce-0 refund, a USDT payout by the validator, and the seven-argumententryrevertingV1EntryDisabled.Merge order with #43
Both PRs open as drafts; whichever merges second resolves the conflicts. Merging #43's head
ed7851f4into7cdb93dconflicts in seven files;src/CATValidator.solmerges cleanly because both PRs add the same threevirtualkeywords:solidity/script/check-v1-bytecode.sh,solidity/snapshots/v1-bytecode.json: both PRs add the same check with the same hashes; the comments name a different subclass. Keep either, naming both subclasses..github/workflows/forge-test.yml: this PR now usesmain's install step; keep one v1 bytecode-check step and this PR's V1.1 step, and keep--ir-minimumplus this PR's--no-match-teston the coverage step.solidity/foundry.toml: onefs_permissionsarray with feat(validator): CATValidatorV2 on top of the audited CATValidator #43's read access ontest/vc/fixturesand this PR's read-write access ontest/vc/fixtures/sca-parity(used byVC_REGEN_SCA_FIXTURE=true).solidity/README.md,typescript/README.md,typescript/src/index.ts: keep both sides (both validators' sections, both ABI exports).With that resolution,
forge testpasses at7cdb93d(444 passed, 9 skipped: #43's RPC-gated tests), and both bytecode checks (check-v1-bytecode.sh,check-v1-1-bytecode.sh) hold.Tests
At
df125fd(main85f700dmerged, base hooks applied,_payoutfolded into the_transferoverrides, transient reentrancy lock, a Tron native-payout test, NatSpec and docs corrections), with Foundry 1.9.0 (the CI pin) and Bun 1.3.12:forge fmt --check: clean../script/check-v1-bytecode.sh:CATValidator0xe3d6838a…andCATValidatorTron0x7ea3b701…, identical before and after thevirtualkeywords../script/check-v1-1-bytecode.sh: re-pinned,ok(the last commit changes only theCATValidatorV1_1.solblob id; both creation hashes are unchanged).forge build --sizes:CATValidatorV1_15,512 B runtime (was 5,932),CATValidatorV1_1Tron5,751 B (was 6,165).forge test: 27 suites, 375 passed, 0 failed (forge-std is now v1.17.0 frommain).testFuzz_entry_v1_overload_is_disabledreplaces the test of the old 7-argument flow;test_entry_leaves_the_storage_guard_slot_untouchedandtestFuzz_nested_entry_reverts_in_pull_and_payoutcover the lock;test_outcome_native_trx_pays_destinationcovers the Tron override's native payout. The standalone reference (test/reference/CATValidatorV1_1Standalone.sol) uses the same transient lock, sotest_gas_entry_delta_standalone_to_subclasscompares like with like (65,810 vs 65,794). Gas snapshots (fresh validators, so the old numbers include the storage guard's first write):entryOneAllowance107,263 to 84,913,entryRefundSweep91,566 to 69,240; the standalone'sentryOneAllowanceStandalone107,012 to 84,897 andentryRefundSweepStandalone91,134 to 69,019.--isolate) on a validator already used once: V1.1 entry 74,754 to 69,739 gas of execution, against the base's 72,704. The storage guard also earned a refund of about 2,800 at the end of the transaction, so the net saving per entry is about 2,200.ScaParityand the TypeScript vectors are unchanged: same digests, salts and accounts.bun install --frozen-lockfile,bun run lint,bun run format:check,bun run build(smoke: 71 named exports): pass.bun run test: 121 passed, 0 failed.At
832f0576, before the merge (Foundry 1.8.6 and 1.7.1; Bun 1.3.14):forge fmt --check: clean.forge build --sizes:CATValidatorV1_15,932 B runtime, 6,716 B initcode;CATValidatorV1_1Tron6,165 B, 6,949 B../script/check-v1-1-bytecode.sh: both creation hashes and all four blob idsok.forge test -vvv: 27 suites, 372 passed, 0 failed (main: 281). The V1.1 suites add 91 (54 + 4 + 5 + 6 + 15 + 7), 32 of them fuzz. Gas snapshots:snapshots/CATValidatorV1_1Test.json,snapshots/ScaParityTest.json.forge coverage --ir-minimum --no-match-coverage "(script|test)" --no-match-test "(test_reference_is_pinned|test_gas_entry_delta_base_to_v1_1)" --report lcov: 370 passed, 0 failed.--isolateby default) and 1.7.1:forge fmt --checkclean,forge test372 passed, 0 failed, with the same creation hashes.bun install --frozen-lockfile,bun run build(smoke: 71 named exports match across ESM and CJS) andbun run coverage:lcov: 112 passed, 0 failed. The spec recomputes the fixture's struct hashes, digests, factory salts and account addresses with viem, and checks the ABI selectors.forge script deployV1_1 --sig "run(string[])" "[local]" --multi --broadcastagainst a local anvil: deploysCATValidatorV1_1(5,932 B) at that head's address0xd7f0DA58…, andeip712Domain()reports"CAT Validator","1.1"; a second run broadcasts nothing. Against an anvil at--hardfork shanghai, the broadcast reverts and no code is deployed.Audit scope
New Solidity:
src/CATValidatorV1_1.sol(263 lines),src/libs/LibExecutionConstraintV1_1.sol(35),src/CATValidatorV1_1.tron.sol(25),src/interfaces/ICATValidatorV1_1.sol(41) andscript/deployV1_1.s.sol(24). The base gains threevirtualkeywords; its bytecode is unchanged (check-v1-bytecode.sh).Linear: CORE-1787