Repository navigation
Add a retargetter path to the §A.1 request-contract checks - #27
Merged
Merged
Conversation
When a request contract's owner() is on the new acceptedRetargetters policy set, §A.1 skips the factory, owner and puller / consumer role checks (no role-events scan) and instead reads the retargetter's live operation(): the contract must be its attached operation request and the operation's repaymentDeadline must sit at least minRetargetterRepaymentBufferSeconds ahead of now (default 80 days, the contract's MIN_DEADLINE_BUFFER). Nothing is cached on this path. §A.4 whitelist ops inherit it per request contract; every other request contract keeps the classic path. The coordinator exposes the set as GUARDIAN_ACCEPTED_RETARGETTERS and the buffer as GUARDIAN_MIN_RETARGETTER_REPAYMENT_BUFFER_SECONDS. Ships retargetterAbi and a MockRetargetter fixture whose operation() tuple matches the production contract.
In-range: viem 2.56.3, zod 4.6.2, @elysiajs/openapi 1.4.16, @noble/hashes 2.4.0, @aws-sdk/client-kms 3.1130.0, prool 0.2.15, @types/bun 1.4.2, oxlint 1.82.0. The elysia override moves to 1.4.30. Majors, each reviewed against its migration notes: - better-result 3.0.1: TaggedError subclasses drop the trailing factory call; matchError / isTaggedError / tryPromise are unchanged. - vitest 5.0.0: nothing here uses vi.mock hoisting, sequential, bench or the removed entry points; unit and integration suites pass. - @google-cloud/kms 6.1.0: only raises the Node floor to 22. - oxfmt 0.67.0: every file already formats identically. Left as-is: @changesets/cli 3 (changes private-package versioning and the version command's exit code, which the release workflow relies on), @noble/curves 2 (viem/ox pin 1.9.1, and the DER API used by the KMS signer was renamed), typescript 7 (needs the 6.0 step first).
🦋 Changeset detectedLatest commit: c9dabf5 The changes in this PR will be included in the next version bump. This PR includes changesets to release 4 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
foundryup is now a compiled binary and the v1.8.0 action still invoked it through bash, so every run failed at the install step with "cannot execute binary file" (foundry-rs/foundry-toolchain#170).
packageManager, the coordinator image base tags, and the README pin. Frozen install, typecheck, lint, build, unit and integration suites all pass under 1.4.2 with the lockfile unchanged.
LukeHackett12
approved these changes
Sep 14, 2026
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
1. A retargetter path for the §A.1 request-contract checks.
A
Retargetter(grunt/src/manager/rebalancer/Retargetter.sol) deploys its own Request instartRetargetting— through its bound factory, with itself as owner, puller and consumer — so the classic §A.1 checks can never pass for it (the retargetter is on none of the accepted owner / puller / consumer sets), and a full role-events scan proves nothing the retargetter's own code doesn't already guarantee.New optional policy set
acceptedRetargetters(coordinator:GUARDIAN_ACCEPTED_RETARGETTERS). The stage-1 multicall is unchanged; when the request contract'sowner()is on the set,runA1branches instead of scanning:One extra read — the retargetter's
operation()— backs the two new checks:operation().requestmust equal the request contract, andoperation().repaymentDeadlinemust sit at leastminRetargetterRepaymentBufferSecondsahead of now. NogetBlockNumber, nogetLogs. §A.4 whitelist ops delegate torunA1per contract, so they inherit the path. Every other request contract keeps the classic path; unset, nothing changes.Minimum runway: 80 days (default). The Request is deployed with
REPAYMENT_DEADLINE_OFFSET= 90 days, and on chainLibStorage.checkConsumptionWindowrefuses to start the loan clock with less thanMIN_DEADLINE_BUFFER= 80 days remaining. A signature issued below that floor could never be consumed, so the Guardian mirrors it — leaving a 10-day window afterstartRetargettingin which bindings can be signed. Configurable viaGUARDIAN_MIN_RETARGETTER_REPAYMENT_BUFFER_SECONDS(non-negative integer, validated at construction); measured fromnow, like the other checks.Choices worth a look:
owner()round-trip for every classic request.operation().requestflips at operation boundaries (startRetargettingsets it, resolution clears it), so an entry could outlive the operation that made it true; the path costs no scan, so there's nothing to amortise. A cached classic-path entry whose owner is now a listed retargetter is treated as a miss (left to expire, not evicted) so listing a retargetter takes effect immediately.operation()is a 503, not a 422. Same rule as a misconfigured accepted factory: the address comes from operator configuration, so the failure is logged at error level and never cached as a client-blamed rejection.info, notwarn. UnliketrustedRequestContracts, this path does verify something on chain; every retargetting operation would otherwise emit a warning in normal operation.2. Fixture:
MockRetargetter. Same pattern asOwnableMockFund— exposes onlyoperation(), with the production return tuple (asserted equal to the realRetargetterartifact's ABI,internalTypeaside), plus an unauthenticatedsetOperation.deployGuardianStacknow also mints a second real Request through theRequestFactorywith the mock as owner / puller / consumer and a 90-day deadline, attached to the mock. Only the local artifact was re-stamped; the grunt / whitelist pins are untouched.3. Dependency updates (separate commit, easy to drop):
bun updateper workspaceTaggedErrorsubclasses drop the trailing();matchError/isTaggedError/tryPromiseunchanged. The error classes are part of@3flabs/guardian's public surface, so the changeset bumps itminorand notes that hosts using their ownbetter-resultshould be on 3.x.vi.mockhoisting,sequential,bench, or the removed entry points; unit + integration suites pass.Deliberately left alone:
guardian-test-fixtures),changeset tagis renamed, andchangeset versionnow exits 1 with no pending changesets; all three touchrelease.yml/changesets/action, so that migration deserves its own PR.1.9.1(the coordinator's exact pin exists to dedupe with them), and the DERSignatureAPI the KMS signer uses was renamed.Testing
bun run test(355),test:integration(40, real anvil),typecheck,lint,format:check,build,fixtures:checkall pass.New coverage:
false(a pass can only come from the retargetter path); attached-to-another-request and idle (address(0)) failures with their reasons; below / at the 80-day floor; custom buffer including0; signature deadline still enforced; owner not on the set stays classic; per-chain scoping + case-insensitivity on both the set and the attached address; deterministicoperation()failure → 503 and no cache entry; transport failure → 503; never writes the cache; bypasses a stale classic entry; builder rejects a fractional / negative buffer.operation()when it is, fails when the buffer exceeds the fixture's 90-day runway, and fails once the mock is re-attached to another Request.CI fix (unrelated to the feature)
The first run failed at
Install Foundrybefore any repo step: foundryup is now a compiled binary and the pinnedfoundry-toolchainv1.8.0 still runs it throughbash(foundry-rs/foundry-toolchain#170). The last commit bumps the action pin to v1.9.1; it would have hit any PR opened againstmaintoday.