Skip to content

feat(proxy): switch FWSS to the upstream ERC-8167 dispatcher atomically - #616

Open
CodeWarriorr wants to merge 63 commits into
refactor/fwss-modular-dispatchfrom
erc-8167-proxy
Open

CodeWarriorr wants to merge 63 commits into
refactor/fwss-modular-dispatchfrom
erc-8167-proxy

Conversation

@CodeWarriorr

@CodeWarriorr CodeWarriorr commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator

Moves the FWSS proxy from the UUPS monolith to the unmodified upstream ERC-8167 dispatcher, Proxy.evm, in one delayed upgrade call. Configured selectors route to their modules, and josuke deploys the modules and the migration that installs their routes. Part of #614, supersedes #615. Business modules come separately; don't run the transition on Calibration or Mainnet before they land.

Why this approach. The dispatcher is upstream bytecode pinned by hash, with no FWSS code and no admin selectors. Live v1.4.0 can only change its implementation through its own delayed UUPS upgrade, and it rejects the raw dispatcher, which is 88 bytes with no proxiableUUID. So a one-shot transition implementation carries josuke's migration through that upgrade. The normal upgrade call installs the dispatcher atomically; an empty-data upgrade can be completed or aborted by the owner. The monolith keeps no transition code and has 1590 bytes of size headroom.

What's changed

  • Transition. ERC8167Transition is a service-agnostic one-shot UUPS implementation. It pins the previous implementation, the dispatcher, the migration and the migration's code hash as immutables, so the announced delay covers exactly what will run. migrate(migration) points the proxy at the dispatcher first, then runs the migration in the proxy's storage, then checks routes. A service supplies authorization and the route check. FWSSDispatcherTransition adds the FWSS owner check, LibUpgradeRoutes, the pinned dispatcher code hash and padding.
  • Padding. v1.4.0's announceUpgradePlan requires over 3000 bytes of code; distinct string chunks, returned by legacyUpgradePadding(), bring the transition to 3276 bytes. Repeated bytes get folded by the optimizer, and bytes appended at deployment would break source verification. A test pins the floor, and the transition tests announce it on the deployed v1.4.0 bytecode.
  • Flow. v1.4.0 runs upgradeToAndCall(transition, migrate(migration)). Any revert leaves v1.4.0 and its plan in place. The ordinary migrate(viewContract) upgrade data reverts, so the regular execute script can't leave the transition installed. After an upgrade with empty data, the owner can still finish through the proxy, or call abortTransition() to restore the pinned previous implementation; that rollback is the only way out if the pinned migration can no longer complete.
  • Modules. FWSSMigrateModule runs later josuke migrations with the same owner check and delay, and rejects one that drops implementation, selectors, announceMigration or migrate, or replaces the dispatcher. OwnershipModule exposes the owner. FWSSViewContractModule returns the view contract address. ExtsloadModule keeps the deployed StateView working.
  • Owner checks. FWSSOwnable replaces LibAccessControl: an abstract contract with a shared onlyOwner and internal helpers, so facets get no extra selectors.
  • josuke. One josuke.json with an entry per network. Offline tests resolve facetSrc, reject duplicate selectors and install the facet set. make install-josuke installs the pinned CLI.
  • Tools. warm-storage-deploy-dispatcher-transition.sh reads josuke's proposed migration from josuke.json, reads the current implementation as the rollback target, deploys the dispatcher if needed and the transition, and supports DRY_RUN. warm-storage-execute-upgrade.sh detects the transition, checks it against the ledger and the proxy's current implementation, and sends migrate(migration). Its read-only calls now run without ETH_KEYSTORE, which cast otherwise tries to unlock, failing silently without a tty; josuke accept records the routes. make install-evm builds the pinned evm and installs it beside forge. tools/README.md documents the sequence.

Compatibility

  • ABI. FilecoinWarmStorageService ABI is unchanged. The FWSSStorage field is _viewContractAddress, so modules don't all export a getter; the monolith and FWSSViewContractModule declare viewContractAddress(). Module and transition ABIs aren't published; the merged proxy ABI comes separately, before this stack reaches main. usdfcTokenAddress() and the other former immutable getters stay unrouted until their business modules land, and StateView getPriceList() waits for them too.
  • Storage. Layout is unchanged; the layout snapshot strips the leading underscore, so the published label and slot constant stay the same. FWSSMigrateModule inherits FWSSStorage and uses its legacy nextUpgrade field. The transition adds no storage.
  • Prerequisite. Improve delegate storage detection wjmelements/josuke#3: behind the ERC-1967 proxy, josuke reads the implementation slot as every selector's route slot, so josuke deploy can't generate the migration yet. Josuke also runs every josuke.json entry against the connected chain with its top-level address.

