Commit 90dcf64
authored
chore(v0.17.1): audit batch — reports + stress harness + remediation (#144)
* audit: reentrancy + griefing red-team 2026-05-16
3 medium / 2 low / 1 informational. No critical or high-severity issues.
Medium: `_distributeBill` push to BSM-resolved developer recipient can
be permanently griefed by a malicious manager; `withdrawRemainingEscrow`
has no fallback when the escrow token is broken; `getNonPaymentTerminationPolicy`
is invoked without a gas cap, letting a malicious BSM grief the livelock
escape.
Low: `_settleDisputeBond` strands dispute bonds on executed proposals
when the treasury push fails; `_operatorActiveSlashProposals` cap check
ordering is inverted (cosmetic).
Confirmed clean: every token-moving entry point holds `nonReentrant`;
the OpenZeppelin ReentrancyGuard storage slot is genuinely shared
across facets via ERC-7201; operator and keeper payments are pull via
`_pendingRewards`; self-call gates fire before any state read; CEI
ordering holds on every withdrawal / claim / refund path; the
subscription livelock escape (`terminateServiceForNonPayment`) is
permissionless.
* audit: economic + oracle red-team 2026-05-16
Read-only audit of subscription billing, share-pool inflation defense,
slashing replay, oracle adapters, and TWAP weighting under adversarial
sequences. Customer is protected by cap-at-nominal; per-operator weight
split is the residual attack surface.
Severity counts: 0 CRITICAL, 1 HIGH, 3 MEDIUM, 2 LOW, 2 INFORMATIONAL.
Top findings:
- H-1: oracle-manipulated weight inflation captures the operator pool
share of a (capped) bill, redistributing rent from honest operators
- M-1: oracle revert during billing bricks subscriptions for the
configured assets (no fund loss, denial-of-billing)
- M-3: stake-ramp at periodEnd inflates within-period TWAP weight
beyond the operator's time-averaged backing
* audit: DoS + access control red-team 2026-05-16
Read-only static audit of DoS surfaces and access-control gaps across
src/core/, src/staking/, and src/beacon/.
Two HIGH findings on the permissionless billing path: unbounded
security-commitment arrays brick subscription billing, and
try IBlueprintServiceManager(...) callsites forward 63/64 of remaining
gas instead of capping at MANAGER_HOOK_GAS_LIMIT.
Four MEDIUM and four LOW notes plus 12 clean checks.
* audit: storage + UUPS upgrade red-team 2026-05-16
* feat(local-env): end-to-end stress test harness + runbook
Single-file harness at scripts/local-env/stress-test.sh that boots a fresh
anvil + LocalTestnet deployment and walks 17 ordered economic checks against
the merged-PR surface (#132 subscription billing rearchitecture, #133
multi-asset bill weighting + EIP-170 facet split, #134 O(1) operator stake
aggregate + share-pool slashing, #136 claimRewardsAll griefing isolation,
#138 indexer event handlers).
Highlights:
- Idempotent: clean cleanup of anvil, broadcast artifacts, indexer state.
- Per-step pass/fail with timing + headline metric. Single-line summary.
- Optional --with-indexer / --with-dapp / --with-operator flags for the
off-chain side processes; none required for the 17-step protocol surface.
- Griefing-token isolation step deploys a RevertingTransferERC20 and seeds
Tangle's _pendingRewards + _pendingRewardTokens AddressSet via
anvil_setStorageAt (vm.store from a broadcast script doesn't propagate
to anvil; the harness drives the seed via curl directly).
Companion docs at scripts/local-env/STRESS-TEST.md cover prereqs, per-step
PR mapping, log locations, debugging recipes, and an extension guide.
A full green run takes ~40-85s on a warm-cache checkout.
* chore(v0.17.1): audit batch — reports + stress harness + remediation
Consolidates the four 2026-05-16 red-team audit reports, the local stress
harness, and remediation fixes for 2 HIGH and 5 MEDIUM findings into a single
batch. Originally PRs #139 #140 #141 #142 #143 — supersedes those.
## Audit reports landed (docs/audits/REDTEAM-*-2026-05-16.md)
- Reentrancy + griefing: 0 H / 3 M / 2 L / 1 Info
- Economic + oracle: 1 H / 3 M / 2 L / 2 Info
- DoS + access control: 2 H / 4 M / 4 L / 3 Info
- Storage + UUPS upgrade: 0 H / 0 M / 1 L / 3 Info
## Stress harness landed
`scripts/local-env/stress-test.sh` drives a 17-step end-to-end flow against a
local anvil node: deploys the protocol, registers operators, opens services,
funds escrow, fires bills, exercises slashing, tests the griefing-token path
via `script/StressGriefingSeed.s.sol` + `anvil_setStorageAt`, and asserts
final state. 17/17 green across five runs. Runbook in `STRESS-TEST.md`.
## Remediations
HIGH — DoS H-1: cap customer-supplied security-requirement arrays at
`MAX_SECURITY_REQUIREMENTS_PER_REQUEST = 16` in `_validateSecurityRequirements`.
An unbounded array let a customer brick their own subscription bills by
forcing the per-bill `O(operators × requirements)` walk past the block gas
limit. 16 is well above any realistic heterogeneous-asset blueprint and
keeps worst-case `64 × 16 = 1024` inner iterations within one block.
HIGH — DoS H-2 (also closes Reentrancy M-3): every bare
`try IBlueprintServiceManager(bp.manager).fn() catch` callsite now routes
through `_tryStaticcallManager(addr, calldata, minReturnLen)` which forwards
exactly `MANAGER_HOOK_GAS_LIMIT` (500_000) gas. Without the cap, Solidity's
`try/catch` forwards 63/64 of remaining gas, letting a malicious BSM drain
the keeper/proposer's budget. Covers `querySlashingOrigin`,
`requiresAggregation` (×2), `getNonPaymentTerminationPolicy`, `canJoin`,
`canLeave`, `forceRemoveAllowsBelowMin`, `getExitConfig`, `getMinOperatorStake`
(×2), `getHeartbeatInterval`, `getHeartbeatThreshold`,
`getRequiredResultCount`, `queryIsPaymentAssetAllowed`,
`getAggregationThreshold`, and converts the mutating `onAggregatedResult`
hook to `_tryCallManager`.
MEDIUM — Reentrancy M-1: developer / TNT-discount / treasury push transfers
in `_distributeBill` wrapped in `PaymentLib.tryTransferPayment`. On failure,
the un-sent amount folds into the operator pool and emits `PushTransferFailed`
with a structured destination tag. A malicious BSM-resolved developer
recipient (or paused/blocklisting token) can no longer brick distribution
for honest operators.
MEDIUM — Economic M-1: `oracle.toUSD` on the billing hot path wrapped in
`_safeToUSD` / `_safeToUSDView` helpers (capped at `ORACLE_QUERY_GAS_LIMIT =
250_000`) with raw-amount fallback + `PriceOracleFallback` event. A stalled
or reverting oracle now degrades to raw token-second weighting instead of
freezing all bills. `_accrueOperatorWeights` drops `view` since the fallback
path emits.
LOW — Storage L-1: `MultiAssetDelegation._authorizeUpgrade` now requires
`UPGRADER_ROLE` (was `ADMIN_ROLE`), restoring the defense-in-depth role
separation the protocol-level contracts already use. Role added to the
initializer.
## Greenfield cleanup
The pre-launch protocol carried audit-round tag comments left over from prior
remediation rounds — `M-8 FIX:`, `H-1 FIX:`, `Round 4 audit S-1:`,
`G-02 follow-up:`, `C-3 (Round 4):`. Stripped across 30 files. Descriptive
content that explains current behavior is retained; historical narrative is
deleted.
VPM also carried `_legacy*` mappings with comments suggesting they were kept
for "storage layout preservation" in a contract that turns out not to be
upgradeable (`ValidatorPodManager` is constructor-deployed `Ownable`, not
`UUPSUpgradeable`). The mappings and misleading comments are deleted
entirely.
Also: the `if (config.currentDeposits >= amountReturned) { … } else { …
clamp to 0 }` patterns in `DepositManager` and `StakingDelegationsFacet` are
collapsed to a single checked subtraction. The clamp described a defensive
case that is structurally unreachable under the current accounting.
## Fuzz coverage
`test/tangle/SubscriptionEscrowInvariant.t.sol` grew two new invariants and
an adversarial actor:
- `invariant_billAmountNeverExceedsNominalRate` — catches a regression of
the cap-at-nominal clamp under adversarial stake-ramp sequences.
- `invariant_baselinePinnedAtActivation` — catches any post-activation code
path that re-pins `subscriptionBaselineStake`.
- `stakeRamper` handler actor that `depositAndDelegate`s / schedules
unstakes against `operator1` during the run.
## What's deliberately deferred to a follow-up
- HIGH Economic H-1 (oracle weight inflation): the proper fix is to snapshot
per-(op, asset) USD prices at activation and reuse them at every bill,
matching the baseline pin. The fix is architecturally involved (touches
`TangleStorage`, `PaymentsBilling._accrueOperatorWeights`,
`PaymentsDistribution._initSubscriptionBaseline`,
`ServicesLifecycle._finalizeJoin`) and warrants its own focused PR with
negative-tested invariants. The customer is safe today (cap-at-nominal
bounds total damage); the residual risk is distributional between
operators in the same service.
- MEDIUM Reentrancy M-2 (escrow rescue path): admin-rescue route for stuck
escrow tokens when the customer's token is centrally paused / blocklists
the service owner. Moderate scope; better as its own PR.
Supersedes #139 #140 #141 #142 #143.1 parent bfed9c0 commit 90dcf64
54 files changed
Lines changed: 2619 additions & 434 deletions
File tree
- docs/audits
- scripts/local-env
- script
- src
- beacon
- bridges
- core
- facets
- staking
- tangle
- governance
- interfaces
- libraries
- rewards
- staking
- test/tangle
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
62 | 62 | | |
63 | 63 | | |
64 | 64 | | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
0 commit comments