chore(deploy): base-sepolia readiness — 2-phase bootstrap + config cleanup#150
Merged
Conversation
Stranded from #147 — the Cargo.toml bumps landed but the corresponding Cargo.lock regen was never committed. No transitive dependency changes, just the two workspace member versions.
`TanglePaymentsFacet`'s runtime bytecode reached 24874 bytes (298 over the 24576-byte EIP-170 limit) after PR #147 added `withdrawRemainingEscrowTo`. Local anvil runs with `--disable-code-size-limit` so the regression slipped through, but Base Sepolia (and every other production chain) enforces the limit strictly — the facet would not deploy. Extract `withdrawRemainingEscrow{,To}` and the private `_withdrawRemainingEscrow` helper into a new `PaymentsRefund` mixin and route those two selectors via `TanglePaymentsRewardsFacet`, which already owns the rewards/admin/views surface and had 8.8 KB of headroom. The customer-facing funding + billing facet shrinks by 770 bytes and lands 472 bytes under the limit. Sizes (default profile, optimizer on): TanglePaymentsFacet: 24874 → 24104 (-770, headroom 472) TanglePaymentsRewardsFacet: 15751 → 16280 (+529, headroom 8296) TanglePaymentsDistributionFacet: 19651 → 19651 (unchanged) The diamond router wires selectors from each facet's `selectors()` view, so `Tangle.sol`'s public surface is unchanged: `withdrawRemainingEscrow{,To}` still dispatch through the same calldata path, just to a different facet. The `ITangleServices.sol` interface is unaffected. Tests: - test/tangle/Payments.t.sol::WithdrawRemainingEscrowTo (3/3 pass) covers the new routing through TanglePaymentsRewardsFacet, the ZeroAddress revert, and the NotServiceOwner gate. - test/EndToEndSubscriptionTest.t.sol (9/9 pass) covers the full lifecycle including post-termination withdrawal. - test/tangle/SubscriptionEscrowInvariant.t.sol (3/3 pass) exercises 3,837 `refundRemaining` handler invocations against the new routing.
`deploy/config/base-sepolia.json` carried 8 stake-asset entries with zero token addresses (stETH, wstETH, EIGEN, USDT, DAI, WBTC, tBTC, lBTC) — not available on Base Sepolia. `FullDeploy.s.sol:818` silently skips them, but they clutter the post-deploy manifest and obscure what's actually live. Keep TNT (sentinel), WETH (`0x4200…0006`), USDC (`0x036C…3cF7e`). Other changes: - `roles.revokeBootstrap`: `false` → `true`. Deployer should not retain bootstrap privileges on a "real" testnet validation deployment. - Strip the `_todo_*` documentation keys — they were planning notes, not config the deploy script reads. Roles still point at a single EOA (`0x03A7…45F4`) which is acceptable for sepolia validation but must be split before mainnet. Migration deploy is left enabled so we exercise the full claim infrastructure on sepolia.
A dry-run of `FullDeploy.s.sol` against real Base Sepolia state surfaced a
latent blocker: the script reverts at the first `registerFacet()` call when
`admin != deployer`. `__Base_init` and `MultiAssetDelegation.initialize`
only grant ADMIN_ROLE to the config's `admin` address, never to
`msg.sender` (the deployer). After init, the deployer has zero roles and
cannot register facets, grant cross-contract roles, or call any
admin-gated setup function from its broadcast context.
Local-env (anvil) hides this because `local.anvil.json` sets
`admin: 0x000…000`, which `FullDeploy` substitutes to the deployer at
runtime — so `admin == deployer` and the script accidentally works.
Production chains (mainnet, Base mainnet, Arbitrum, Optimism, Tangle) are
even more affected: `_requireProductionRoles` *enforces* `admin != deployer`,
so the previous code could never deploy to any production chain.
Switch the deploy flow to a 2-phase bootstrap pattern:
1. **Bootstrap phase**: every proxy / contract is initialized with the
*deployer* as the initial DEFAULT_ADMIN_ROLE / ADMIN_ROLE holder. The
deployer can then register facets, grant `MANAGER_ROLE` on the MBSM
registry, enable assets, wire the rewards manager, etc., all from its
own broadcast context.
2. **Handoff phase**: existing `_applyRoleHandoff` already (a) grants
permanent roles to `timelock` + `multisig` and (b) revokes the bootstrap
admin's roles when `revokeBootstrap == true`. We just thread `deployer`
as the `bootstrapAdmin` parameter so the right address gets revoked.
`_assertGovernanceConfiguration` is updated in lockstep.
No protocol contract changes — this is purely a script-side fix. The
`admin` parameter from config now flows only into `_resolveCredits`
(Credits is non-upgradeable Ownable, used as the final owner directly)
and as the fallback handoff target when timelock/multisig are unset.
Verified end-to-end against real Base Sepolia state via:
PRIVATE_KEY=… FULL_DEPLOY_CONFIG=deploy/config/base-sepolia.json \
forge script script/FullDeploy.s.sol:FullDeploy \
--rpc-url https://sepolia.base.org
The regenerated `deployments/base-sepolia/{latest,migration}.json` are
included as evidence — every proxy + facet deployed cleanly, role
handoff to the config admin succeeded, smoke tests passed, and the
deployer ended with zero roles. With the EIP-170 facet split now on
main (PR #149), the script produces a complete, byte-size-clean
deployment that a real broadcast can sign.
Also included on this branch (commit c8159f1):
- Prune `deploy/config/base-sepolia.json`: drop 8 zero-token stake assets
(stETH, wstETH, EIGEN, USDT, DAI, WBTC, tBTC, lBTC), strip stale
`_todo_*` doc keys, flip `revokeBootstrap` to `true`.
Contributor
Author
🔍 Reviewing
|
| Pass | Status | ETA |
|---|---|---|
| Kimi Code K2.6 | Running (14 min) | ~5-15 min |
Agent review running. Reads the actual code. This comment updates in place.
tangletools · #150 · model: kimi-for-coding · started 2026-05-20T12:30:41Z
Asserts every deployable Tangle and staking facet has runtime bytecode under the 24576-byte EIP-170 limit. Catches the exact regression PR #149 had to clean up: `TanglePaymentsFacet` shipped at 24874 bytes because the local stress harness runs anvil with `--disable-code-size-limit`, so the test suite never deployed it on a chain that enforces the limit. The check measures `address(new Facet()).code.length` after constructor under the default profile (optimizer on, via-IR) — i.e. the actual bytecode a real broadcast would deploy. Two test functions (one per diamond family) so a single regression surfaces only the failing family. Current headroom for the tighter facets: TanglePaymentsFacet: 24104 (472 under) TangleServicesFacet: ~21k (~3.5k under, monitor) TanglePaymentsDistributionFacet: 19651 (4925 under) A future PR that grows TanglePaymentsFacet by >472 bytes — or any similar regression — will now fail CI on PR with a precise overage number rather than reverting at deploy time on the first production broadcast.
2 tasks
drewstone
added a commit
that referenced
this pull request
May 20, 2026
The Foundry CI job has been red on every PR since the codebase grew past the ubuntu-latest runner's 16 GB memory budget. `forge build --sizes` gets OOM-killed (exit 143) during the 352-file via-IR compile around the 20-minute mark — same failure on every commit including the now-merged #149 and #150. Until we have access to a larger runner (or split the compile graph), this job is pure noise: it blocks no merge (PRs are merged over red CI as a matter of course) and surfaces no signal a developer can act on. EIP-170 facet sizes are now enforced via `test/FacetSize.t.sol` (added in #150), which is much cheaper to run and produces a precise overage number on regression. Local `forge test` remains the source of truth for correctness. Restore once we have a runner with enough RAM (or a leaner compile), or replace with a sharded matrix that fits each shard inside 16 GB. Co-authored-by: Drew Stone <drewstone329@gmail.com>
4 tasks
drewstone
added a commit
that referenced
this pull request
May 20, 2026
* chore(ci): remove broken Foundry CI The Foundry CI job has been red on every PR since the codebase grew past the ubuntu-latest runner's 16 GB memory budget. `forge build --sizes` gets OOM-killed (exit 143) during the 352-file via-IR compile around the 20-minute mark — same failure on every commit including the now-merged #149 and #150. Until we have access to a larger runner (or split the compile graph), this job is pure noise: it blocks no merge (PRs are merged over red CI as a matter of course) and surfaces no signal a developer can act on. EIP-170 facet sizes are now enforced via `test/FacetSize.t.sol` (added in #150), which is much cheaper to run and produces a precise overage number on regression. Local `forge test` remains the source of truth for correctness. Restore once we have a runner with enough RAM (or a leaner compile), or replace with a sharded matrix that fits each shard inside 16 GB. * chore(deploy): first Base Sepolia deployment (chainId 84532) Live deployment from the bootstrap-fix path landed in #150. Broadcast from `0x2420FFf17c4213A4075cf5f7B6dc33429Aaf22Bb` (the shared testnet deployer in `~/company/devops/secrets/shared-testnet-deployer.env`), total cost 0.0009 ETH on Base Sepolia. Key addresses: Tangle 0xC9b0716a187072be0f38A5D972392C6479b9Cfe3 MultiAssetDelegation 0xfEB417fc6d343e0fc88EC9fDb8294BF84d69F0Ca TNT Token 0x541d93d0650F8E4583B7Db6545cCeee649975831 RewardVaults 0x8AFfb8C215679210329ef9129d8427fA4E7bd087 InflationPool 0x2C44736AaF0EeC7C9b852eE71F0DC05b1606803b ServiceFeeDistributor 0xdA134D167a99fCb9bD5bb977F1F886D9401b1098 TangleMetrics 0xBBc1386b628716241b37e83F99C50A43878c42C2 TangleMigration 0x0f24429FF66B3fF61390888ef7C69F07533D4b01 SP1ZKVerifier 0x609C9Fb4B687D3f172719B13C0012530C003449E Credits 0xEeba50602c52096091cCe56cDB12cA1DF049542A OperatorStatusRegistry 0x81443688Fce1e4eDb822c1D5794C3DAc608e9a23 Role handoff verified on-chain via `cast call hasRole`: - deployer (0x2420…22Bb) → DEFAULT_ADMIN_ROLE = false (revoked) - admin (0x03A7…45F4) → DEFAULT_ADMIN_ROLE = true (granted) 109.25M TNT minted to admin. TNT / WETH / USDC enabled as staking assets. Migration deployed with the configured Merkle root + SP1 verifier (real, not mock). Single-EOA governance for sepolia (admin = treasury = timelock = multisig = 0x03A7…45F4) is acceptable for testnet validation but must be split before any mainnet broadcast. * chore(indexer): point base-sepolia config at first deployment Update indexer/config.yaml with the contract addresses from the live Base Sepolia broadcast that landed earlier in this branch. Populates MasterBlueprintServiceManager and Credits where they were previously empty (those contracts didn't exist in the prior partial deployment). Tangle 0x62281eac… → 0xC9b0716a… MultiAssetDelegation 0x96e682cc… → 0xfEB417fc… OperatorStatusRegistry 0x17746107… → 0x81443688… RewardVaults 0x37979744… → 0x8AFfb8C2… InflationPool 0x8152f13c… → 0x2C44736A… MBSM (none) → 0xf259444a… Credits (none) → 0xEeba5060… ValidatorPodManager / LiquidDelegationFactory / LiquidDelegationVault remain unset — those are separate beacon / liquid-staking deploys not part of the FullDeploy script. Indexer can now `npm run dev` against base-sepolia and start ingesting events at block 0 of the new deployment. --------- Co-authored-by: Drew Stone <drewstone329@gmail.com>
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.
Summary
Two related changes that together unblock real Base Sepolia (and mainnet) broadcasts of
FullDeploy.s.sol. Verified end-to-end via a forge dry-run against live Base Sepolia state.1. 2-phase bootstrap pattern in
FullDeploy.s.sol/Deploy.s.solA dry-run against real Base Sepolia surfaced a latent blocker: the script reverts at the first
registerFacet()call wheneveradmin != deployer.__Base_initandMultiAssetDelegation.initializeonly grantADMIN_ROLEto the config'sadminaddress — never tomsg.sender. After init the deployer has zero roles, so it cannot register facets, grant cross-contract roles, or call any admin-gated setup function from its broadcast context.Local-env (anvil) hides this because
local.anvil.jsonsetsadmin: 0x000…000, whichFullDeploysubstitutes to the deployer at runtime — soadmin == deployercoincidentally. Production chains are even more affected:_requireProductionRolesenforcesadmin != deployeron mainnet (chainId 1), Base mainnet (8453), Tangle mainnet (5845), Arbitrum (42161), Optimism (10). As written, the script could never deploy to any of them.Fix: thread
deployeras the initial admin everywhere in the deploy phase:admin(config) holdsADMIN_ROLEetc., but no key broadcasts as itdeployerholdsADMIN_ROLEso it canregisterFacet,grantRole,enableAsset, etc._applyRoleHandoff)timelock+multisig, revokes fromadmin(which never held them at runtime)timelock+multisig, revokes the deployer's bootstrap roles_assertGovernanceConfigurationis updated in lockstep to assert the deployer (not admin) ends with zero roles.No protocol contract changes. The fix is purely in
script/Deploy.s.sol::_deployCoreandscript/FullDeploy.s.sol(the call sites passingadmin→ nowdeployerto_prepareIncentives,_deployServiceFeeDistributorProxy,_deployStreamingPaymentManagerProxy,_applyRoleHandoff,_assertGovernanceConfiguration).2.
deploy/config/base-sepolia.jsoncleanup (commit c8159f1)FullDeploy.s.sol:818silently skips them, but they clutter the post-deploy manifest. Keep TNT (sentinel), WETH (0x4200…0006), USDC (0x036C…3cF7e).roles.revokeBootstrap:false→true._todo_*documentation keys.Roles still point at a single EOA (
0x03A7…45F4) which is acceptable for sepolia validation but must be split before mainnet.Evidence
Regenerated
deployments/base-sepolia/{latest,migration}.jsonfrom a forge dry-run againsthttps://sepolia.base.org:Every proxy + facet deployed cleanly, role handoff to the config admin succeeded, smoke tests passed, deployer ended with zero roles. With the EIP-170 facet split now on
main(#149), the produced manifest is a complete, byte-size-clean deployment ready for an actual broadcast.Test plan
What this does NOT cover (follow-ups for mainnet)
timelock+multisigcontract addresses in a mainnet config (sepolia uses single EOA)--disable-code-size-limitmasked the EIP-170 regression fix(payments): split refund out of TanglePaymentsFacet to clear EIP-170 #149 fixed; same trap will recur)requireAdapters: true