Testing

The transition tests run against the deployed mainnet v1.4.0 proxy and implementation bytecode, not a rebuild. The scripts ran on an anvil chain with the v1.4.0 bytecode etched at the mainnet addresses and a hand-written josuke ledger: dispatcher and transition deploy, dry run, announcement, and the Safe calldata from the execute script. A real josuke deploy against a v1.4.0 proxy waits on josuke#3.

From service_contracts/: make force-gen && make update-abi, forge fmt --check, forge lint --deny notes --quiet, make build test (929 passed), GITHUB_BASE_REF=refactor/fwss-modular-dispatch make check-layout, make contract-size-check, bash tools/check_deployments_checksums.sh deployments.json.

@CodeWarriorr

Copy link
Copy Markdown
Collaborator Author

The PR mostly waits for #611 to be rebased, and its ready to review

@Filip-L
Filip-L force-pushed the refactor/provider-management-module branch from 99b1769 to aa70045 Compare September 29, 2026 12:42
@Filip-L
Filip-L force-pushed the refactor/provider-management-module branch from af1982b to 31e0d47 Compare September 29, 2026 13:25

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we only ever call this with msg.sender, so best to remove the parameter

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you convert LibAccessControl into a contract, you can share this modifier in the superclass

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done, LibAccessControl became FWSSOwnable

