Skip to content

chore(audit): round 3 deferred fixes + bindings v0.14.0#126

Merged
drewstone merged 1 commit into
mainfrom
chore/audit-round-3-deferred-fixes
May 8, 2026
Merged

chore(audit): round 3 deferred fixes + bindings v0.14.0#126
drewstone merged 1 commit into
mainfrom
chore/audit-round-3-deferred-fixes

Conversation

@drewstone
Copy link
Copy Markdown
Contributor

Summary

Round 3 lands the deferred items from PR #125's "follow-up" list. Each fix is small enough to review on its own; bundled here because they all share the same migration window (off-chain BLS aggregator format change + new claim selector + struct field reorder already coordinated).

1398 / 1398 tests pass on this branch (heavy fuzz / invariant suites excluded; same exclusions as Round 2 PR #125). Bindings regenerated to v0.14.0.

Fixes by severity

CRITICAL

Slash-and-dispute reentrancy (Round 2 economic F3)
_settleDisputeBond no longer pushes the bond back to the disputer on cancel. Instead the bond is credited to a per-disputer pull-pattern mapping (_pendingDisputeBondRefunds) and the disputer drains it via the new claimDisputeBond() selector. Closes the re-entrancy window where a contract-disputer's fallback could re-enter staking — at the bond-refund call point, _operatorPendingSlashCount had already been decremented, so an unstake call would slip past the slash-blocking gate and exit at the pre-slash exchange rate.

HIGH

  • JobsAggregation snapshot binding (operator-collusion #2c) — BLS aggregation message now binds chainid, contract address, and a hash of the operator-set snapshot. Swap-and-pop reorder invalidates in-flight signatures. BREAKING for off-chain aggregators: new message format documented in CHANGELOG.
  • TangleToken burn disabled (governance LINK staking #5) — burn and burnFrom revert with BurnDisabled(). Previously any holder could lower getPastTotalSupply() before snapshot to deflate quorum.
  • Proposal-action limit reduction (governance [TASK] Update and clean out old precompile/blueprint contracts #8) — MAX_PROPOSAL_ACTIONS 50 → 10, MAX_ACTION_VALUE 100k ETH → 10k ETH. Real legitimate proposals touch ≤ 5 targets; the wide bound let proposers bury privileged calls in action Update MSBM w/ new features in Tangle (same branch name) #50.
  • Fee-on-transfer rejection (economic F6) — PaymentLib.collectPayment and DepositManager._handleErc20Deposit now reject FoT / rebasing tokens at ingress via balance-delta check.

MEDIUM

  • RebasingAssetAdapter virtual offset (economic F2) — first-depositor inflation defense (matches staking-pool offsets).
  • Arbitrum L2 fee refund (cross-chain H-1) — new setL2RefundAddress(address). Excess refunds from createRetryableTicket no longer leak to the L1 caller's L2 alias.
  • Beacon BSM hook design note (cross-chain H-5) — documented intentional decision to NOT iterate operator's blueprint list on beacon slash. BSMs subscribe to BeaconSlashExecuted off-chain. On-chain enumeration would be O(N) gas-DoS.
  • Missing __gap on rewards UUPS (storage F-3) — uint256[50] private __gap added to TangleMetrics, RewardVaults, InflationPool, ServiceFeeDistributor, StreamingPaymentManager.

LOW

  • ServiceFeeDistributor reentrancy review (Slither finding) — verified non-exploitable. All claim entry points are nonReentrant. Documented inline.

Tests

  • New test/security/StorageLayoutSnapshotTest.t.sol pins critical storage slot positions for Tangle and verifies OZ ERC-7201 namespaced slots match v5.1.0. Round 2 storage F-1 / F-2 flagged upgrade-time field-reorder risks; this catches drift in CI.
  • test_DisputeSlash_BondRefundedOnCancel updated to drive the new pull-pattern flow.
  • 3 RebasingAdapter tests updated for virtual-offset math.
  • 3 BLS E2E tests updated for the new aggregation message format.

Bindings v0.14.0

Regenerated via cargo xtask gen-bindings. CHANGELOG entry covers every fix and explicitly enumerates the BREAKING changes (burn disabled, proposal limits, aggregation message, FoT rejection). New facet selectors registered: claimDisputeBond, pendingDisputeBondRefund, getSlashConfig (the v0.13 getSlashConfig view was added but never wired on the slashing facet — making it unreachable on the proxy; fixed here).

Off-chain consumer migration

Consumer Action
tnt-core-bindings Bump to 0.14.0 (publish via cargo xtask publish after merge)
blueprint SDK Re-pin tnt-core-bindings = "0.14.0" once published; update aggregator to use new BLS message format
dapp Re-run yarn sync:tnt-core-assets; surface claimDisputeBond in the dispute resolution UX
docs Add v0.14.0 release notes; update slashing dispute-flow doc to describe pull-pattern

Findings still architecturally pending (out of scope here)

Each genuinely needs design discussion before implementation; not bundling forced-architectural changes into this PR.

  • C-3 receiver-rotation replay (epoch versioning) — cross-chain
  • F5 billSubscription stake-JIT (TWAP design) — economic
  • S-1 LDV redeem controller scoping (interface change) — staking
  • S-2 registerAdapter migration logic — staking
  • G-02 ValidatorPodManager._slash share-based pool migration — gas/DoS

🤖 Generated with Claude Code

Lands the deferred items from Round 2 PR #125's "follow-up" list. Each
fix is small enough to review on its own; PR is structured as one
commit because they all share a single migration window (off-chain BLS
aggregator format change + new claim selector + struct field reorder
already coordinated).

CRITICAL — slash-and-dispute reentrancy (Round 2 economic F3)
- `_settleDisputeBond` no longer pushes the bond back to the disputer
  on cancel. Instead the bond is credited to a per-disputer pull-pattern
  mapping (`_pendingDisputeBondRefunds`) and the disputer drains it via
  the new `claimDisputeBond()` selector. Closed the re-entrancy window
  where a contract-disputer's fallback could re-enter staking — at the
  bond-refund call point, `_operatorPendingSlashCount` has already been
  decremented, so an unstake call would slip past the slash-blocking
  gate and exit at the pre-slash exchange rate.
- Treasury forfeit on execute remains push (treasury is protocol-
  controlled; no operator-side bypass possible there).
- Added selectors: `claimDisputeBond()`, `pendingDisputeBondRefund(address)`,
  and `getSlashConfig()` (which was added in v0.13 but never registered
  on the slashing facet — the surface was unreachable on the proxy).

HIGH — JobsAggregation snapshot binding (Round 2 operator-collusion #2c)
- The BLS aggregation message now binds chainid, contract address, and
  a hash of the operator-set snapshot in addition to (serviceId, callId,
  output). A swap-and-pop reorder (operator leaves / forceRemove)
  invalidates any in-flight aggregated signature instead of silently
  mis-crediting a different operator at the same bitmap index.
- BREAKING for off-chain aggregators: the message format now is
  `abi.encode("TANGLE_BLS_AGG_v1", chainid, address(tangle), serviceId,
  callId, keccak256(abi.encode(operators)), keccak256(output))`.
- Test helpers updated; new `BLSTestHelper.buildJobResultMessage(serviceId,
  callId, tangleAddress, operators, output)` overload alongside the
  legacy 3-arg form (retained for tests that don't go through the
  on-chain aggregation path).

HIGH — TangleToken burn disabled (Round 2 governance #5)
- `burn(uint256)` and `burnFrom(address,uint256)` revert with
  `BurnDisabled()`. Unrestricted ERC20 burn allowed any holder to lower
  `getPastTotalSupply()` before a snapshot, deflating quorum and letting
  low-stake proposals pass. There is no protocol use case for users
  burning their TNT (inflation is governance-controlled via
  InflationPool). The internal `_burn` is retained for any future
  upgradeable code path that gates burn behind an explicit role.

HIGH — proposal-action limit reduction (Round 2 governance #8)
- `MAX_PROPOSAL_ACTIONS` lowered from 50 to 10. `MAX_ACTION_VALUE`
  lowered from 100k ETH to 10k ETH per action. 50 actions × 100k ETH
  was a vast surface area where a malicious proposer could bury a
  privileged call (`grantRole`, `upgradeToAndCall`, etc.) in action
  #50 of 50 where UI tooling may truncate / skim. Real legitimate
  proposals touch ≤ 5 targets.

HIGH — fee-on-transfer rejection (Round 2 economic F6)
- `PaymentLib.collectPayment` and `DepositManager._handleErc20Deposit`
  now reject FoT / rebasing tokens at ingress via balance-delta check.
  Without this, escrow / share accounting credits `amount` while the
  contract physically receives less, eventually bricking downstream
  transfers. Reverts with the new `FeeOnTransferTokenRejected(token)`
  error.

MEDIUM — RebasingAssetAdapter virtual offset (Round 2 economic F2)
- Apply VIRTUAL_SHARES=1e8 / VIRTUAL_ASSETS=1 (matching the staking-pool
  defenses) to the rebasing-adapter's deposit/withdraw share math. A
  one-wei seed plus a donation can no longer round a victim's later
  V-token deposit to zero shares. Adapter test fixtures updated for the
  new round-trip dust (~1 wei).

MEDIUM — Arbitrum L2 fee refund leak (Round 2 cross-chain H-1)
- New `setL2RefundAddress(address)` on `ArbitrumCrossChainMessenger`.
  Excess-fee and call-value refunds from `createRetryableTicket` route
  to the configured sweep address instead of the L1 caller's L2 alias
  (which has no receive logic, so refunds were permanently locked).

MEDIUM — beacon BSM hook bypass (Round 2 cross-chain H-5, design note)
- Documented intentional decision to NOT iterate the operator's
  blueprint list on `TangleL2Slasher.slashOperator`. Liquid-staking
  BSMs that need to react to beacon slashes must subscribe to the
  existing `BeaconSlashExecuted` event off-chain. On-chain enumeration
  would be O(N) gas-DoS — any single misbehaving BSM could grief every
  beacon slash on the chain.

MEDIUM — missing __gap on rewards UUPS (Round 2 storage F-3)
- Added `uint256[50] private __gap` to `TangleMetrics`, `RewardVaults`,
  `InflationPool`, `ServiceFeeDistributor`, `StreamingPaymentManager`.
  Without these, a future field addition risked colliding with newly-
  introduced parent contracts.

LOW — ServiceFeeDistributor reentrancy review (Slither false positive)
- Slither flagged `_claimAllForToken` as state-after-external-call inside
  loop. Verified non-exploitable: every reaching entry point
  (`claimFor`/`claimAll`/`claimAllBatch`) is `nonReentrant`. Documented
  inline.

Tests
- New `test/security/StorageLayoutSnapshotTest.t.sol` pins critical
  storage slot positions for `Tangle` and verifies the OZ ERC-7201
  namespaced slots (`Initializable`, `AccessControl`,
  `ReentrancyGuard`) match v5.1.0. Round 2 storage F-1 / F-2 flagged
  upgrade-time field-reorder risks; this catches drift in CI.
- `test_DisputeSlash_BondRefundedOnCancel` updated to drive the new
  pull-pattern flow.
- 3 RebasingAdapter tests updated for virtual-offset math.
- 3 BLS E2E tests updated for the new aggregation message format.

Bindings v0.14.0
- Regenerated via `cargo xtask gen-bindings`. CHANGELOG details every
  fix and the BREAKING changes (burn disabled, proposal limits,
  aggregation message, FoT rejection).

1398/1398 forge tests pass on this branch (heavy fuzz / invariant
suites excluded; same exclusions as Round 2 PR #125).

Findings still architecturally pending (each genuinely needs design
discussion, not in scope here):
- Round 2 cross-chain C-3: receiver-rotation replay (epoch versioning)
- Round 2 economic F5: billSubscription stake-JIT (TWAP design)
- Round 1 staking S-1: LDV redeem controller scoping (interface change)
- Round 1 staking S-2: registerAdapter migration logic
- Round 1 G-02: ValidatorPodManager._slash share-based pool migration
@drewstone drewstone merged commit 98a5484 into main May 8, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant