Skip to content

feat(registry): route-keyed registry, with owner() and the governance Safe verified on chain - #18

Merged
daochild merged 3 commits into
mainfrom
feat/registry-route-keyed
Aug 27, 2026
Merged

feat(registry): route-keyed registry, with owner() and the governance Safe verified on chain#18
daochild merged 3 commits into
mainfrom
feat/registry-route-keyed

Conversation

@syedhassan-aifinpay

@syedhassan-aifinpay syedhassan-aifinpay commented Aug 27, 2026

Copy link
Copy Markdown
Member

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 alongside treasury(), treasuryBps() and ipCreatorBps(), and pinned in the registry. 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 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 if settlementEnabled is 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

24/24 verified against chain state.
Governance Safe 0xFd936f75D9221949f2FEaB54Cd342F7527154eD5 verified 3-of-5 on 9 chain(s).

Rejected, as it should be

Each of these was run against the live chains and exits non-zero:

Tampered registry Result
owner pinned to an address the contract does not report rejected
governance threshold lowered to 2 rejected — Safe threshold is 3, expected exactly 2
one governance signer dropped rejected — Safe has unrecorded owner(s) 0x849930eb…
a signer swapped for another address rejected — missing and unrecorded reported
EOA-owned legacy splitter marked settlementEnabled: true rejected
governance Safe pointed at the old Polygon Safe rejected
governance block removed entirely rejected before any chain read

schemaVersion 2 → 3. The generated artifact now carries owner per route and a governance block, which the SDK consumes in AiFinPay/sdk#40.

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.
syedhassan-aifinpay and others added 2 commits August 28, 2026 00:25
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.
@syedhassan-aifinpay syedhassan-aifinpay changed the title feat(registry): key by chain and route, add the 18 v1.3 splitters feat(registry): route-keyed registry, with owner() and the governance Safe verified on chain Aug 27, 2026
@daochild
daochild merged commit c6f242a into main Aug 27, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants