feat(beacon): UUPS upgradeable L2 slashing receivers (C-3)#128
Closed
drewstone wants to merge 1 commit into
Closed
feat(beacon): UUPS upgradeable L2 slashing receivers (C-3)#128drewstone wants to merge 1 commit into
drewstone wants to merge 1 commit into
Conversation
… v0.15.0
Round 4 audit fix C-3. The five L2-side receivers that accept cross-chain
slashing and bridge messages were immutable, so any post-mainnet bug
required redeploying the receiver and re-authorising every sender on every
bridge. They are now UUPS upgradeable, with state moved to ERC-7201
namespaced slots and `_authorizeUpgrade` gated on the existing owner role.
Contracts converted:
- src/beacon/L2SlashingReceiver.sol (`tangle.beacon.L2SlashingReceiver`)
- src/beacon/bridges/{Arbitrum,Base,Hyperlane,LayerZero}*Receiver
(`tangle.beacon.bridges.<name>`)
Each contract:
- Inherits Initializable + UUPSUpgradeable + OwnableUpgradeable.
- Disables initializers on the implementation (`_disableInitializers()`
in the constructor).
- Holds all mutable state in a single namespaced struct + 50-slot
`__gap` for forward compatibility.
- Routes upgrade authority through `onlyOwner` (production owner should
be a multisig / timelock; the receiver's H-4 SENDER_ACTIVATION_DELAY
timelock on messenger / slasher swaps remains unchanged).
Init order: ownership is granted before any other initialization step so
the configuration calls that follow (`setMessenger`, `setAuthorizedSender`,
`setTrustedSender`, `setPeer`, ...) are guarded from block one. The
bootstrap exemption that allows `setMessenger`/`setSlasher` to write
immediately when the current value is zero is preserved so deploy scripts
do not deadlock for two days.
Migration: existing deployments must be re-deployed behind a fresh
ERC1967 proxy. The receivers were not yet on mainnet, so there is no
in-place storage migration path to support; this is documented in the
bindings CHANGELOG.
Deploy + tests:
- script/DeployL2Slashing.s.sol now deploys impl + ERC1967 proxy and
encodes `initialize(...)` calldata for all four bridge variants.
- New test/beacon/L2SlashingReceiverUUPS.t.sol covers init-is-one-shot,
impl-cannot-be-initialized, owner-gated upgrade, attacker-rejected
upgrade, state-persistence-across-upgrade, and ERC-7201 slot
placement (vm.load on the namespaced slot).
- All 5 affected existing test files were updated to deploy via proxy
and to expect `OwnableUnauthorizedAccount` instead of "Only owner"
for the receivers.
Bindings + versioning:
- cargo xtask gen-bindings re-run.
- bindings/Cargo.toml + fixtures/Cargo.toml bumped to 0.15.0
(coordinated minor for all four Round 4 PRs).
- CHANGELOG entry added.
9 tasks
Contributor
Author
|
Superseded by #129 — consolidated PR rolling C-3 + F5 + G-02 into a single coordinated bindings v0.15.0 cut. The CI failure here was a SIGTERM at the 25-minute mark on the resource-heavy via-IR |
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
Round 4 audit fix C-3. Convert the immutable L2 slashing receivers to UUPS upgradeable so post-mainnet bug remediation does not require re-deploying every bridge endpoint and re-authorising every sender — an operational nightmare that would also leak slashing windows.
Five contracts are converted:
L2SlashingReceiver(src/beacon/L2SlashingReceiver.sol) — the canonical destination receiver applied to operator stake on the L2.ArbitrumL2Receiver,BaseL2Receiver,HyperlaneReceiver,LayerZeroReceiver.Each contract now:
Initializable + UUPSUpgradeable + OwnableUpgradeable, matching the canonical Tangle UUPS pattern.tangle.beacon.L2SlashingReceiver,tangle.beacon.bridges.*Receiver) plus a 50-slot__gap._disableInitializers()in the constructor).onlyOwner. The owner is the same role gating sender / messenger / slasher mutations, so upgrade authority is no broader than existing admin authority — production owner should be a multisig or timelock.Init order: ownership is granted before any other configuration step so
setMessenger/setAuthorizedSender/setTrustedSender/setPeerare gated from block one. The H-4 timelock on messenger / slasher swaps and the bootstrap exemption that allows the firstsetMessenger/setSlasherto write immediately are preserved.Migration story
The receivers are not yet on mainnet, so this PR explicitly does not carry an in-place storage layout-compatibility shim. Operators redeploying onto a chain that had a previous (non-UUPS) receiver must:
ERC1967Proxywithinitialize(slasher, address(0), owner).setChainConfigto point at the new proxy.This is documented in
bindings/CHANGELOG.md.Per-contract changes
L2SlashingReceiverinitialize(address slasher, address messenger, address owner)ArbitrumL2Receiverinitialize(address l1Sender, address receiver, uint256 sourceChainId, address owner)BaseL2Receiverinitialize(address l2Messenger, address l1Sender, address receiver, uint256 sourceChainId, address owner)HyperlaneReceiverinitialize(address mailbox, address receiver, address owner)LayerZeroReceiverinitialize(address endpoint, address receiver, address owner)Owner-gated functions revert with
OwnableUnauthorizedAccount(account)instead of"Only owner".transferOwnership(address(0))reverts withOwnableInvalidOwner.Deploy script (
script/DeployL2Slashing.s.sol) now deploys impl +ERC1967Proxyand encodesinitialize(...)calldata for all four bridge variants. The deployer is wired as initial owner so the post-deploy configuration succeeds, then ownership is transferred toadmin.Test coverage
test/beacon/L2SlashingReceiverUUPS.t.sol(new, 18 tests):OwnableUnauthorizedAccount.L2SlashingReceiver,HyperlaneReceiver,LayerZeroReceiver.vm.loadon the namespaced slot forL2SlashingReceiverconfirmsslasherandmessengerland at slot+0 / slot+1, and slot 0 is empty.ZeroAddress()reverts on init with zero owner.CrossChainMessengersTest,CrossChainSlashingTest,L2SlashingSilentDedupTest,DeploymentScriptsTest) were updated to deploy viaERC1967Proxyand to expect theOwnableUnauthorizedAccounterror for owner-only paths.Verified locally
test/beacon/L2SlashingReceiverUUPS.t.soltest/beacon/bridges/CrossChainMessengersTest.t.soltest/beacon/CrossChainSlashingTest.t.soltest/security/L2SlashingSilentDedupTest.t.soltest/scripts/DeploymentScriptsTest.t.solBindings + versioning
cargo xtask gen-bindingsre-run;bindings/abi/*.jsonreflect current artefact ordering.bindings/Cargo.toml+fixtures/Cargo.tomlbumped to 0.15.0 — the coordinated minor for all four Round 4 PRs.bindings/CHANGELOG.mdcarries a BREAKING entry describing the receiver init-signature change and migration.ITangle*andMultiAssetDelegation), so no Rust API surface changes; the version bump is for parity with the protocol bump.Test plan
FOUNDRY_PROFILE=local_build forge buildclean.cargo build --release -p tnt-core-bindingsclean at v0.15.0.forge testfull suite (running locally; broader suite does not reference these contracts so a green directly-affected slice is the main signal).