Comment thread service_contracts/src/modules/FWSSViewContractModule.sol Outdated
Comment thread service_contracts/src/lib/LibAccessControl.sol Outdated
Comment thread service_contracts/src/modules/FWSSViewContractModule.sol Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't need separate declarations per network. they can all go in the same file. there's a chainId key in deployments

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(for future proxies we'll have the same address for all networks)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWSS has different addresses on mainnet and calibnet though, and josuke uses the entry's address on every chain. How would you fit both in one file?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trivially. What do you think is the difficulty?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

merging is fine, what worries me is josuke deploy - it runs every entry against whatever chain its connected to, using that entry address. maybe we should embed chain id into the josuke config ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI: rebase rewrote you commit, sorry about that, content is unchanged of just the hash

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

chainId is the key in deployments, and there's no support for initial deployments

@CodeWarriorr CodeWarriorr Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kept both network entries in one josuke.json, as in your commit. The transition scripts select the proposed migration by the existing FWSS proxy address and deployments[chainId]. The body calls out the current deploy limitations; a real josuke deploy against the v1.4.0 proxy remains gated on wjmelements/josuke#3.

Comment thread service_contracts/foundry.toml Outdated
Comment thread service_contracts/foundry.toml Outdated
Comment thread service_contracts/foundry.toml
Comment thread service_contracts/test/helpers/JosukeFacetSet.sol

@wjmelements wjmelements left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks pretty good. As a follow-on task, we'll want to run most of the functional tests pre- and post-dispatcher.

Perhaps the view contract address(self) change can be a separate PR so we can merge this in sooner

@CodeWarriorr

Copy link
Copy Markdown
Collaborator Author

Rebased on #611.
Beyond the threads:

  • FWSS-specific modules now use a short FWSS prefix, like FWSSStorage. Generic ones like OwnershipModule and ExtsloadModule stay unprefixed
  • migrate reverts if a migration swaps the dispatcher

Pre/post-dispatcher tests are a follow-up once the business modules are in.

@wjmelements

Copy link
Copy Markdown
Contributor

Rebased on #611.

Why? I wish you hadn't.

@CodeWarriorr

Copy link
Copy Markdown
Collaborator Author

Rebased on #611.

Why? I wish you hadn't.

sorry, my bad. #611 moved with fixed CI and I wanted to update the PR. because we are doing squash merges to main branches the history doesn't really matter that much for each PR, I'm going to merge base branches from now on.

The monolith stays the implementation during the migration phase, so it
keeps its full v1.4.0 interface. Scripts and synapse-sdk read the getter
from the proxy and from the published ABI.

The storage field becomes _viewContractAddress so a same-named getter can
coexist with it. The layout snapshot drops the leading underscore, so the
published label and VIEW_CONTRACT_ADDRESS_SLOT are unchanged.
FWSSViewContractModule now inherits FWSSStorage instead of reading the slot.
Base automatically changed from refactor/provider-management-module to refactor/fwss-modular-dispatch October 1, 2026 16:07
@wjmelements

Copy link
Copy Markdown
Contributor

resolve conflicts

Comment thread service_contracts/Makefile Outdated
Comment thread service_contracts/Makefile Outdated
Comment thread service_contracts/Makefile
Comment thread service_contracts/Makefile Outdated
Comment thread service_contracts/foundry.toml Outdated
Squash-merged #611 restored the pre-rename provider module, its test and
LibAccessControl; drop them in favour of FWSSProviderManagementModule and
FWSSOwnable, which differ only in formatting and the owner base. Keep the
base's commented ignored_error_codes and track FWSSMetadataModule's size.
…orge

Exporting PATH inside make only reached make's own recipes, so josuke and
shells never found the assembler. Installing it next to forge puts it on
the PATH every Foundry user already has.
The FWSS monolith has 193 bytes of size headroom left, so it cannot keep
carrying the one-shot dispatcher switch. ERC8167Transition holds only the
UUPS entry point and the pinned dispatcher and migration; services supply
authorization and the route check. FWSSDispatcherTransition pads itself
past v1.4.0's 3000-byte announcement floor with distinct string chunks,
which stay verifiable from source, unlike bytes appended at deployment.
The transition flows now upgrade the deployed v1.4.0 bytecode to the slim
transition with migrate(migration). Re-entry reaches the dispatcher, since
the transition uninstalls itself first, and a migration that replaces the
dispatcher reverts. Tests of the monolith's own transition code stay until
that code is removed.
FilecoinWarmStorageService goes back to the base implementation plus the
_viewContractAddress rename: no transition constructor arguments,
immutables or completeDispatcherTransition, and its ABI matches the base.
FWSSDispatcherTransition carries the switch instead.

The merge of #611 had kept this branch's deletion of addApprovedProvider
and removeApprovedProvider; restore them with the base tests and deploy
scripts, since the monolith stays complete during the migration.

ERC8167Transition leaves src/lib/, whose events update-abi merges into
the FWSS ABI.
josuke now generates and records every FWSS module migration, so its
revision is pinned like the evm assembler's.
The transition pins the migration josuke deploy proposes in josuke.json.
The new deploy script reads it from there, deploys the dispatcher if
needed and records FWSSDispatcherTransition in deployments.json. The
execute script detects the transition, checks its migration and code
hash against the ledger, and sends migrate(migration); josuke accept
then records the routes.
An upgrade to the transition without migrate data, followed by a migration
that cannot complete, would leave the proxy with no way back. The transition
now pins the implementation it replaces and abortTransition restores it.
The deploy script reads the current implementation for the transition's
rollback target, deploys nothing under DRY_RUN and records checksummed
addresses. The execute script checks the pinned previous implementation
against the proxy before migrating.
cast unlocks ETH_KEYSTORE even for read-only calls, so with the keystore
exported each read prompted for a password, or failed without a tty and
was hidden by the stderr redirect. The plan check then reported an empty
planned address.
@CodeWarriorr
CodeWarriorr marked this pull request as ready for review October 2, 2026 12:11
@CodeWarriorr
CodeWarriorr requested a review from Kubuxu as a code owner October 2, 2026 12:11

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deploy using forge scripts, not bash scripts

Comment on lines +18 to +21
# cast unlocks ETH_KEYSTORE even for read-only calls, which prompts for a password or fails without a tty.
cast_call() {
env -u ETH_KEYSTORE cast call "$@"
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the -f parameter used in every invocation here also prevents the password prompt

JOSUKE_LEDGER="${JOSUKE_LEDGER:-$(dirname "${BASH_SOURCE[0]}")/../josuke.json}"

# Prints the EIP-55 migration `josuke deploy` proposed for a proxy on a chain, or nothing if there is none
# Args: $1=chain_id, $2=proxy_address

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

drop the first param and use $CHAIN

Comment on lines +33 to +37
* @notice Leaves the contract without an owner, disabling owner-only functions and migrations
*/
function renounceOwnership() external onlyOwner {
_transferOwnership(address(0));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's remove this function. it's dangerous. If we want to abdicate we would uninstall the onlyOwner methods instead

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: ✔️ Approved by reviewer

Development

Successfully merging this pull request may close these issues.

4 participants