feat(registry): route-keyed registry, with owner() and the governance Safe verified on chain - #18
Merged
Merged
Conversation
From v1.3 a chain carries one splitter per protocol route, because the fee split is immutable at construction and the two protocols need different economics. The registry was keyed by chain alone and could not express that. Keys are now '<chain>:<route>' and every entry names both explicitly, with the generator rejecting any entry whose key and fields disagree. Why the key matters more than it looks: the splitters were deployed with CREATE, so the address derives from deployer and nonce and the same address recurs on other chains for the other route. OP's merchant-aifp1 is the same address as Base's agent-x402; Avalanche shares two with Arbitrum; XRPL and BOT share both. An address alone therefore says nothing about which economics apply, and resolving by chain alone, or falling back between routes, would settle at the wrong fee split. Adds the eighteen v1.3 entries, each verified against its own chain: treasury, treasuryBps, ipCreatorBps and runtime code hash read from the contract, and the tuple entrypoint selector found in the deployed bytecode. There are exactly two distinct runtime code hashes across all eighteen — one per route — which is independent evidence the right profile reached every chain, since the bps are immutable and baked into runtime code. settlementEnabled is false on all eighteen. Deployed and verified is not payable: each chain and route is enabled individually, after a successful mainnet paid end-to-end settlement with verified balance deltas. The policy window moves to 90 days from activation, replacing the short window that expires 2026-09-03. Renewal and review are owned by the Web3/CTO side under Governance Safe control. The six v1.1/v1.2 entries are retained as superseded with settlement disabled rather than deleted — historical deployments are evidence. Corrects two errors in the registry's own documentation: v1.3 was described with a flat payNative(bytes32,address,uint256,address,string) signature when the deployed entrypoint takes a tuple, and as "fee on top" when v1.3 is gross-inclusive. Also states plainly that today's agent-x402 route is 0/0 and is NOT fee-on-top; those semantics arrive in a future contract version. verify-registry: 24/24 verified against chain state. generate-sdk-table --check: table matches the registry.
The registry proved what is deployed and where money goes, but not who can change either. The owner controls pause/unpause, the treasury address and the stablecoin whitelist, so every other pinned field was provisional: whoever holds it can rewrite them. owner() is now read from each live splitter alongside treasury and the fee split, and pinned. Every v1.3 route splitter must report the governance Safe 0xFd936f75D9221949f2FEaB54Cd342F7527154eD5; anything else fails. Reporting the right owner is only half the answer, so the Safe itself is read on the same chain: getOwners() must be the exact recorded five and getThreshold() exactly 3 (ADR-006). Compared as an exact shape, never a floor — "threshold >= 2" is what let a 4-of-5 Safe pass silently as 3-of-5 in the production deploy script, and a floor waves through precisely that case. The six legacy v1.1/v1.2 splitters report a deployer EOA, not the Safe. That is history and stays recorded, but ownership and payability are now tied together: any entry not owned by the governance Safe fails if settlementEnabled is true. A single key cannot become a settlement target. The Safe read tries the same RPC list as the splitter and re-checks chain identity on each, so a rate-limited node is a retry rather than a false failure — and all nodes failing is still a failure, never a skip. Verified: 24/24 against chain state, Safe 3-of-5 on all nine chains. Rejected, as it should: owner pinned to an address the contract does not report; threshold lowered in the registry; a signer dropped; a signer swapped; an EOA-owned splitter marked settlement-enabled; the Safe pointed at the old Polygon Safe; the governance block removed. schemaVersion 2 -> 3.
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.
Closes point 1 of the 2026-08-27 review, on top of the original route-keying work.
What was missing
The registry proved what is deployed and where money goes, but not who can change either.
owner()controls pause/unpause, the treasury address and the stablecoin whitelist — so every other pinned field was provisional: whoever holds it can rewrite them.Ownership is now read from the chain
owner()is read from each live splitter alongsidetreasury(),treasuryBps()andipCreatorBps(), and pinned in the registry. Every v1.3 route splitter must report the governance Safe0xFd936f75D9221949f2FEaB54Cd342F7527154eD5; anything else fails.Reporting the right owner is only half the answer, so the Safe itself is read on the same chain:
getOwners()must be the exact recorded five andgetThreshold()exactly 3 (ADR-006).Compared as an exact shape, never a floor.
threshold >= 2is what let a 4-of-5 Safe pass silently as 3-of-5 in the production deploy script, and a floor waves through precisely the case worth catching.Legacy splitters cannot become settlement targets
The six legacy v1.1/v1.2 splitters report a deployer EOA, not the Safe — five of them
0x1D5eF769…directly. That is history and stays recorded, but ownership and payability are now tied together: any entry not owned by the governance Safe fails ifsettlementEnabledis true. A single key cannot become a payout target.Reliability
The Safe read tries the same RPC list as the splitter and re-checks chain identity on each, so a rate-limited node is a retry rather than a false failure. All nodes failing is still a failure, never a skip.
Result
Rejected, as it should be
Each of these was run against the live chains and exits non-zero:
ownerpinned to an address the contract does not reportsettlementEnabled: trueschemaVersion2 → 3. The generated artifact now carriesownerper route and agovernanceblock, which the SDK consumes in AiFinPay/sdk#40.