Skip to content

Commit 90dcf64

Browse files
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

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,7 @@ script/tanglepod-cli/tanglepod-cli
6262
*.log
6363
*.out
6464
*.toc
65+
66+
# Per-machine marker: opts this repo into the ~/bin/forge task-spooler queue
67+
# (one forge build/test at a time even under parallel CC agents).
68+
.forge-queue

docs/audits/REDTEAM-DOS-ACCESS-2026-05-16.md

Lines changed: 199 additions & 0 deletions
Large diffs are not rendered by default.

docs/audits/REDTEAM-ECONOMIC-ORACLE-2026-05-16.md

Lines changed: 219 additions & 0 deletions
Large diffs are not rendered by default.

docs/audits/REDTEAM-REENTRANCY-GRIEFING-2026-05-16.md

Lines changed: 441 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
# Red-team audit: storage + UUPS — 2026-05-16
2+
3+
Scope: PRs #132 (subscription billing rearchitecture), #133 (payments facet split + RFQ
4+
hardening + multi-asset bill weighting), #134 (O(1) operator stake aggregate + VPM
5+
share-pool slashing), #136 (claimRewardsAll griefing isolation), #137 (bindings regen),
6+
#138 (indexer events). Audit window: `8b2777b..HEAD` on `main`.
7+
8+
## Summary
9+
10+
| Severity | Count |
11+
| --- | --- |
12+
| CRITICAL | 0 |
13+
| HIGH | 0 |
14+
| MEDIUM | 0 |
15+
| LOW | 1 |
16+
| INFORMATIONAL | 3 |
17+
18+
No upgrade-safety regressions detected. One LOW finding on divergent upgrade-authority
19+
role between `Base` and `MultiAssetDelegation`. Three informational notes on
20+
storage-layout reinterpretation and non-proxy "legacy slot preservation" framing.
21+
22+
## Findings
23+
24+
### L-1 — `MultiAssetDelegation._authorizeUpgrade` is gated by `ADMIN_ROLE`, not `UPGRADER_ROLE`
25+
26+
- **Where:** `src/staking/MultiAssetDelegation.sol:116`
27+
- **What:** `_authorizeUpgrade(address) internal override onlyRole(ADMIN_ROLE) { }`.
28+
The contract never defines or grants a separate `UPGRADER_ROLE`. By contrast,
29+
`Base.sol:42,177,232` defines `UPGRADER_ROLE = keccak256("UPGRADER_ROLE")` and
30+
uses it for `_authorizeUpgrade`. `MBSMRegistry`, `TangleMetrics`,
31+
`ServiceFeeDistributor`, `RewardVaults`, `InflationPool`, and
32+
`StreamingPaymentManager` all follow the `Base` pattern.
33+
- **Impact:** Whoever holds `ADMIN_ROLE` on MAD can both administer parameters
34+
(commission, asset config, adapter migration, slasher role grants) AND upgrade
35+
the implementation. There is no way to delegate upgrade authority to a separate
36+
multisig/timelock without also granting day-to-day admin power, which collapses
37+
the role-separation that `Base` is structured to enforce.
38+
- **Reproducer:** Read `_authorizeUpgrade` on the deployed `MultiAssetDelegation`
39+
proxy and the role mask of `ADMIN_ROLE`. Compare to `Tangle.UPGRADER_ROLE`. Any
40+
account with `ADMIN_ROLE` can call `upgradeToAndCall(maliciousImpl, "")` directly.
41+
- **Fix:** Mirror `Base`: declare `bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE")`;
42+
grant it in `initialize`; switch `_authorizeUpgrade` to `onlyRole(UPGRADER_ROLE)`.
43+
Storage-safe (no new slots; role data lives in
44+
`AccessControlUpgradeable`'s ERC-7201 storage). Existing deployments need a
45+
follow-up admin tx that grants `UPGRADER_ROLE` to the chosen upgrader before
46+
any subsequent upgrade.
47+
48+
### INFO-1 — Slot 77 in `TangleStorage` was retyped between #132 and #133 (`_twapCursorByOp``_twapCursorByOpAsset`)
49+
50+
- **Where:** `src/TangleStorage.sol:423` (`_twapCursorByOpAsset`).
51+
- **What:** PR #132 introduced `mapping(uint64 => mapping(address => uint256)) _twapCursorByOp`
52+
at slot 77. PR #133 replaced the declaration with
53+
`mapping(uint64 => mapping(address => mapping(bytes32 => uint256))) _twapCursorByOpAsset`
54+
at the same slot. Mapping root slot is reused; data lives at hashed addresses
55+
so the value reads at any (svcId, op) key from the previous declaration are
56+
now orphaned (never read by the new code path).
57+
- **Impact:** Production proxies that were already initialized under #132 and
58+
carry attribution data at the old layout would lose that data on upgrade to
59+
the #133 layout. Verified the only known live deployment manifest
60+
(`deployments/base-sepolia/latest.json`) is from `19887b5` (Tangle v0.10.9),
61+
which predates PR #132 — so no production proxy holds `_twapCursorByOp`
62+
data. No on-chain impact. Calling out so future deployments do not assume
63+
the slot's data type is stable.
64+
- **Fix (if any prior deployment is later discovered):** Migration script must
65+
read the old slot and re-key the data via `_twapCursorByOpAsset[svcId][op][bondAssetHash]`
66+
before the first `billSubscription` call.
67+
68+
### INFO-2 — `ValidatorPodManager` "legacy slot preserved as placeholder" is defensive but inapplicable
69+
70+
- **Where:** `src/beacon/ValidatorPodManager.sol:33,99,104,116,119`.
71+
- **What:** The contract is declared `contract ValidatorPodManager is IStaking, Ownable, ReentrancyGuard`
72+
with a `constructor(address, uint256)` (line 275). It is deployed directly by
73+
`script/DeployBeaconSlashing.s.sol:168` and `script/LocalTestnet.s.sol:972`
74+
with `new ValidatorPodManager(...)` — no proxy, no UUPS, no upgrade path. The
75+
comments labelling slots 12/13/15/16 as "legacy slot, retained for layout
76+
compatibility" therefore protect against an upgrade vector that does not exist.
77+
- **Impact:** Zero. The placeholder slots cost one bytes32 each on every new
78+
pod-manager deployment and are never read by the live code. Consider this
79+
informational hygiene: either (a) document that VPM is intentionally
80+
non-upgradeable and remove the preservation comments, or (b) move VPM to a
81+
UUPS proxy if storage continuity across versions matters operationally.
82+
- **Verification of layout claim:** Despite the upgrade path being absent, the
83+
layout *is* faithfully preserved: `forge inspect ValidatorPodManager storage-layout`
84+
shows the legacy mappings at the same indices they occupied at `8b2777b`
85+
(`delegations``_legacyDelegations` slot 12, `operatorDelegatedStake`
86+
`_legacyOperatorDelegatedStake` slot 13, `_operatorDelegators` slot 15,
87+
`_isDelegator` slot 16). New share-pool state (`_operatorDelegationPools`,
88+
`_delegationShares`) is appended at slots 25–26. Public ABI is preserved via
89+
explicit `delegations(address,address)` and `operatorDelegatedStake(address)`
90+
view functions at L377/L384 of the new file.
91+
92+
### INFO-3 — `DelegationStorage` gap arithmetic verified
93+
94+
- **Where:** `src/staking/DelegationStorage.sol:491,495`.
95+
- **What:** `_operatorDelegatedAggregate` was appended in PR #134 to back the
96+
O(1) `_getOperatorDelegatedStakeForAsset` lookup. The gap was reduced from 44
97+
to 43 to absorb the one-slot addition. Pre-existing TWAP cursor and
98+
`lastUpdate` slots (`_cumStakeSeconds`, `_cumStakeSecondsLastUpdate`) keep
99+
their indices. All other named slots above the gap retain their previous
100+
positions.
101+
- **Impact:** Safe. Aggregate invariant
102+
`_operatorDelegatedAggregate[op][h] == _rewardPools[op][h].totalAssets + Σ_bp _blueprintPools[op][bp][h].totalAssets`
103+
is the only correctness contract that matters for upgrade — and it lives in
104+
contract logic, not storage layout.
105+
106+
## Clean checks
107+
108+
- **TangleStorage slot ordering.** `forge inspect Tangle storage-layout` confirms
109+
slot 0 (`_staking`) through slot 77 (`_twapCursorByOpAsset`) match the
110+
declaration order in `src/TangleStorage.sol`, including all packed sub-slots
111+
(`_treasury`+`_maxBlueprintsPerOperator` packed in slot 1;
112+
`_rewardVaults`+`_defaultTntMinExposureBps`+`_deprecatedTntStakerFeeBps`+`_tntPaymentDiscountBps`
113+
packed in slot 60; the four uint64 TTL fields packed in slot 66). Gap is
114+
`uint256[40]` at slot 78. OZ-Upgradeable parents (`Initializable`,
115+
`UUPSUpgradeable`, `PausableUpgradeable`, `ReentrancyGuardUpgradeable`,
116+
`AccessControlUpgradeable`) all use ERC-7201 namespaced storage in OZ v5 and
117+
therefore do not occupy contiguous slots — `_staking` correctly sits at slot 0.
118+
- **Payments facet split storage shared via `Base`/`TangleStorage`.** Audited every
119+
new and modified mixin (`PaymentsCore`, `PaymentsEscrow`, `PaymentsBilling`,
120+
`PaymentsDistribution`, `PaymentsRewards`, `PaymentsEffectiveExposure`,
121+
`ServicesApprovalsViews`). None declares any state variable. Mixins that touch
122+
protocol state inherit `Base` (which inherits `TangleStorage`), so reads/writes
123+
resolve to the same shared slots used by the pre-split `Payments.sol`.
124+
`PaymentsEffectiveExposure` is the lone mixin that does *not* inherit `Base`
125+
(it only defines `internal virtual` hooks and `EXPOSURE_PRECISION`/`_BPS_DENOM`
126+
constants) so it cannot collide.
127+
- **Facet storage sharing.** `TanglePaymentsFacet` inherits `PaymentsEscrow + PaymentsBilling`,
128+
`TanglePaymentsDistributionFacet` inherits `PaymentsDistribution`,
129+
`TanglePaymentsRewardsFacet` inherits `PaymentsRewards`,
130+
`TangleServicesViewsFacet` inherits `ServicesApprovalsViews`. Each is intended
131+
to be `delegatecall`-ed from `Tangle` via `FacetRouterBase._fallbackToFacet`,
132+
so their storage view is the proxy's storage — i.e. `TangleStorage`. None
133+
declares its own state; the only members are pure `selectors()` functions and
134+
pure dispatchers to the parent. No collision risk.
135+
- **`_disableInitializers` + `initializer` modifier.** Verified across every
136+
UUPS contract: `Base` (152), `MultiAssetDelegation` (31),
137+
`MBSMRegistry` (90), `L2SlashingReceiver` (127), `TangleMetrics` (138),
138+
`ServiceFeeDistributor` (156), `RewardVaults` (195), `InflationPool` (243),
139+
`StreamingPaymentManager` (90), `TangleTimelock` (41), `TangleGovernor` (77),
140+
`TangleToken` (66). All `initialize` functions carry the `initializer` modifier.
141+
All constructors are tagged `@custom:oz-upgrades-unsafe-allow constructor`.
142+
- **`__gap` declarations.** Enumerated all 12 occurrences:
143+
`TangleStorage` 40 (was 41 pre-#132, reduced by 1 for `_twapCursorByOp[Asset]`);
144+
`DelegationStorage` 43 (was 44 pre-#134, reduced by 1 for `_operatorDelegatedAggregate`);
145+
`MBSMRegistry` 50; rewards/ contracts all 50; cross-chain bridge namespaced
146+
structs each carry an internal `uint256[50] __gap`. No bridge changed in the
147+
audit window.
148+
- **External library statelessness.** `AttestationLib`, `ServiceValidationLib`,
149+
`PaymentLib`, `SignatureLib`, `SlashingLib`, `SchemaLib` — all declared
150+
`library X` with only `internal constant` declarations (no state, no
151+
`external` functions on non-`internal` storage). Solc embeds calls to these
152+
libraries inline; no `delegatecall` storage hazard exists.
153+
- **VPM legacy-slot preservation by inspection.** `forge inspect
154+
ValidatorPodManager storage-layout` (run under `FOUNDRY_PROFILE=local_build`)
155+
confirms slot 12 = `_legacyDelegations`, slot 13 = `_legacyOperatorDelegatedStake`,
156+
slot 15 = `_legacyOperatorDelegators`, slot 16 = `_legacyIsDelegator`,
157+
slot 25 = `_operatorDelegationPools`, slot 26 = `_delegationShares`. Matches
158+
the comments in source.
159+
- **Upgrade authorization sweep.** Every `_authorizeUpgrade` in the tree gates
160+
on `onlyRole(UPGRADER_ROLE)` except `MultiAssetDelegation` (L-1 above) and
161+
`L2SlashingReceiver` (intentionally `onlyOwner` because it inherits
162+
`OwnableUpgradeable`, not `AccessControl`).
163+
164+
## Method
165+
166+
- `git log --oneline 8b2777b..HEAD -- src/` to enumerate in-scope file changes.
167+
Only `ValidatorPodManager.sol` (PR #134), `DelegationStorage.sol` (PR #134),
168+
`TangleStorage.sol` (PR #133), `Payments.sol` → split (PR #133),
169+
`core/Payments{Core,Escrow,Billing,Distribution,Rewards,EffectiveExposure}.sol`,
170+
`core/ServicesApprovals{,Views}.sol` were touched in this window.
171+
- `grep -rn "uint256\[.*\] (private|internal) __gap" src/` to enumerate gaps.
172+
- `grep -rn "_authorizeUpgrade\|UPGRADER_ROLE" src/` to confirm gating.
173+
- `grep -rn "_disableInitializers" src/` to confirm constructor lock.
174+
- `forge inspect Tangle storage-layout` and `forge inspect ValidatorPodManager
175+
storage-layout` (under `FOUNDRY_PROFILE=local_build`) to verify slot indices
176+
match the declarations.
177+
- Cross-checked `deployments/base-sepolia/latest.json` to determine whether
178+
any reinterpreted slot (INFO-1) could affect a live proxy.

script/StressGriefingSeed.s.sol

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.26;
3+
4+
import { Script } from "forge-std/Script.sol";
5+
import { console2 } from "forge-std/console2.sol";
6+
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
7+
8+
/// @notice ERC20 whose `transfer` reverts. `transferFrom`/balance/approve still work so a
9+
/// service could in principle collect payment; the revert only fires when the
10+
/// diamond tries to flush operator rewards via the `_claimRewardsToken` path.
11+
contract RevertingTransferERC20 is ERC20 {
12+
error TransferGriefed();
13+
14+
constructor() ERC20("Griefer", "GRF") { }
15+
16+
function mint(address to, uint256 amount) external {
17+
_mint(to, amount);
18+
}
19+
20+
function transfer(address, uint256) public pure override returns (bool) {
21+
revert TransferGriefed();
22+
}
23+
}
24+
25+
/// @title StressGriefingSeed
26+
/// @notice Deploys a `RevertingTransferERC20` whose address the bash harness uses for
27+
/// the per-token griefing storage seed (issued via `anvil_setStorageAt`).
28+
/// `vm.store` in a broadcast script only mutates simulation state — it is NOT
29+
/// propagated to anvil — so the seeding lives entirely in the harness's RPC
30+
/// calls. This script's only on-chain side effect is the ERC20 deployment.
31+
contract StressGriefingSeed is Script {
32+
function run() external {
33+
uint256 deployerKey = vm.envUint("DEPLOYER_KEY");
34+
vm.startBroadcast(deployerKey);
35+
RevertingTransferERC20 grief = new RevertingTransferERC20();
36+
vm.stopBroadcast();
37+
// Print only this final line, parsed by the harness via `grep -oE`.
38+
console2.log("GRIEF_TOKEN=", address(grief));
39+
}
40+
}

0 commit comments

Comments
 (0)