From cbb2c843be136c157d0015e3cc4e0e359e669bd4 Mon Sep 17 00:00:00 2001 From: Drew Stone Date: Tue, 5 May 2026 06:09:50 -0600 Subject: [PATCH 1/2] docs(protocol): slashing, auth surface, upgrade discipline Three new pages under /developers: - slashing.mdx: lifecycle (Pending/Disputed/Executed/Cancelled), authorization table with preconditions, configuration parameters with bounds and defaults, operator runbook (received-a-slash, leave-the-network), SLASH_ADMIN runbook (review-a-dispute, detect-stuck-state), dispute economics including bond fallback semantics, per-asset commitment slashing, and the future-evolution path through UUPS upgrade. - auth-surface.mdx: every privileged function in Tangle, MultiAssetDelegation (split across StakingAdminFacet/StakingAssetsFacet/StakingSlashingFacet/ StakingOperatorsFacet), MBSMRegistry, the beacon stack (ValidatorPodManager/L2SlashingReceiver/L2SlashingConnector), and governance (TangleTimelock/TangleGovernor/TangleToken). Role registry clarifies that Tangle and MAD do not share role constants and that MAD has neither UPGRADER_ROLE nor PAUSER_ROLE; both flow through ADMIN_ROLE. - upgrade-discipline.mdx: per-contract _authorizeUpgrade gate (UPGRADER_ROLE on most peripherals, ADMIN_ROLE on MAD, onlyGovernance on TangleGovernor, onlySelf on TangleTimelock), storage gap rules with discipline checklist, the OZ ERC-7201 namespaced slot used for TangleTimelock._minDelay (0x9a37c2aa...fb3600 + 1, pinned to OZ Upgradeable 5.1.0), parameter migration playbook, and post-deploy role-renunciation assertions. Code links pin to tangle-network/tnt-core@main. No em dashes; ASCII punctuation only. Headings in title case to match neighboring developer docs. All three pages were cross-checked by parallel audit subagents against the on-chain code and corrected for factual accuracy: dispute bond fallback semantics (bond restored on transfer fail rather than silently lost), instant slash flag is reserved (not exposed through the public proposeSlash entrypoint), MAD's actual role surface (no UPGRADER_ROLE), TangleMetrics added to the upgradeable list. Wired into pages/developers/_meta.ts under "Protocol Integration". --- pages/developers/_meta.ts | 3 + pages/developers/auth-surface.mdx | 264 ++++++++++++++++++++++++ pages/developers/slashing.mdx | 200 ++++++++++++++++++ pages/developers/upgrade-discipline.mdx | 152 ++++++++++++++ 4 files changed, 619 insertions(+) create mode 100644 pages/developers/auth-surface.mdx create mode 100644 pages/developers/slashing.mdx create mode 100644 pages/developers/upgrade-discipline.mdx diff --git a/pages/developers/_meta.ts b/pages/developers/_meta.ts index 8528a07..703795c 100644 --- a/pages/developers/_meta.ts +++ b/pages/developers/_meta.ts @@ -31,6 +31,9 @@ const meta: Meta = { endpoints: "Endpoints", "protocol-architecture": "Protocol Architecture", "system-architecture": "System Architecture", + slashing: "Slashing", + "auth-surface": "Auth Surface", + "upgrade-discipline": "Upgrade Discipline", "-- contribute": { type: "separator", title: "Contribute", diff --git a/pages/developers/auth-surface.mdx b/pages/developers/auth-surface.mdx new file mode 100644 index 0000000..3462184 --- /dev/null +++ b/pages/developers/auth-surface.mdx @@ -0,0 +1,264 @@ +--- +title: Auth Surface +description: Role-by-role permissions across Tangle protocol contracts. +--- + +# Auth Surface + +This page enumerates every privileged function in the protocol, who can call it, and which production address SHOULD hold the role. If you are setting up a new deployment or auditing an existing one, this is the canonical reference. + +For the slashing-specific subset see [Slashing](/developers/slashing). + +## Role Registry + +Roles are defined in [`Base.sol`](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) and granted at initialization. The deploy script ([`script/FullDeploy.s.sol`](https://github.com/tangle-network/tnt-core/blob/main/script/FullDeploy.s.sol)) hands every role to the deployer EOA at first, then transfers them to production multisigs and the timelock through `_applyRoleHandoff`. + +Note: each contract defines its own role set. `Tangle` and `MultiAssetDelegation` (MAD) do NOT share role constants; the same name on both contracts is a different keccak slot. + +| Role | Where | Suggested holder | +|---|---|---| +| `DEFAULT_ADMIN_ROLE` | All AccessControl contracts | `TangleTimelock` | +| `ADMIN_ROLE` | Tangle, MAD, and most peripherals | `TangleTimelock` | +| `PAUSER_ROLE` | Tangle only | Operations multisig | +| `UPGRADER_ROLE` | Tangle, MBSMRegistry, TangleToken, RewardVaults, InflationPool, TangleMetrics, ServiceFeeDistributor, StreamingPaymentManager | `TangleTimelock` | +| `SLASH_ADMIN_ROLE` | Tangle only | Slashing oversight multisig | +| `MANAGER_ROLE` | MBSMRegistry only | `TangleTimelock` | +| `ASSET_MANAGER_ROLE` | MAD only | Operations multisig | +| `SLASHER_ROLE` | MAD only | Tangle (via `addSlasher`) | +| `TANGLE_ROLE` | MAD only | Tangle (via `setTangle`) | +| `MINTER_ROLE` | TangleToken only | `TangleTimelock` | + +`MultiAssetDelegation` does NOT define a `UPGRADER_ROLE`. Its `_authorizeUpgrade` is gated on `ADMIN_ROLE`. Its pause/unpause are gated on `ADMIN_ROLE` (no separate `PAUSER_ROLE` on MAD). + +## Tangle (`src/Tangle.sol`) + +Tangle is the protocol entrypoint, composed of mixins under `src/core/`. Every state-changing path is gated by exactly one of: + +- A specific OZ AccessControl role +- Service or blueprint ownership +- Permissionless gating (open to any caller) + +### Contract Administration + +| Function | Caller | Source | +|---|---|---| +| `pause()` | `PAUSER_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | +| `unpause()` | `PAUSER_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | +| `_authorizeUpgrade(newImpl)` | `UPGRADER_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | +| `setMBSMRegistry(addr)` | `ADMIN_ROLE` (when not paused) | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | +| `setMetricsRecorder(addr)` | `ADMIN_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | +| `setOperatorStatusRegistry(addr)` | `ADMIN_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | +| `setServiceFeeDistributor(addr)` | `ADMIN_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | +| `setPriceOracle(addr)` | `ADMIN_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | +| `setTreasury(addr)` | `ADMIN_ROLE` (NOT `whenNotPaused`) | [Payments.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Payments.sol) | +| `setPaymentSplit(split)` | `ADMIN_ROLE` (NOT `whenNotPaused`) | [Payments.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Payments.sol) | +| `setTntToken(addr)` | `ADMIN_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | +| `setRewardVaults(addr)` | `ADMIN_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | +| `setMaxBlueprintsPerOperator(n)` | `ADMIN_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | +| `setDefaultTntMinExposureBps(bps)` | `ADMIN_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | +| `setTntPaymentDiscountBps(bps)` | `ADMIN_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | +| `setMinServiceTtl(seconds)` | `ADMIN_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | +| `setMaxServiceTtl(seconds)` | `ADMIN_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | +| `setRequestExpiryGracePeriod(seconds)` | `ADMIN_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | +| `setMaxQuoteAge(seconds)` | `ADMIN_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | + +Most admin setters in `Base.sol` are `whenNotPaused`. The two exceptions, both in `Payments.sol`, are `setTreasury` and `setPaymentSplit`. Paused governance can still rewire payment routing; this is intentional so a halted protocol can fix a known-bad treasury before resuming. + +### Slashing + +| Function | Caller | Source | +|---|---|---| +| `proposeSlash(serviceId, op, slashBps, evidence)` | Service owner, blueprint owner, or BSM-declared origin | [Slashing.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Slashing.sol) | +| `disputeSlash(slashId, reason)` `payable` | Slashed operator (must post bond), or `SLASH_ADMIN_ROLE` | [Slashing.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Slashing.sol) | +| `executeSlash(slashId)` | Anyone (gated by `isExecutable`) | [Slashing.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Slashing.sol) | +| `executeSlashBatch(ids)` | Anyone | [Slashing.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Slashing.sol) | +| `cancelSlash(slashId, reason)` | `SLASH_ADMIN_ROLE` | [Slashing.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Slashing.sol) | +| `setSlashConfig(...)` | `ADMIN_ROLE` | [Slashing.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Slashing.sol) | + +See [Slashing](/developers/slashing) for the full lifecycle and runbooks. + +### Blueprints + +| Function | Caller | Source | +|---|---|---| +| `createBlueprint(def)` | Anyone (when not paused), `nonReentrant` | [BlueprintsCreate.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/BlueprintsCreate.sol) | +| `updateBlueprint(id, uri, hash)` | Blueprint owner, `nonReentrant` | [BlueprintsManage.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/BlueprintsManage.sol) | +| `transferBlueprint(id, newOwner)` | Blueprint owner, `nonReentrant` | [BlueprintsManage.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/BlueprintsManage.sol) | +| `deactivateBlueprint(id)` | Blueprint owner, `nonReentrant` | [BlueprintsManage.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/BlueprintsManage.sol) | +| `setJobEventRates(id, idxs, rates)` | Blueprint owner, `nonReentrant` | [BlueprintsManage.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/BlueprintsManage.sol) | +| `setBlueprintResourceRequirements(id, reqs)` | Blueprint owner, `nonReentrant` | [BlueprintsManage.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/BlueprintsManage.sol) | + +### Operators + +| Function | Caller | Source | +|---|---|---| +| `registerOperator(...)` | Anyone with valid stake (when not paused), `nonReentrant` | [Operators.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Operators.sol) | +| `unregisterOperator(id)` | The operator (no active services), `nonReentrant` | [Operators.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Operators.sol) | +| `updateOperatorPreferences(id, key, rpc)` | The operator | [Operators.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Operators.sol) | + +### Services + +| Function | Caller | +|---|---| +| `requestService(...)` | Anyone (when not paused) | +| `approveService(requestId, stakingPercent)` | Operator listed in the request (request not expired) | +| `approveServiceWithCommitments(...)` | Operator listed in the request | +| `approveServiceWithBls(requestId, stakingPercent, blsPubkey)` | Operator listed in the request | +| `approveServiceWithCommitmentsAndBls(...)` | Operator listed in the request | +| `rejectService(requestId)` | Operator listed in the request | +| `expireServiceRequest(requestId)` | Anyone, after grace period (when not activated) | +| `terminateService(serviceId)` | Service owner | +| `forceRemoveOperator(serviceId, operator)` | Blueprint manager only | + +### Payments + +| Function | Caller | +|---|---| +| `fundService(serviceId, amount)` `payable` | Anyone (re-checks manager policy and TTL) | +| `withdrawRemainingEscrow(serviceId)` | Service owner, after termination | +| `billSubscription(serviceId)` | Anyone, gated by interval | +| `billSubscriptionBatch(ids)` | Anyone | +| `claimReward(token)` | The reward beneficiary | +| `claimRewards(tokens)` | The reward beneficiary | + +## MultiAssetDelegation (`src/staking/MultiAssetDelegation.sol`) + +Staking and delegation are a separate proxy with its own role registry. Functions are split across facets: `StakingAdminFacet`, `StakingAssetsFacet`, `StakingOperatorsFacet`, `StakingSlashingFacet`, `StakingDelegationsFacet`, `StakingDepositsFacet`, `StakingViewsFacet`. + +### Upgrade and Global Config (StakingAdminFacet) + +| Function | Caller | +|---|---| +| `_authorizeUpgrade(newImpl)` | `ADMIN_ROLE` (NOT a separate `UPGRADER_ROLE`; SHOULD be held by `TangleTimelock`) | +| `pause()` / `unpause()` | `ADMIN_ROLE` (no separate `PAUSER_ROLE` on MAD) | +| `setTangle(addr)` | `ADMIN_ROLE` -- grants `TANGLE_ROLE` | +| `setOperatorBondToken(token)` | `ADMIN_ROLE` -- locked once any operator exists | +| `addSlasher(addr)` / `removeSlasher(addr)` | `ADMIN_ROLE` -- grants/revokes `SLASHER_ROLE` | +| `setOperatorCommission(bps)` | `ADMIN_ROLE` (queues a 7-day timelocked commission change) | +| `executeCommissionChange()` / `cancelCommissionChange()` | `ADMIN_ROLE` | +| `setDelays(...)` | `ADMIN_ROLE` | +| `setRewardsManager(addr)` / `setServiceFeeDistributor(addr)` | `ADMIN_ROLE` | +| `rescueTokens(token, to, amount)` | `DEFAULT_ADMIN_ROLE` | +| `sweepDust(token, to)` | `ADMIN_ROLE` | +| `resetPendingSlashCount(op, count)` | `ADMIN_ROLE` (recovery only; default reverts unless overridden) | + +### Asset Configuration (StakingAssetsFacet) + +| Function | Caller | +|---|---| +| `enableAsset(asset, config)` | `ASSET_MANAGER_ROLE` | +| `enableAssetWithAdapter(asset, config, adapter)` | `ASSET_MANAGER_ROLE` | +| `disableAsset(asset)` | `ASSET_MANAGER_ROLE` | +| `registerAdapter(token, adapter)` | `ASSET_MANAGER_ROLE` | +| `removeAdapter(token)` | `ASSET_MANAGER_ROLE` | +| `setRequireAdapters(bool)` | `ASSET_MANAGER_ROLE` | +| `startAdapterMigration` / `completeAdapterMigration` / `cancelAdapterMigration` | `ASSET_MANAGER_ROLE` | + +### Slashing Entrypoints (StakingSlashingFacet) + +| Function | Caller | +|---|---| +| `slashForBlueprint(...)` | `SLASHER_ROLE` (Tangle is the canonical slasher) | +| `slashForService(...)` | `SLASHER_ROLE` -- per-asset commitment slashing | +| `slash(...)` | `SLASHER_ROLE` -- native consensus slash | +| `incrementPendingSlash(operator)` | `SLASHER_ROLE` | +| `decrementPendingSlash(operator)` | `SLASHER_ROLE` | + +### Operator Wiring From Tangle (StakingOperatorsFacet) + +| Function | Caller | +|---|---| +| `addBlueprintForOperator(op, id)` | `TANGLE_ROLE` | +| `removeBlueprintForOperator(op, id)` | `TANGLE_ROLE` | + +## MBSMRegistry (`src/MBSMRegistry.sol`) + +| Function | Caller | +|---|---| +| `addVersion(mbsm)` | `MANAGER_ROLE`. Rejects EOA *targets* (`mbsmAddress.code.length != 0`); the caller can be any holder of the role. | +| `pinBlueprint(blueprintId, revision)` | `MANAGER_ROLE` | +| `unpinBlueprint(blueprintId)` | `MANAGER_ROLE` | +| `initiateDeprecation(revision)` | `MANAGER_ROLE` | +| `completeDeprecation(revision)` | `MANAGER_ROLE`, after grace period | +| `queueEmergencyDeprecation(revision)` | `MANAGER_ROLE` | +| `executeEmergencyDeprecation(revision)` | `MANAGER_ROLE`, after `EMERGENCY_DEPRECATION_DELAY` (24h) | +| `cancelEmergencyDeprecation(revision)` | `MANAGER_ROLE` | +| `setDeprecationGracePeriod(seconds)` | `MANAGER_ROLE` | +| `_authorizeUpgrade(newImpl)` | `UPGRADER_ROLE` | + +`MANAGER_ROLE` SHOULD be the timelock. The 24h emergency deprecation delay is the floor on how fast a single role can disable an MBSM revision; combined with the timelock delay on top, a queued malicious deprecation has at least `timelockDelay + 24h` of observability before it lands. + +## Beacon stack + +### `ValidatorPodManager` + +| Function | Caller | +|---|---| +| `createPod()` | Anyone (one pod per address) | +| `recordBeaconChainEthBalanceUpdate(podOwner, sharesDelta)` | Pod (the contract itself) | + +### `L2SlashingReceiver` + +| Function | Caller | +|---|---| +| `receiveMessage(sourceChainId, sender, payload)` | The configured cross-chain messenger only | +| `setAuthorizedSender(chainId, sender, authorized)` | Owner; authorization is timelocked (2 days) | +| `activateAuthorizedSender(chainId, sender)` | Owner, after `SENDER_ACTIVATION_DELAY` | +| `setMessenger(addr)` | Owner | + +### `L2SlashingConnector` + +| Function | Caller | +|---|---| +| `propagateBeaconSlashing(pod, newFactor)` | `slashingOracle` OR `owner` (the `onlySlashingOracle` modifier accepts both) | +| `propagateBeaconSlashingToChain(pod, newFactor, destChain)` | `slashingOracle` or `owner` | +| `batchPropagateBeaconSlashing(...)` | `slashingOracle` or `owner` | +| `setMessenger(addr)` | Owner | +| `setSlashingOracle(addr)` | Owner | +| `setChainConfig(...)` | Owner | +| `setDefaultDestinationChain(chainId)` | Owner | +| `registerPodOperator(pod, operator)` | Owner | +| `batchRegisterPodOperators(...)` | Owner | +| `transferOwnership(newOwner)` | Owner | + +## Governance + +### `TangleTimelock` + +| Function | Caller | +|---|---| +| `schedule(...)` | `PROPOSER_ROLE` (TangleGovernor) | +| `execute(...)` | `EXECUTOR_ROLE` (TangleGovernor or open) | +| `cancel(...)` | `CANCELLER_ROLE` | +| `updateDelay(newDelay)` | The timelock itself only (`onlySelf`), bounded `[1d, 30d]` | +| `_authorizeUpgrade(newImpl)` | The timelock itself (`onlySelf`) | + +### `TangleGovernor` + +| Function | Caller | +|---|---| +| `propose(...)` | Anyone with voting power above `proposalThreshold` | +| `castVote(...)` | Token holders with voting power at the snapshotted timestamp | +| `_authorizeUpgrade(newImpl)` | Through governance proposal only | + +### `TangleToken` + +| Function | Caller | +|---|---| +| `mint(to, amount)` | `MINTER_ROLE` (timelock); subject to `MAX_SUPPLY` | +| `_authorizeUpgrade(newImpl)` | `UPGRADER_ROLE` | +| `clock()` | view, returns `block.timestamp` (ERC-6372) | + +## Recommended Production Grant Matrix + +| Role | Holder | Why | +|---|---|---| +| `DEFAULT_ADMIN_ROLE` | TangleTimelock | Role grants and revokes flow through governance | +| `ADMIN_ROLE` | TangleTimelock | Setters need a delay window for community exit | +| `UPGRADER_ROLE` | TangleTimelock | Upgrades need a delay window | +| `PAUSER_ROLE` | Operations multisig | Fast response to active incidents | +| `SLASH_ADMIN_ROLE` | Slashing oversight multisig | Distinct from operations to prevent conflict of interest | +| `MANAGER_ROLE` (MBSM) | TangleTimelock | MBSM version changes need a delay | +| `MINTER_ROLE` (TangleToken) | TangleTimelock | Inflation flows through governance | + +The deployer EOA MUST hold zero roles after deployment. Verify with the on-chain audit that `_applyRoleHandoff` produced. See [Upgrade Discipline](/developers/upgrade-discipline) for the post-deploy checklist. diff --git a/pages/developers/slashing.mdx b/pages/developers/slashing.mdx new file mode 100644 index 0000000..666f3d6 --- /dev/null +++ b/pages/developers/slashing.mdx @@ -0,0 +1,200 @@ +--- +title: Slashing +description: Lifecycle, dispute economics, and operations runbook for the Tangle slashing system. +--- + +# Slashing + +When an operator misbehaves, the protocol burns or redistributes a fraction of their stake and, proportionally, their delegators' stake. This page covers the on-chain lifecycle, the authorization surface, dispute economics, and runbooks for operators and the slashing admin. + +The slashing path is implemented in: + +- [`src/core/Slashing.sol`](https://github.com/tangle-network/tnt-core/blob/main/src/core/Slashing.sol) -- propose, dispute, execute, cancel +- [`src/libraries/SlashingLib.sol`](https://github.com/tangle-network/tnt-core/blob/main/src/libraries/SlashingLib.sol) -- proposal struct, status state machine, executability rules +- [`src/staking/SlashingManager.sol`](https://github.com/tangle-network/tnt-core/blob/main/src/staking/SlashingManager.sol) -- actual stake reduction (`_slashForBlueprint`, `_slashForService`) + +## Lifecycle + +``` + proposeSlash + | + v + [ Pending ] + / | \ + disputeSlash | isExecutable && executeSlash + | | | + v v v + [ Disputed ] cancelSlash [ Executed ] + | \ | + | \ v + | cancelSlash (refunds bond) [ Cancelled ] + v + isExecutable after disputeDeadline + -> auto-fails, executeSlash succeeds, bond forfeit to treasury +``` + +Each transition has a single responsible caller and a single state effect. + +### `proposeSlash(serviceId, operator, slashBps, evidence)` + +Anyone the protocol accepts as a slasher creates a `SlashProposal`. The proposal: + +1. Validates `slashBps <= maxSlashBps` and caps it. +2. Computes the operator's effective exposure for this service from their per-asset commitments. +3. Increments two counters: `_operatorPendingSlashCount[operator]` (in staking, blocks delegator withdrawals) and `_operatorActiveSlashProposals[operator]` (in Tangle, enforces the per-operator cap). +4. Sets `executeAfter = block.timestamp + disputeWindow`. +5. Calls the blueprint manager's `onUnappliedSlash` hook (best effort, capped gas). + +If the operator already has `maxPendingSlashesPerOperator` pending, the call reverts. Default cap is 32. + +### `disputeSlash(slashId, reason)` `payable` + +The operator (or `SLASH_ADMIN_ROLE`) contests the slash within the dispute window. + +The operator must post `config.disputeBond` in native asset. `SLASH_ADMIN` posts no bond. The dispute snapshots `config.disputeResolutionDeadline` onto the proposal (`disputeDeadline = block.timestamp + deadline`), so a later admin-driven shrink of the live config cannot retroactively shorten the operator's review window. + +### `executeSlash(slashId)` (permissionless) + +Anyone calls this. The proposal's `isExecutable` check accepts: + +- A `Pending` proposal whose `executeAfter + TIMESTAMP_BUFFER` has elapsed +- A `Disputed` proposal whose snapshotted `disputeDeadline` has elapsed + +Execution routes through `_executeSlashOnStaking`: + +- If the operator made explicit per-asset commitments to the service, calls `slashForService` and only burns the committed assets proportionally. +- Otherwise (legacy services with no commitments) falls back to `slashForBlueprint`. + +After execution: pending counters decrement, slash is recorded for metrics, blueprint manager's `onSlash` hook fires (capped gas, best effort), and any dispute bond is forwarded to the treasury. + +### `cancelSlash(slashId, reason)` (`SLASH_ADMIN_ROLE`) + +The admin agrees with a dispute or otherwise wants to drop a proposal. State transitions complete first, then the bond refund executes. CEI ordering plus `nonReentrant` defends against a malicious disputer contract re-entering on the refund. + +## Authorization + +| Function | Caller | +|---|---| +| `proposeSlash` | Service owner, blueprint owner, or BSM-declared `querySlashingOrigin`. Operator must already be in the service (`OperatorNotInService` revert otherwise). `slashBps` must be in `(0, BPS_DENOMINATOR]` and produce non-zero effective bps after exposure scaling. | +| `disputeSlash` | The slashed operator (must post bond), or `SLASH_ADMIN_ROLE` (no bond) | +| `executeSlash` | Anyone, gated by `isExecutable` | +| `executeSlashBatch` | Anyone. Non-executable ids in the batch are SILENTLY SKIPPED, not reverted. | +| `cancelSlash` | `SLASH_ADMIN_ROLE` only. Can cancel from `Pending` or `Disputed`. | +| `setSlashConfig` | `ADMIN_ROLE` (route through `TangleTimelock` in production) | + +The BSM-declared slashing origin is queried dynamically on each `proposeSlash` call. Operators MUST audit the BSM's code AND its upgradeability before joining any blueprint that uses one. A BSM whose owner is compromised can grant slashing rights to an attacker. If you do not accept that risk, only join blueprints whose BSM is non-upgradeable. + +See [Auth Surface](/developers/auth-surface) for the full role-by-role table across all protocol contracts. + +## Configuration Parameters + +Set with `setSlashConfig(disputeWindow, instantSlashEnabled, maxSlashBps, disputeResolutionDeadline, disputeBond, maxPendingSlashesPerOperator)`. All parameters are admin-tunable and SHOULD be timelocked in production. + +| Parameter | Default | Bounds | Purpose | +|---|---|---|---| +| `disputeWindow` | 7 days | [1 hour, 30 days] | How long after `proposeSlash` the operator has to dispute | +| `instantSlashEnabled` | `false` | bool | Reserved for emergencies. The current public `proposeSlash` hardcodes `instant=false`, so this flag has no effect through the standard API. A future internal entrypoint (or upgrade) would honor it. | +| `maxSlashBps` | 10000 | (0, 10000] | Hard cap on any single slash proposal | +| `disputeResolutionDeadline` | 14 days | [1 day, 60 days] | Time `SLASH_ADMIN` has to resolve a dispute before it auto-fails | +| `disputeBond` | 0 | uint256 | Native-asset bond required from the operator to dispute. Forfeit to treasury on auto-fail or execute, refunded on cancel | +| `maxPendingSlashesPerOperator` | 32 | (0, uint16.max] | Cap on concurrent pending slashes per operator (anti-spam) | + +`disputeBond` defaults to 0 (disabled). On a live network you SHOULD set a bond high enough to deter free self-disputes but low enough that legitimate disputes are not gated by capital. A reasonable starting point is 1% of `minOperatorStake`. + +## Operator Runbook + +### You Received a Slash Proposal + +You will see a `SlashProposed(slashId, serviceId, operator, ...)` event indexed against your operator address. The dispute window has started. + +1. **Read the evidence.** The `evidence` field is typically an IPFS CID. Fetch it. Verify whether the alleged misbehavior happened. +2. **Decide whether to dispute.** + - If the slash is correct, do nothing. It will execute after `disputeWindow + TIMESTAMP_BUFFER`. Your delegators' withdrawals are blocked until then. + - If the slash is incorrect, dispute before `executeAfter`. +3. **To dispute**, call `disputeSlash{value: config.disputeBond}(slashId, "concise reason")`. If you do not post exactly `disputeBond`, the call reverts. +4. **After disputing**, escalate to `SLASH_ADMIN`. Provide your evidence off-chain. The admin has up to `disputeResolutionDeadline` to call `cancelSlash` (refunds your bond) or do nothing (slash auto-executes, bond forfeit). +5. **Watch your `_operatorPendingSlashCount`.** If it stays elevated long after a dispute resolved, raise a ticket with protocol governance to investigate. + +### You Want to Leave the Network + +`_startLeaving` requires BOTH zero pending slashes AND zero active services for the operator (the latter is queried from Tangle via `getOperatorTotalActiveServices`). If either count is non-zero you must resolve first. + +If you were slashed below `minOperatorStake` and your status flipped to `Inactive`, you can still call `_startLeaving` to exit through the standard delay. The remaining stake is returned at `_completeLeaving` time. + +## SLASH_ADMIN Runbook + +`SLASH_ADMIN_ROLE` is the protocol's slashing oversight role. It SHOULD be a multisig distinct from `ADMIN_ROLE` and `UPGRADER_ROLE`. + +### Reviewing a Dispute + +A `SlashDisputed(slashId, disputer, reason)` event fires. You have `disputeResolutionDeadline` (default 14 days) to act. + +1. Read the dispute reason and the original evidence. +2. Off-chain communication with the operator and the slash proposer is expected. +3. **If the dispute is valid**: `cancelSlash(slashId, "summary")`. Bond refunds to the operator, pending counters decrement. +4. **If the dispute is spurious**: do nothing. After the deadline passes, anyone can call `executeSlash`, the slash applies, and the bond is forfeit to the treasury. +5. **To force-resolve early in the spurious case**: call `cancelSlash` only if you mean to drop the proposal entirely. There is no "reject the dispute" path. The auto-execution path is the right answer when the operator is wrong. + +### Detecting Stuck State + +If `_operatorPendingSlashCount[op]` stays elevated and there is no matching `Pending`/`Disputed` proposal, the counter has drifted. Recovery: + +- Investigate whether a slash was cancelled or executed without decrementing the counter (would indicate a protocol bug; file an issue). +- As a last resort, `SlashingManager.resetPendingSlashCount(operator, count)` corrects the counter. Default implementation reverts; an admin override SHOULD be deployed only when a real drift is observed and only by governance. + +## Dispute Economics + +The dispute bond is the only economic counter to free DoS by the operator. Without a bond, an operator slashed for misbehavior could: + +1. Be slashed +2. Self-dispute (free) +3. Lock all their delegators for `disputeResolutionDeadline` +4. Continue extracting value during the window + +With the bond, in the common case: + +- A spurious self-dispute costs the operator `disputeBond`, forfeit to treasury when the dispute auto-fails. +- A legitimate dispute is refunded to the operator on `cancelSlash`. + +Edge cases (handled by `_settleDisputeBond`): + +- If `_treasury == address(0)` when a forfeit would occur, the bond is RESTORED on the proposal (left claimable) instead of being stranded. Configure the treasury before any forfeit-eligible slash auto-fails. +- If the recipient call (`disputer.call` or `treasury.call`) reverts, the bond is RESTORED on the proposal symmetrically. The bond is never silently lost. +- If `proposal.disputer == address(0)` on a refund call (no disputer recorded), the bond falls through to treasury rather than reverting. + +Setting `disputeBond` correctly is a governance call. Too low and DoS is cheap. Too high and operators with legitimate disputes cannot afford to defend themselves. + +The deadline (`disputeResolutionDeadline`) matters because without auto-resolution, a compromised, lost-key, or forgetful `SLASH_ADMIN` would create a permanent lockup vector. The protocol prefers automatic forward progress over indefinite adjudication. + +## Per-Asset Commitment Slashing + +When an operator joins a service via `approveServiceWithCommitments`, they declare per-asset exposure (`AssetSecurityCommitment[]`). On slash: + +- `_executeSlashOnStaking` reads `_serviceSecurityCommitments[serviceId][operator]`. +- If commitments exist, it routes to `slashForService(operator, blueprintId, serviceId, commitments, slashBps, evidence)` which only burns the committed assets proportionally to `commitment.exposureBps`. +- If no commitments exist (legacy services), it falls back to `slashForBlueprint` which burns all enabled assets uniformly. + +This differs from prior behavior, which slashed all enabled assets uniformly. An operator who committed `TNT@5000bps` and `USDC@2000bps` to service A but only `TNT@5000bps` to service B will have only TNT slashed for a service-B violation. + +## Future Evolution + +The `Tangle` contract is UUPS-upgradeable through `TangleTimelock`. The slashing model can evolve without redeployment: + +- **Config tunings** (no upgrade): all six `SlashConfig` fields via `setSlashConfig` +- **Behavior changes** (UUPS upgrade through timelock): + - Operator-specific emergency freeze + - Slash escrow with redistribution to harmed parties + - Migration to UMA optimistic oracle, Kleros, or multi-tier escalation + - Per-blueprint `maxSlashBps` overrides + - Cumulative slash caps per operator per epoch + +Storage gaps are left in `TangleStorage` and `MBSMRegistry` with the discipline documented in [Upgrade Discipline](/developers/upgrade-discipline). Adding fields to `SlashProposal` or `SlashConfig` is non-destructive when appended (the structs are stored in mappings, not packed arrays). + +## Reference + +- Interface: [`ITangleSlashing`](/developers/api/reference/ITangleSlashing) +- Source: [`Slashing.sol`](https://github.com/tangle-network/tnt-core/blob/main/src/core/Slashing.sol) +- Library: [`SlashingLib.sol`](https://github.com/tangle-network/tnt-core/blob/main/src/libraries/SlashingLib.sol) +- Stake reduction: [`SlashingManager.sol`](https://github.com/tangle-network/tnt-core/blob/main/src/staking/SlashingManager.sol) +- Auth surface: [Auth Surface](/developers/auth-surface) +- Upgrade discipline: [Upgrade Discipline](/developers/upgrade-discipline) diff --git a/pages/developers/upgrade-discipline.mdx b/pages/developers/upgrade-discipline.mdx new file mode 100644 index 0000000..bd06b4a --- /dev/null +++ b/pages/developers/upgrade-discipline.mdx @@ -0,0 +1,152 @@ +--- +title: Upgrade Discipline +description: UUPS upgrade rules, storage gap discipline, and parameter migration playbook for Tangle protocol contracts. +--- + +# Upgrade Discipline + +Every upgradeable Tangle contract is UUPS. The role that gates `_authorizeUpgrade` differs by contract: `UPGRADER_ROLE` on most peripherals, `ADMIN_ROLE` on `MultiAssetDelegation`, `onlyGovernance` on `TangleGovernor`, and `onlySelf` on `TangleTimelock`. In production the relevant role SHOULD be held by [`TangleTimelock`](https://github.com/tangle-network/tnt-core/blob/main/src/governance/TangleTimelock.sol) so all upgrades flow through governance with a delay window. This page lists the rules for upgrading the protocol so changes stay safe and reviewable. + +## Upgradeable Contracts + +| Contract | Upgrade gate | Source | +|---|---|---| +| `Tangle` | `UPGRADER_ROLE` | [`src/Tangle.sol`](https://github.com/tangle-network/tnt-core/blob/main/src/Tangle.sol) | +| `MultiAssetDelegation` | `ADMIN_ROLE` (no `UPGRADER_ROLE` on this contract) | [`src/staking/MultiAssetDelegation.sol`](https://github.com/tangle-network/tnt-core/blob/main/src/staking/MultiAssetDelegation.sol) | +| `MBSMRegistry` | `UPGRADER_ROLE` | [`src/MBSMRegistry.sol`](https://github.com/tangle-network/tnt-core/blob/main/src/MBSMRegistry.sol) | +| `TangleGovernor` | `onlyGovernance` (proposal-driven) | [`src/governance/TangleGovernor.sol`](https://github.com/tangle-network/tnt-core/blob/main/src/governance/TangleGovernor.sol) | +| `TangleTimelock` | `onlySelf` (the timelock executing its own queued proposal) | [`src/governance/TangleTimelock.sol`](https://github.com/tangle-network/tnt-core/blob/main/src/governance/TangleTimelock.sol) | +| `TangleToken` | `UPGRADER_ROLE` | [`src/governance/TangleToken.sol`](https://github.com/tangle-network/tnt-core/blob/main/src/governance/TangleToken.sol) | +| `RewardVaults` | `UPGRADER_ROLE` | [`src/rewards/RewardVaults.sol`](https://github.com/tangle-network/tnt-core/blob/main/src/rewards/RewardVaults.sol) | +| `InflationPool` | `UPGRADER_ROLE` | [`src/rewards/InflationPool.sol`](https://github.com/tangle-network/tnt-core/blob/main/src/rewards/InflationPool.sol) | +| `ServiceFeeDistributor` | `UPGRADER_ROLE` | [`src/rewards/ServiceFeeDistributor.sol`](https://github.com/tangle-network/tnt-core/blob/main/src/rewards/ServiceFeeDistributor.sol) | +| `StreamingPaymentManager` | `UPGRADER_ROLE` | [`src/rewards/StreamingPaymentManager.sol`](https://github.com/tangle-network/tnt-core/blob/main/src/rewards/StreamingPaymentManager.sol) | +| `TangleMetrics` | `UPGRADER_ROLE` | [`src/rewards/TangleMetrics.sol`](https://github.com/tangle-network/tnt-core/blob/main/src/rewards/TangleMetrics.sol) | + +## Storage Layout Rules + +Each upgradeable contract reserves a `__gap` array at the end of its storage. New state variables MUST be added between the existing variables and the gap, AND the gap size MUST be reduced by the number of slots consumed. + +### Gap Pattern + +```solidity +// Existing storage above this line is locked. +mapping(uint64 => Service) internal _services; +// ... other vars ... + +// Reserved for future fields. Reduce by exactly N when adding N slots. +uint256[44] private __gap; +``` + +When adding a `mapping` or a single `uint256`, reduce the gap by 1. When adding a struct that occupies 3 slots in storage, reduce by 3. Adding a `uint64` AND a `uint64` AND an `address` that pack into a single slot reduces by 1. Pay attention to packing. + +For a struct stored INSIDE a mapping, appending fields to the struct does NOT consume gap slots in the parent contract (the struct is not inline). Slashing's `SlashProposal` and `SlashConfig` are extended this way. + +### Discipline Checklist (Every Upgrade) + +1. List every new state variable in the upgrade. +2. Determine slot consumption (account for packing). +3. Reduce the relevant `__gap` by exactly that count. +4. Run a storage layout diff against the prior deployment: + ``` + forge inspect Tangle storage-layout > new.json + # diff against the snapshot taken from the previously deployed bytecode + ``` +5. Confirm only appends, never reorders or removes existing variables. + +### When You Would NOT Decrement + +- The deployment is greenfield (first deploy). Gaps are arbitrary on the first slot map; later upgrades inherit them. +- The struct change is to a struct stored in a mapping (the struct is heap-allocated per key, not inline in the parent contract's slot map). + +### When You MUST Decrement + +- Any direct addition of a state variable in the contract or any inherited base. +- An inherited OpenZeppelin contract that uses ERC-7201 namespaced storage (e.g. `AccessControlUpgradeable`, `TimelockControllerUpgradeable`) SHOULD be vetted before upgrading. Such contracts do not consume the parent's gap; they live in their own namespaced slot. tnt-core's own contracts use sequential storage with `__gap` arrays, not ERC-7201 namespacing. + +## Authorization (the Upgrade Itself) + +Each upgradeable contract has an `_authorizeUpgrade(address)` override gated as listed in the table above. Whichever role gates a given contract MUST be held by `TangleTimelock` (or, for `TangleGovernor`/`TangleTimelock` themselves, the contract is structurally self-controlled). An EOA MUST NOT hold any of these roles in production. + +Upgrade flow: + +1. Author writes new implementation `MyContractV2`. +2. Storage layout diff verified clean. +3. Governance proposal targets the proxy: `tangle.upgradeToAndCall(MyContractV2, initData)`. +4. Proposal reaches quorum, queues to timelock, ages out the timelock delay. +5. Anyone executes the timelock operation. +6. Proxy now points at V2. + +The `proxiableUUID()` check on the new implementation prevents pointing the proxy at a non-UUPS contract, which would brick the proxy. + +## TangleTimelock Storage Write + +The timelock's `_minDelay` is in OZ's ERC-7201 namespaced storage at: + +``` +TIMELOCK_CONTROLLER_STORAGE_LOCATION + = 0x9a37c2aa9d186a0969ff8a8267bf4e07e864c2f2768f5040949e28a624fb3600 +``` + +The struct layout is `(_timestamps mapping, _minDelay uint256)`, so `_minDelay` lives at `STORAGE_LOCATION + 1`. This slot is hardcoded in [`TangleTimelock._setMinDelay`](https://github.com/tangle-network/tnt-core/blob/main/src/governance/TangleTimelock.sol). It is pinned to OpenZeppelin Contracts Upgradeable 5.1.0. Before bumping OZ, confirm: + +1. `TimelockControllerStorageLocation` is unchanged. +2. The struct order (mapping first, `_minDelay` second) is unchanged. +3. If either changed, update the constant and run the regression test in [`test/security/TimelockSetMinDelayTest.t.sol`](https://github.com/tangle-network/tnt-core/blob/main/test/security/TimelockSetMinDelayTest.t.sol). + +## Parameter Migrations + +A parameter change that does NOT alter storage layout (e.g. tuning slashing config) goes through the standard governance setter path. No upgrade needed. + +A parameter change that DOES alter storage layout (e.g. moving `_treasury` from a single address to a mapping) requires: + +1. Storage migration plan: how do existing values move? +2. Initializer: post-upgrade `initializeV2` or similar to copy values. +3. Storage gap reduction. +4. Test against a fork: deploy V1, set realistic state, upgrade to V2, verify reads match. + +## Post-Deploy Checklist + +After `FullDeploy` runs, verify on-chain that the deployer EOA holds NO roles: + +```solidity +// On Tangle +assert(!tangle.hasRole(DEFAULT_ADMIN_ROLE, deployer)); +assert(!tangle.hasRole(ADMIN_ROLE, deployer)); +assert(!tangle.hasRole(UPGRADER_ROLE, deployer)); +assert(!tangle.hasRole(PAUSER_ROLE, deployer)); +assert(!tangle.hasRole(SLASH_ADMIN_ROLE, deployer)); + +// On MultiAssetDelegation (note: NO UPGRADER_ROLE on this contract; +// upgrades are gated by ADMIN_ROLE). +assert(!staking.hasRole(DEFAULT_ADMIN_ROLE, deployer)); +assert(!staking.hasRole(ADMIN_ROLE, deployer)); +assert(!staking.hasRole(ASSET_MANAGER_ROLE, deployer)); + +// On MBSMRegistry +assert(!mbsmRegistry.hasRole(DEFAULT_ADMIN_ROLE, deployer)); +assert(!mbsmRegistry.hasRole(MANAGER_ROLE, deployer)); +assert(!mbsmRegistry.hasRole(UPGRADER_ROLE, deployer)); + +// On TangleTimelock +assert(!timelock.hasRole(DEFAULT_ADMIN_ROLE, deployer)); +``` + +The deploy script's `_assertGovernanceConfiguration` runs an equivalent check at the end of `FullDeploy.run()`. Re-run the same assertions from a separate verification script before announcing the deployment. + +See [`script/sh/deploy-mainnet-base-ethereum.sh`](https://github.com/tangle-network/tnt-core/blob/main/script/sh/deploy-mainnet-base-ethereum.sh) for the production launch wrapper. + +## Audit Hooks + +Before any production upgrade: + +1. External firm reviews the diff. +2. Storage layout snapshot is committed to the repo, dated and signed. +3. The proposal calldata is published (e.g. on the governance forum) before voting begins so the community can verify it matches the audited diff. +4. Forks of mainnet state are upgraded and the full integration test suite runs against them. + +## Reference + +- Auth surface: [Auth Surface](/developers/auth-surface) +- Slashing: [Slashing](/developers/slashing) +- Source: [`tangle-network/tnt-core`](https://github.com/tangle-network/tnt-core) From 45a693cfa327418834648883c2872cbc7bc2c8cf Mon Sep 17 00:00:00 2001 From: Drew Stone Date: Tue, 5 May 2026 06:24:29 -0600 Subject: [PATCH 2/2] docs(protocol): apply prettier formatting --- pages/developers/auth-surface.mdx | 326 ++++++++++++------------ pages/developers/slashing.mdx | 32 +-- pages/developers/upgrade-discipline.mdx | 26 +- 3 files changed, 192 insertions(+), 192 deletions(-) diff --git a/pages/developers/auth-surface.mdx b/pages/developers/auth-surface.mdx index 3462184..42c0c85 100644 --- a/pages/developers/auth-surface.mdx +++ b/pages/developers/auth-surface.mdx @@ -15,18 +15,18 @@ Roles are defined in [`Base.sol`](https://github.com/tangle-network/tnt-core/blo Note: each contract defines its own role set. `Tangle` and `MultiAssetDelegation` (MAD) do NOT share role constants; the same name on both contracts is a different keccak slot. -| Role | Where | Suggested holder | -|---|---|---| -| `DEFAULT_ADMIN_ROLE` | All AccessControl contracts | `TangleTimelock` | -| `ADMIN_ROLE` | Tangle, MAD, and most peripherals | `TangleTimelock` | -| `PAUSER_ROLE` | Tangle only | Operations multisig | -| `UPGRADER_ROLE` | Tangle, MBSMRegistry, TangleToken, RewardVaults, InflationPool, TangleMetrics, ServiceFeeDistributor, StreamingPaymentManager | `TangleTimelock` | -| `SLASH_ADMIN_ROLE` | Tangle only | Slashing oversight multisig | -| `MANAGER_ROLE` | MBSMRegistry only | `TangleTimelock` | -| `ASSET_MANAGER_ROLE` | MAD only | Operations multisig | -| `SLASHER_ROLE` | MAD only | Tangle (via `addSlasher`) | -| `TANGLE_ROLE` | MAD only | Tangle (via `setTangle`) | -| `MINTER_ROLE` | TangleToken only | `TangleTimelock` | +| Role | Where | Suggested holder | +| -------------------- | ----------------------------------------------------------------------------------------------------------------------------- | --------------------------- | +| `DEFAULT_ADMIN_ROLE` | All AccessControl contracts | `TangleTimelock` | +| `ADMIN_ROLE` | Tangle, MAD, and most peripherals | `TangleTimelock` | +| `PAUSER_ROLE` | Tangle only | Operations multisig | +| `UPGRADER_ROLE` | Tangle, MBSMRegistry, TangleToken, RewardVaults, InflationPool, TangleMetrics, ServiceFeeDistributor, StreamingPaymentManager | `TangleTimelock` | +| `SLASH_ADMIN_ROLE` | Tangle only | Slashing oversight multisig | +| `MANAGER_ROLE` | MBSMRegistry only | `TangleTimelock` | +| `ASSET_MANAGER_ROLE` | MAD only | Operations multisig | +| `SLASHER_ROLE` | MAD only | Tangle (via `addSlasher`) | +| `TANGLE_ROLE` | MAD only | Tangle (via `setTangle`) | +| `MINTER_ROLE` | TangleToken only | `TangleTimelock` | `MultiAssetDelegation` does NOT define a `UPGRADER_ROLE`. Its `_authorizeUpgrade` is gated on `ADMIN_ROLE`. Its pause/unpause are gated on `ADMIN_ROLE` (no separate `PAUSER_ROLE` on MAD). @@ -40,86 +40,86 @@ Tangle is the protocol entrypoint, composed of mixins under `src/core/`. Every s ### Contract Administration -| Function | Caller | Source | -|---|---|---| -| `pause()` | `PAUSER_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | -| `unpause()` | `PAUSER_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | -| `_authorizeUpgrade(newImpl)` | `UPGRADER_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | -| `setMBSMRegistry(addr)` | `ADMIN_ROLE` (when not paused) | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | -| `setMetricsRecorder(addr)` | `ADMIN_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | -| `setOperatorStatusRegistry(addr)` | `ADMIN_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | -| `setServiceFeeDistributor(addr)` | `ADMIN_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | -| `setPriceOracle(addr)` | `ADMIN_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | -| `setTreasury(addr)` | `ADMIN_ROLE` (NOT `whenNotPaused`) | [Payments.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Payments.sol) | -| `setPaymentSplit(split)` | `ADMIN_ROLE` (NOT `whenNotPaused`) | [Payments.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Payments.sol) | -| `setTntToken(addr)` | `ADMIN_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | -| `setRewardVaults(addr)` | `ADMIN_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | -| `setMaxBlueprintsPerOperator(n)` | `ADMIN_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | -| `setDefaultTntMinExposureBps(bps)` | `ADMIN_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | -| `setTntPaymentDiscountBps(bps)` | `ADMIN_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | -| `setMinServiceTtl(seconds)` | `ADMIN_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | -| `setMaxServiceTtl(seconds)` | `ADMIN_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | -| `setRequestExpiryGracePeriod(seconds)` | `ADMIN_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | -| `setMaxQuoteAge(seconds)` | `ADMIN_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | +| Function | Caller | Source | +| -------------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------ | +| `pause()` | `PAUSER_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | +| `unpause()` | `PAUSER_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | +| `_authorizeUpgrade(newImpl)` | `UPGRADER_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | +| `setMBSMRegistry(addr)` | `ADMIN_ROLE` (when not paused) | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | +| `setMetricsRecorder(addr)` | `ADMIN_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | +| `setOperatorStatusRegistry(addr)` | `ADMIN_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | +| `setServiceFeeDistributor(addr)` | `ADMIN_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | +| `setPriceOracle(addr)` | `ADMIN_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | +| `setTreasury(addr)` | `ADMIN_ROLE` (NOT `whenNotPaused`) | [Payments.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Payments.sol) | +| `setPaymentSplit(split)` | `ADMIN_ROLE` (NOT `whenNotPaused`) | [Payments.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Payments.sol) | +| `setTntToken(addr)` | `ADMIN_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | +| `setRewardVaults(addr)` | `ADMIN_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | +| `setMaxBlueprintsPerOperator(n)` | `ADMIN_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | +| `setDefaultTntMinExposureBps(bps)` | `ADMIN_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | +| `setTntPaymentDiscountBps(bps)` | `ADMIN_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | +| `setMinServiceTtl(seconds)` | `ADMIN_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | +| `setMaxServiceTtl(seconds)` | `ADMIN_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | +| `setRequestExpiryGracePeriod(seconds)` | `ADMIN_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | +| `setMaxQuoteAge(seconds)` | `ADMIN_ROLE` | [Base.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Base.sol) | Most admin setters in `Base.sol` are `whenNotPaused`. The two exceptions, both in `Payments.sol`, are `setTreasury` and `setPaymentSplit`. Paused governance can still rewire payment routing; this is intentional so a halted protocol can fix a known-bad treasury before resuming. ### Slashing -| Function | Caller | Source | -|---|---|---| -| `proposeSlash(serviceId, op, slashBps, evidence)` | Service owner, blueprint owner, or BSM-declared origin | [Slashing.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Slashing.sol) | -| `disputeSlash(slashId, reason)` `payable` | Slashed operator (must post bond), or `SLASH_ADMIN_ROLE` | [Slashing.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Slashing.sol) | -| `executeSlash(slashId)` | Anyone (gated by `isExecutable`) | [Slashing.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Slashing.sol) | -| `executeSlashBatch(ids)` | Anyone | [Slashing.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Slashing.sol) | -| `cancelSlash(slashId, reason)` | `SLASH_ADMIN_ROLE` | [Slashing.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Slashing.sol) | -| `setSlashConfig(...)` | `ADMIN_ROLE` | [Slashing.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Slashing.sol) | +| Function | Caller | Source | +| ------------------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------ | +| `proposeSlash(serviceId, op, slashBps, evidence)` | Service owner, blueprint owner, or BSM-declared origin | [Slashing.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Slashing.sol) | +| `disputeSlash(slashId, reason)` `payable` | Slashed operator (must post bond), or `SLASH_ADMIN_ROLE` | [Slashing.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Slashing.sol) | +| `executeSlash(slashId)` | Anyone (gated by `isExecutable`) | [Slashing.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Slashing.sol) | +| `executeSlashBatch(ids)` | Anyone | [Slashing.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Slashing.sol) | +| `cancelSlash(slashId, reason)` | `SLASH_ADMIN_ROLE` | [Slashing.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Slashing.sol) | +| `setSlashConfig(...)` | `ADMIN_ROLE` | [Slashing.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Slashing.sol) | See [Slashing](/developers/slashing) for the full lifecycle and runbooks. ### Blueprints -| Function | Caller | Source | -|---|---|---| -| `createBlueprint(def)` | Anyone (when not paused), `nonReentrant` | [BlueprintsCreate.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/BlueprintsCreate.sol) | -| `updateBlueprint(id, uri, hash)` | Blueprint owner, `nonReentrant` | [BlueprintsManage.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/BlueprintsManage.sol) | -| `transferBlueprint(id, newOwner)` | Blueprint owner, `nonReentrant` | [BlueprintsManage.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/BlueprintsManage.sol) | -| `deactivateBlueprint(id)` | Blueprint owner, `nonReentrant` | [BlueprintsManage.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/BlueprintsManage.sol) | -| `setJobEventRates(id, idxs, rates)` | Blueprint owner, `nonReentrant` | [BlueprintsManage.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/BlueprintsManage.sol) | -| `setBlueprintResourceRequirements(id, reqs)` | Blueprint owner, `nonReentrant` | [BlueprintsManage.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/BlueprintsManage.sol) | +| Function | Caller | Source | +| -------------------------------------------- | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------- | +| `createBlueprint(def)` | Anyone (when not paused), `nonReentrant` | [BlueprintsCreate.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/BlueprintsCreate.sol) | +| `updateBlueprint(id, uri, hash)` | Blueprint owner, `nonReentrant` | [BlueprintsManage.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/BlueprintsManage.sol) | +| `transferBlueprint(id, newOwner)` | Blueprint owner, `nonReentrant` | [BlueprintsManage.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/BlueprintsManage.sol) | +| `deactivateBlueprint(id)` | Blueprint owner, `nonReentrant` | [BlueprintsManage.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/BlueprintsManage.sol) | +| `setJobEventRates(id, idxs, rates)` | Blueprint owner, `nonReentrant` | [BlueprintsManage.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/BlueprintsManage.sol) | +| `setBlueprintResourceRequirements(id, reqs)` | Blueprint owner, `nonReentrant` | [BlueprintsManage.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/BlueprintsManage.sol) | ### Operators -| Function | Caller | Source | -|---|---|---| -| `registerOperator(...)` | Anyone with valid stake (when not paused), `nonReentrant` | [Operators.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Operators.sol) | -| `unregisterOperator(id)` | The operator (no active services), `nonReentrant` | [Operators.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Operators.sol) | -| `updateOperatorPreferences(id, key, rpc)` | The operator | [Operators.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Operators.sol) | +| Function | Caller | Source | +| ----------------------------------------- | --------------------------------------------------------- | -------------------------------------------------------------------------------------------- | +| `registerOperator(...)` | Anyone with valid stake (when not paused), `nonReentrant` | [Operators.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Operators.sol) | +| `unregisterOperator(id)` | The operator (no active services), `nonReentrant` | [Operators.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Operators.sol) | +| `updateOperatorPreferences(id, key, rpc)` | The operator | [Operators.sol](https://github.com/tangle-network/tnt-core/blob/main/src/core/Operators.sol) | ### Services -| Function | Caller | -|---|---| -| `requestService(...)` | Anyone (when not paused) | -| `approveService(requestId, stakingPercent)` | Operator listed in the request (request not expired) | -| `approveServiceWithCommitments(...)` | Operator listed in the request | -| `approveServiceWithBls(requestId, stakingPercent, blsPubkey)` | Operator listed in the request | -| `approveServiceWithCommitmentsAndBls(...)` | Operator listed in the request | -| `rejectService(requestId)` | Operator listed in the request | -| `expireServiceRequest(requestId)` | Anyone, after grace period (when not activated) | -| `terminateService(serviceId)` | Service owner | -| `forceRemoveOperator(serviceId, operator)` | Blueprint manager only | +| Function | Caller | +| ------------------------------------------------------------- | ---------------------------------------------------- | +| `requestService(...)` | Anyone (when not paused) | +| `approveService(requestId, stakingPercent)` | Operator listed in the request (request not expired) | +| `approveServiceWithCommitments(...)` | Operator listed in the request | +| `approveServiceWithBls(requestId, stakingPercent, blsPubkey)` | Operator listed in the request | +| `approveServiceWithCommitmentsAndBls(...)` | Operator listed in the request | +| `rejectService(requestId)` | Operator listed in the request | +| `expireServiceRequest(requestId)` | Anyone, after grace period (when not activated) | +| `terminateService(serviceId)` | Service owner | +| `forceRemoveOperator(serviceId, operator)` | Blueprint manager only | ### Payments -| Function | Caller | -|---|---| +| Function | Caller | +| ------------------------------------------ | ----------------------------------------- | | `fundService(serviceId, amount)` `payable` | Anyone (re-checks manager policy and TTL) | -| `withdrawRemainingEscrow(serviceId)` | Service owner, after termination | -| `billSubscription(serviceId)` | Anyone, gated by interval | -| `billSubscriptionBatch(ids)` | Anyone | -| `claimReward(token)` | The reward beneficiary | -| `claimRewards(tokens)` | The reward beneficiary | +| `withdrawRemainingEscrow(serviceId)` | Service owner, after termination | +| `billSubscription(serviceId)` | Anyone, gated by interval | +| `billSubscriptionBatch(ids)` | Anyone | +| `claimReward(token)` | The reward beneficiary | +| `claimRewards(tokens)` | The reward beneficiary | ## MultiAssetDelegation (`src/staking/MultiAssetDelegation.sol`) @@ -127,64 +127,64 @@ Staking and delegation are a separate proxy with its own role registry. Function ### Upgrade and Global Config (StakingAdminFacet) -| Function | Caller | -|---|---| -| `_authorizeUpgrade(newImpl)` | `ADMIN_ROLE` (NOT a separate `UPGRADER_ROLE`; SHOULD be held by `TangleTimelock`) | -| `pause()` / `unpause()` | `ADMIN_ROLE` (no separate `PAUSER_ROLE` on MAD) | -| `setTangle(addr)` | `ADMIN_ROLE` -- grants `TANGLE_ROLE` | -| `setOperatorBondToken(token)` | `ADMIN_ROLE` -- locked once any operator exists | -| `addSlasher(addr)` / `removeSlasher(addr)` | `ADMIN_ROLE` -- grants/revokes `SLASHER_ROLE` | -| `setOperatorCommission(bps)` | `ADMIN_ROLE` (queues a 7-day timelocked commission change) | -| `executeCommissionChange()` / `cancelCommissionChange()` | `ADMIN_ROLE` | -| `setDelays(...)` | `ADMIN_ROLE` | -| `setRewardsManager(addr)` / `setServiceFeeDistributor(addr)` | `ADMIN_ROLE` | -| `rescueTokens(token, to, amount)` | `DEFAULT_ADMIN_ROLE` | -| `sweepDust(token, to)` | `ADMIN_ROLE` | -| `resetPendingSlashCount(op, count)` | `ADMIN_ROLE` (recovery only; default reverts unless overridden) | +| Function | Caller | +| ------------------------------------------------------------ | --------------------------------------------------------------------------------- | +| `_authorizeUpgrade(newImpl)` | `ADMIN_ROLE` (NOT a separate `UPGRADER_ROLE`; SHOULD be held by `TangleTimelock`) | +| `pause()` / `unpause()` | `ADMIN_ROLE` (no separate `PAUSER_ROLE` on MAD) | +| `setTangle(addr)` | `ADMIN_ROLE` -- grants `TANGLE_ROLE` | +| `setOperatorBondToken(token)` | `ADMIN_ROLE` -- locked once any operator exists | +| `addSlasher(addr)` / `removeSlasher(addr)` | `ADMIN_ROLE` -- grants/revokes `SLASHER_ROLE` | +| `setOperatorCommission(bps)` | `ADMIN_ROLE` (queues a 7-day timelocked commission change) | +| `executeCommissionChange()` / `cancelCommissionChange()` | `ADMIN_ROLE` | +| `setDelays(...)` | `ADMIN_ROLE` | +| `setRewardsManager(addr)` / `setServiceFeeDistributor(addr)` | `ADMIN_ROLE` | +| `rescueTokens(token, to, amount)` | `DEFAULT_ADMIN_ROLE` | +| `sweepDust(token, to)` | `ADMIN_ROLE` | +| `resetPendingSlashCount(op, count)` | `ADMIN_ROLE` (recovery only; default reverts unless overridden) | ### Asset Configuration (StakingAssetsFacet) -| Function | Caller | -|---|---| -| `enableAsset(asset, config)` | `ASSET_MANAGER_ROLE` | -| `enableAssetWithAdapter(asset, config, adapter)` | `ASSET_MANAGER_ROLE` | -| `disableAsset(asset)` | `ASSET_MANAGER_ROLE` | -| `registerAdapter(token, adapter)` | `ASSET_MANAGER_ROLE` | -| `removeAdapter(token)` | `ASSET_MANAGER_ROLE` | -| `setRequireAdapters(bool)` | `ASSET_MANAGER_ROLE` | +| Function | Caller | +| ------------------------------------------------------------------------------- | -------------------- | +| `enableAsset(asset, config)` | `ASSET_MANAGER_ROLE` | +| `enableAssetWithAdapter(asset, config, adapter)` | `ASSET_MANAGER_ROLE` | +| `disableAsset(asset)` | `ASSET_MANAGER_ROLE` | +| `registerAdapter(token, adapter)` | `ASSET_MANAGER_ROLE` | +| `removeAdapter(token)` | `ASSET_MANAGER_ROLE` | +| `setRequireAdapters(bool)` | `ASSET_MANAGER_ROLE` | | `startAdapterMigration` / `completeAdapterMigration` / `cancelAdapterMigration` | `ASSET_MANAGER_ROLE` | ### Slashing Entrypoints (StakingSlashingFacet) -| Function | Caller | -|---|---| -| `slashForBlueprint(...)` | `SLASHER_ROLE` (Tangle is the canonical slasher) | -| `slashForService(...)` | `SLASHER_ROLE` -- per-asset commitment slashing | -| `slash(...)` | `SLASHER_ROLE` -- native consensus slash | -| `incrementPendingSlash(operator)` | `SLASHER_ROLE` | -| `decrementPendingSlash(operator)` | `SLASHER_ROLE` | +| Function | Caller | +| --------------------------------- | ------------------------------------------------ | +| `slashForBlueprint(...)` | `SLASHER_ROLE` (Tangle is the canonical slasher) | +| `slashForService(...)` | `SLASHER_ROLE` -- per-asset commitment slashing | +| `slash(...)` | `SLASHER_ROLE` -- native consensus slash | +| `incrementPendingSlash(operator)` | `SLASHER_ROLE` | +| `decrementPendingSlash(operator)` | `SLASHER_ROLE` | ### Operator Wiring From Tangle (StakingOperatorsFacet) -| Function | Caller | -|---|---| -| `addBlueprintForOperator(op, id)` | `TANGLE_ROLE` | +| Function | Caller | +| ------------------------------------ | ------------- | +| `addBlueprintForOperator(op, id)` | `TANGLE_ROLE` | | `removeBlueprintForOperator(op, id)` | `TANGLE_ROLE` | ## MBSMRegistry (`src/MBSMRegistry.sol`) -| Function | Caller | -|---|---| -| `addVersion(mbsm)` | `MANAGER_ROLE`. Rejects EOA *targets* (`mbsmAddress.code.length != 0`); the caller can be any holder of the role. | -| `pinBlueprint(blueprintId, revision)` | `MANAGER_ROLE` | -| `unpinBlueprint(blueprintId)` | `MANAGER_ROLE` | -| `initiateDeprecation(revision)` | `MANAGER_ROLE` | -| `completeDeprecation(revision)` | `MANAGER_ROLE`, after grace period | -| `queueEmergencyDeprecation(revision)` | `MANAGER_ROLE` | -| `executeEmergencyDeprecation(revision)` | `MANAGER_ROLE`, after `EMERGENCY_DEPRECATION_DELAY` (24h) | -| `cancelEmergencyDeprecation(revision)` | `MANAGER_ROLE` | -| `setDeprecationGracePeriod(seconds)` | `MANAGER_ROLE` | -| `_authorizeUpgrade(newImpl)` | `UPGRADER_ROLE` | +| Function | Caller | +| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| `addVersion(mbsm)` | `MANAGER_ROLE`. Rejects EOA _targets_ (`mbsmAddress.code.length != 0`); the caller can be any holder of the role. | +| `pinBlueprint(blueprintId, revision)` | `MANAGER_ROLE` | +| `unpinBlueprint(blueprintId)` | `MANAGER_ROLE` | +| `initiateDeprecation(revision)` | `MANAGER_ROLE` | +| `completeDeprecation(revision)` | `MANAGER_ROLE`, after grace period | +| `queueEmergencyDeprecation(revision)` | `MANAGER_ROLE` | +| `executeEmergencyDeprecation(revision)` | `MANAGER_ROLE`, after `EMERGENCY_DEPRECATION_DELAY` (24h) | +| `cancelEmergencyDeprecation(revision)` | `MANAGER_ROLE` | +| `setDeprecationGracePeriod(seconds)` | `MANAGER_ROLE` | +| `_authorizeUpgrade(newImpl)` | `UPGRADER_ROLE` | `MANAGER_ROLE` SHOULD be the timelock. The 24h emergency deprecation delay is the floor on how fast a single role can disable an MBSM revision; combined with the timelock delay on top, a queued malicious deprecation has at least `timelockDelay + 24h` of observability before it lands. @@ -192,73 +192,73 @@ Staking and delegation are a separate proxy with its own role registry. Function ### `ValidatorPodManager` -| Function | Caller | -|---|---| -| `createPod()` | Anyone (one pod per address) | -| `recordBeaconChainEthBalanceUpdate(podOwner, sharesDelta)` | Pod (the contract itself) | +| Function | Caller | +| ---------------------------------------------------------- | ---------------------------- | +| `createPod()` | Anyone (one pod per address) | +| `recordBeaconChainEthBalanceUpdate(podOwner, sharesDelta)` | Pod (the contract itself) | ### `L2SlashingReceiver` -| Function | Caller | -|---|---| -| `receiveMessage(sourceChainId, sender, payload)` | The configured cross-chain messenger only | +| Function | Caller | +| -------------------------------------------------- | ------------------------------------------- | +| `receiveMessage(sourceChainId, sender, payload)` | The configured cross-chain messenger only | | `setAuthorizedSender(chainId, sender, authorized)` | Owner; authorization is timelocked (2 days) | -| `activateAuthorizedSender(chainId, sender)` | Owner, after `SENDER_ACTIVATION_DELAY` | -| `setMessenger(addr)` | Owner | +| `activateAuthorizedSender(chainId, sender)` | Owner, after `SENDER_ACTIVATION_DELAY` | +| `setMessenger(addr)` | Owner | ### `L2SlashingConnector` -| Function | Caller | -|---|---| -| `propagateBeaconSlashing(pod, newFactor)` | `slashingOracle` OR `owner` (the `onlySlashingOracle` modifier accepts both) | -| `propagateBeaconSlashingToChain(pod, newFactor, destChain)` | `slashingOracle` or `owner` | -| `batchPropagateBeaconSlashing(...)` | `slashingOracle` or `owner` | -| `setMessenger(addr)` | Owner | -| `setSlashingOracle(addr)` | Owner | -| `setChainConfig(...)` | Owner | -| `setDefaultDestinationChain(chainId)` | Owner | -| `registerPodOperator(pod, operator)` | Owner | -| `batchRegisterPodOperators(...)` | Owner | -| `transferOwnership(newOwner)` | Owner | +| Function | Caller | +| ----------------------------------------------------------- | ---------------------------------------------------------------------------- | +| `propagateBeaconSlashing(pod, newFactor)` | `slashingOracle` OR `owner` (the `onlySlashingOracle` modifier accepts both) | +| `propagateBeaconSlashingToChain(pod, newFactor, destChain)` | `slashingOracle` or `owner` | +| `batchPropagateBeaconSlashing(...)` | `slashingOracle` or `owner` | +| `setMessenger(addr)` | Owner | +| `setSlashingOracle(addr)` | Owner | +| `setChainConfig(...)` | Owner | +| `setDefaultDestinationChain(chainId)` | Owner | +| `registerPodOperator(pod, operator)` | Owner | +| `batchRegisterPodOperators(...)` | Owner | +| `transferOwnership(newOwner)` | Owner | ## Governance ### `TangleTimelock` -| Function | Caller | -|---|---| -| `schedule(...)` | `PROPOSER_ROLE` (TangleGovernor) | -| `execute(...)` | `EXECUTOR_ROLE` (TangleGovernor or open) | -| `cancel(...)` | `CANCELLER_ROLE` | -| `updateDelay(newDelay)` | The timelock itself only (`onlySelf`), bounded `[1d, 30d]` | -| `_authorizeUpgrade(newImpl)` | The timelock itself (`onlySelf`) | +| Function | Caller | +| ---------------------------- | ---------------------------------------------------------- | +| `schedule(...)` | `PROPOSER_ROLE` (TangleGovernor) | +| `execute(...)` | `EXECUTOR_ROLE` (TangleGovernor or open) | +| `cancel(...)` | `CANCELLER_ROLE` | +| `updateDelay(newDelay)` | The timelock itself only (`onlySelf`), bounded `[1d, 30d]` | +| `_authorizeUpgrade(newImpl)` | The timelock itself (`onlySelf`) | ### `TangleGovernor` -| Function | Caller | -|---|---| -| `propose(...)` | Anyone with voting power above `proposalThreshold` | -| `castVote(...)` | Token holders with voting power at the snapshotted timestamp | -| `_authorizeUpgrade(newImpl)` | Through governance proposal only | +| Function | Caller | +| ---------------------------- | ------------------------------------------------------------ | +| `propose(...)` | Anyone with voting power above `proposalThreshold` | +| `castVote(...)` | Token holders with voting power at the snapshotted timestamp | +| `_authorizeUpgrade(newImpl)` | Through governance proposal only | ### `TangleToken` -| Function | Caller | -|---|---| -| `mint(to, amount)` | `MINTER_ROLE` (timelock); subject to `MAX_SUPPLY` | -| `_authorizeUpgrade(newImpl)` | `UPGRADER_ROLE` | -| `clock()` | view, returns `block.timestamp` (ERC-6372) | +| Function | Caller | +| ---------------------------- | ------------------------------------------------- | +| `mint(to, amount)` | `MINTER_ROLE` (timelock); subject to `MAX_SUPPLY` | +| `_authorizeUpgrade(newImpl)` | `UPGRADER_ROLE` | +| `clock()` | view, returns `block.timestamp` (ERC-6372) | ## Recommended Production Grant Matrix -| Role | Holder | Why | -|---|---|---| -| `DEFAULT_ADMIN_ROLE` | TangleTimelock | Role grants and revokes flow through governance | -| `ADMIN_ROLE` | TangleTimelock | Setters need a delay window for community exit | -| `UPGRADER_ROLE` | TangleTimelock | Upgrades need a delay window | -| `PAUSER_ROLE` | Operations multisig | Fast response to active incidents | -| `SLASH_ADMIN_ROLE` | Slashing oversight multisig | Distinct from operations to prevent conflict of interest | -| `MANAGER_ROLE` (MBSM) | TangleTimelock | MBSM version changes need a delay | -| `MINTER_ROLE` (TangleToken) | TangleTimelock | Inflation flows through governance | +| Role | Holder | Why | +| --------------------------- | --------------------------- | -------------------------------------------------------- | +| `DEFAULT_ADMIN_ROLE` | TangleTimelock | Role grants and revokes flow through governance | +| `ADMIN_ROLE` | TangleTimelock | Setters need a delay window for community exit | +| `UPGRADER_ROLE` | TangleTimelock | Upgrades need a delay window | +| `PAUSER_ROLE` | Operations multisig | Fast response to active incidents | +| `SLASH_ADMIN_ROLE` | Slashing oversight multisig | Distinct from operations to prevent conflict of interest | +| `MANAGER_ROLE` (MBSM) | TangleTimelock | MBSM version changes need a delay | +| `MINTER_ROLE` (TangleToken) | TangleTimelock | Inflation flows through governance | The deployer EOA MUST hold zero roles after deployment. Verify with the on-chain audit that `_applyRoleHandoff` produced. See [Upgrade Discipline](/developers/upgrade-discipline) for the post-deploy checklist. diff --git a/pages/developers/slashing.mdx b/pages/developers/slashing.mdx index 666f3d6..6e617b5 100644 --- a/pages/developers/slashing.mdx +++ b/pages/developers/slashing.mdx @@ -73,14 +73,14 @@ The admin agrees with a dispute or otherwise wants to drop a proposal. State tra ## Authorization -| Function | Caller | -|---|---| -| `proposeSlash` | Service owner, blueprint owner, or BSM-declared `querySlashingOrigin`. Operator must already be in the service (`OperatorNotInService` revert otherwise). `slashBps` must be in `(0, BPS_DENOMINATOR]` and produce non-zero effective bps after exposure scaling. | -| `disputeSlash` | The slashed operator (must post bond), or `SLASH_ADMIN_ROLE` (no bond) | -| `executeSlash` | Anyone, gated by `isExecutable` | -| `executeSlashBatch` | Anyone. Non-executable ids in the batch are SILENTLY SKIPPED, not reverted. | -| `cancelSlash` | `SLASH_ADMIN_ROLE` only. Can cancel from `Pending` or `Disputed`. | -| `setSlashConfig` | `ADMIN_ROLE` (route through `TangleTimelock` in production) | +| Function | Caller | +| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `proposeSlash` | Service owner, blueprint owner, or BSM-declared `querySlashingOrigin`. Operator must already be in the service (`OperatorNotInService` revert otherwise). `slashBps` must be in `(0, BPS_DENOMINATOR]` and produce non-zero effective bps after exposure scaling. | +| `disputeSlash` | The slashed operator (must post bond), or `SLASH_ADMIN_ROLE` (no bond) | +| `executeSlash` | Anyone, gated by `isExecutable` | +| `executeSlashBatch` | Anyone. Non-executable ids in the batch are SILENTLY SKIPPED, not reverted. | +| `cancelSlash` | `SLASH_ADMIN_ROLE` only. Can cancel from `Pending` or `Disputed`. | +| `setSlashConfig` | `ADMIN_ROLE` (route through `TangleTimelock` in production) | The BSM-declared slashing origin is queried dynamically on each `proposeSlash` call. Operators MUST audit the BSM's code AND its upgradeability before joining any blueprint that uses one. A BSM whose owner is compromised can grant slashing rights to an attacker. If you do not accept that risk, only join blueprints whose BSM is non-upgradeable. @@ -90,14 +90,14 @@ See [Auth Surface](/developers/auth-surface) for the full role-by-role table acr Set with `setSlashConfig(disputeWindow, instantSlashEnabled, maxSlashBps, disputeResolutionDeadline, disputeBond, maxPendingSlashesPerOperator)`. All parameters are admin-tunable and SHOULD be timelocked in production. -| Parameter | Default | Bounds | Purpose | -|---|---|---|---| -| `disputeWindow` | 7 days | [1 hour, 30 days] | How long after `proposeSlash` the operator has to dispute | -| `instantSlashEnabled` | `false` | bool | Reserved for emergencies. The current public `proposeSlash` hardcodes `instant=false`, so this flag has no effect through the standard API. A future internal entrypoint (or upgrade) would honor it. | -| `maxSlashBps` | 10000 | (0, 10000] | Hard cap on any single slash proposal | -| `disputeResolutionDeadline` | 14 days | [1 day, 60 days] | Time `SLASH_ADMIN` has to resolve a dispute before it auto-fails | -| `disputeBond` | 0 | uint256 | Native-asset bond required from the operator to dispute. Forfeit to treasury on auto-fail or execute, refunded on cancel | -| `maxPendingSlashesPerOperator` | 32 | (0, uint16.max] | Cap on concurrent pending slashes per operator (anti-spam) | +| Parameter | Default | Bounds | Purpose | +| ------------------------------ | ------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `disputeWindow` | 7 days | [1 hour, 30 days] | How long after `proposeSlash` the operator has to dispute | +| `instantSlashEnabled` | `false` | bool | Reserved for emergencies. The current public `proposeSlash` hardcodes `instant=false`, so this flag has no effect through the standard API. A future internal entrypoint (or upgrade) would honor it. | +| `maxSlashBps` | 10000 | (0, 10000] | Hard cap on any single slash proposal | +| `disputeResolutionDeadline` | 14 days | [1 day, 60 days] | Time `SLASH_ADMIN` has to resolve a dispute before it auto-fails | +| `disputeBond` | 0 | uint256 | Native-asset bond required from the operator to dispute. Forfeit to treasury on auto-fail or execute, refunded on cancel | +| `maxPendingSlashesPerOperator` | 32 | (0, uint16.max] | Cap on concurrent pending slashes per operator (anti-spam) | `disputeBond` defaults to 0 (disabled). On a live network you SHOULD set a bond high enough to deter free self-disputes but low enough that legitimate disputes are not gated by capital. A reasonable starting point is 1% of `minOperatorStake`. diff --git a/pages/developers/upgrade-discipline.mdx b/pages/developers/upgrade-discipline.mdx index bd06b4a..50402ed 100644 --- a/pages/developers/upgrade-discipline.mdx +++ b/pages/developers/upgrade-discipline.mdx @@ -9,19 +9,19 @@ Every upgradeable Tangle contract is UUPS. The role that gates `_authorizeUpgrad ## Upgradeable Contracts -| Contract | Upgrade gate | Source | -|---|---|---| -| `Tangle` | `UPGRADER_ROLE` | [`src/Tangle.sol`](https://github.com/tangle-network/tnt-core/blob/main/src/Tangle.sol) | -| `MultiAssetDelegation` | `ADMIN_ROLE` (no `UPGRADER_ROLE` on this contract) | [`src/staking/MultiAssetDelegation.sol`](https://github.com/tangle-network/tnt-core/blob/main/src/staking/MultiAssetDelegation.sol) | -| `MBSMRegistry` | `UPGRADER_ROLE` | [`src/MBSMRegistry.sol`](https://github.com/tangle-network/tnt-core/blob/main/src/MBSMRegistry.sol) | -| `TangleGovernor` | `onlyGovernance` (proposal-driven) | [`src/governance/TangleGovernor.sol`](https://github.com/tangle-network/tnt-core/blob/main/src/governance/TangleGovernor.sol) | -| `TangleTimelock` | `onlySelf` (the timelock executing its own queued proposal) | [`src/governance/TangleTimelock.sol`](https://github.com/tangle-network/tnt-core/blob/main/src/governance/TangleTimelock.sol) | -| `TangleToken` | `UPGRADER_ROLE` | [`src/governance/TangleToken.sol`](https://github.com/tangle-network/tnt-core/blob/main/src/governance/TangleToken.sol) | -| `RewardVaults` | `UPGRADER_ROLE` | [`src/rewards/RewardVaults.sol`](https://github.com/tangle-network/tnt-core/blob/main/src/rewards/RewardVaults.sol) | -| `InflationPool` | `UPGRADER_ROLE` | [`src/rewards/InflationPool.sol`](https://github.com/tangle-network/tnt-core/blob/main/src/rewards/InflationPool.sol) | -| `ServiceFeeDistributor` | `UPGRADER_ROLE` | [`src/rewards/ServiceFeeDistributor.sol`](https://github.com/tangle-network/tnt-core/blob/main/src/rewards/ServiceFeeDistributor.sol) | -| `StreamingPaymentManager` | `UPGRADER_ROLE` | [`src/rewards/StreamingPaymentManager.sol`](https://github.com/tangle-network/tnt-core/blob/main/src/rewards/StreamingPaymentManager.sol) | -| `TangleMetrics` | `UPGRADER_ROLE` | [`src/rewards/TangleMetrics.sol`](https://github.com/tangle-network/tnt-core/blob/main/src/rewards/TangleMetrics.sol) | +| Contract | Upgrade gate | Source | +| ------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| `Tangle` | `UPGRADER_ROLE` | [`src/Tangle.sol`](https://github.com/tangle-network/tnt-core/blob/main/src/Tangle.sol) | +| `MultiAssetDelegation` | `ADMIN_ROLE` (no `UPGRADER_ROLE` on this contract) | [`src/staking/MultiAssetDelegation.sol`](https://github.com/tangle-network/tnt-core/blob/main/src/staking/MultiAssetDelegation.sol) | +| `MBSMRegistry` | `UPGRADER_ROLE` | [`src/MBSMRegistry.sol`](https://github.com/tangle-network/tnt-core/blob/main/src/MBSMRegistry.sol) | +| `TangleGovernor` | `onlyGovernance` (proposal-driven) | [`src/governance/TangleGovernor.sol`](https://github.com/tangle-network/tnt-core/blob/main/src/governance/TangleGovernor.sol) | +| `TangleTimelock` | `onlySelf` (the timelock executing its own queued proposal) | [`src/governance/TangleTimelock.sol`](https://github.com/tangle-network/tnt-core/blob/main/src/governance/TangleTimelock.sol) | +| `TangleToken` | `UPGRADER_ROLE` | [`src/governance/TangleToken.sol`](https://github.com/tangle-network/tnt-core/blob/main/src/governance/TangleToken.sol) | +| `RewardVaults` | `UPGRADER_ROLE` | [`src/rewards/RewardVaults.sol`](https://github.com/tangle-network/tnt-core/blob/main/src/rewards/RewardVaults.sol) | +| `InflationPool` | `UPGRADER_ROLE` | [`src/rewards/InflationPool.sol`](https://github.com/tangle-network/tnt-core/blob/main/src/rewards/InflationPool.sol) | +| `ServiceFeeDistributor` | `UPGRADER_ROLE` | [`src/rewards/ServiceFeeDistributor.sol`](https://github.com/tangle-network/tnt-core/blob/main/src/rewards/ServiceFeeDistributor.sol) | +| `StreamingPaymentManager` | `UPGRADER_ROLE` | [`src/rewards/StreamingPaymentManager.sol`](https://github.com/tangle-network/tnt-core/blob/main/src/rewards/StreamingPaymentManager.sol) | +| `TangleMetrics` | `UPGRADER_ROLE` | [`src/rewards/TangleMetrics.sol`](https://github.com/tangle-network/tnt-core/blob/main/src/rewards/TangleMetrics.sol) | ## Storage Layout Rules