Skip to content

Commit 5201cf0

Browse files
authored
chore(audit): round 2 red-team fixes + bindings v0.13.0 (#125)
Round 2 ran five adversarial red-team agents with explicit attacker mindsets (economic / MEV searcher, operator collusion, governance takeover, cross-chain bridge, storage / upgrade / init) and Slither static analysis. ~50 new findings on top of Round 1; this commit lands the small-but-critical fixes that don't require interface-level redesign. CRITICAL — beacon SSZ endianness (B-01) - `BeaconChainProofs.getEffectiveBalanceGwei`, `getActivationEpoch`, `getExitEpoch`, `getWithdrawableEpoch`, and `_extractBalanceFromLeaf` now perform the correct little-endian byte-swap on SSZ-packed uint64 fields. The consensus layer packs values into the HIGH 8 bytes of a bytes32 chunk in little-endian; the previous code read the LOW 64 bits as if big-endian, returning 0 (or a byte-swapped wrong value) for every effective balance, exit epoch, and validator balance proven from a real EigenPod-CLI fixture. In-tree tests passed only because the fixtures mirrored the same wrong packing. Fixtures are now SSZ-correct and `test_extractBalance_RealSszLeaf_32ETH` pins the canonical 32-ETH encoding. CRITICAL — TNTLockFactory delegate-on-init airdrop capture (gov #1) - `getOrCreateLock` requires `msg.sender == beneficiary`. Without the gate, anyone could front-run the victim's first interaction with the deterministic lock, supply themselves as `delegatee`, and persistently capture every future inbound TNT transfer's voting power. HIGH — JobQuoteDetails missing requester (Round 2 economic F1, BREAKING) - `Types.JobQuoteDetails` now carries `address requester` and the `JOB_QUOTE_TYPEHASH` includes it. Any `_permittedCaller` (or anyone watching the mempool) could previously lift another caller's signed per-job RFQ quote and consume it for themselves. Off-chain signers MUST update. HIGH — wildcard quote rejection - `verifyQuoteBatch` rejects `requester == address(0)` outright. Wildcard quotes have no good production use case and let mempool observers race the legitimate caller for the single-use digest. HIGH — slash dispute dead-zone (Round 2 economic F4) - `SlashingLib.disputeSlash` window extends through `executeAfter + TIMESTAMP_BUFFER`, mirroring `isExecutable`. The prior asymmetry created a deterministic 15-second window where a sequencer could land an operator's dispute (revert) and then 15s later anyone could call `executeSlash`. Operator dispute and execute now use the same buffer. HIGH — SLASH_ADMIN self-dispute (Round 2 governance #4) - A SLASH_ADMIN that is also the proposer can no longer self-dispute. Without this, a single role-holder could propose, immediately self-dispute (no bond), and freeze operator stake for the full `disputeResolutionDeadline` window — and (when treasury == admin) capture the operator's bond on auto-execution. HIGH — L2 slash CEI (Round 1 deferred S-1) - `L2SlashingReceiver._handleSlashMessage` now applies the slash BEFORE consuming the nonce. If `canSlash` returns false (paused, unknown operator, etc.) or `slashBps == 0`, the call reverts so the bridge keeps the message available for retry. Previously the nonce was consumed first and the slash silently dropped on transient failure, locking that slash out forever. HIGH — L2 setMessenger / setSlasher timelock (Round 2 cross-chain C-2) - Both swaps now require `SENDER_ACTIVATION_DELAY` (2 days) for non-bootstrap changes. A compromised owner could otherwise hot-swap the messenger to a contract they control and impersonate any previously-authorised sender, undercutting the H-4 timelock on `authorizedSenders`. The first swap (when current is unset) is a bootstrap exemption so deploy scripts can wire the bridge without deadlocking. MEDIUM — MBSM grace-period pinning (Round 1 deferred gov H-3) - `MBSMRegistry.pinBlueprint` rejects revisions already scheduled for deprecation. Pinning during the grace window meant `getMBSM` returned `address(0)` the moment `completeDeprecation` ran, breaking every BSM call for the pinned blueprint. MEDIUM — forceRemoveOperator min-operators floor (Round 2 collusion #7) - A blueprint manager can no longer evict operators below `minOperators` unless their BSM explicitly opts in via the new `forceRemoveAllowsBelowMin(serviceId)` hook. The previous unconditional bypass let a malicious BSM bias the operator set toward sybils. Default implementation in `BlueprintServiceManagerBase` returns `false`, enforcing the floor. MEDIUM — slash `proposeSlash` rejects `bytes32(0)` evidence (Round 1) - Off-chain monitors keying off non-zero evidence no longer see silently-zero entries. LOW — `Types.ServiceRequest.activated` moved to end of struct - Upgrade-safety: appending the field at the end means a hypothetical upgrade from a pre-`activated` storage layout cannot accidentally read a non-zero byte from a different field as `activated == true`. Test suite - `test/security/AuditFixesTest.t.sol` adds `disputeSlash` admin self- dispute regression test on top of the Round 1 entries. - New `test_extractBalance_RealSszLeaf_32ETH` pins SSZ uint64 decoding. - `BeaconTestBase._generateValidatorFields` and `_generateBalanceRoot` now use SSZ-correct packing via the `_sszUint64` / `_reverseUint64` helpers. - Updated quote-typehash literals + signing helpers across every test file that constructs `QuoteDetails` / `JobQuoteDetails`. - EIP712 cross-repo deterministic vectors regenerated for the new typehash. Rust `pricing-engine/tests/eip712_compat.rs` MUST be regenerated to match. Bindings - Regenerated via `cargo xtask gen-bindings`. Bumped to 0.13.0 (BREAKING: typehash changes; consumers MUST update their EIP-712 payloads). - Slither sweep: 1279 results, mostly unused-state-variable + cache-array-length + constable-states (all informational on upgradeable contracts). Two real findings flagged for follow-up: `ServiceFeeDistributor._claimAllForToken` reentrancy-eth pattern (state-after-external-call inside a nonReentrant-guarded entry, needs CEI review) and `RebasingAssetAdapter` first-depositor inflation surface (needs virtual-offset migration; deferred). Findings deferred for follow-up PRs (out of scope here) - Cross-chain C-3: receiver-rotation replay across re-deployments. - Cross-chain H-1: Arbitrum L2 fee leak to inaccessible alias. - Cross-chain H-5: BSM `onSlash` bypass on beacon-slash path. - Cross-chain H-6: multi-pod operator under-slash by ~9-15%. - Operator collusion 2c: bitmap snapshot binding for aggregated jobs (swap-and-pop reorder breaks in-flight signatures). - Economic F2: `RebasingAssetAdapter` virtual-offset migration. - Economic F3: `cancelSlash` bond-refund reentrancy via disputer. - Economic F5: stake-just-in-time `billSubscription` arbitrage. - Economic F6: fee-on-transfer / rebasing token escrow drift. - Storage F-3: missing `__gap` on five rewards/* UUPS implementations. - Governance #5: ERC20Burnable burn-before-snapshot quorum suppression. - Governance #8: 50-action proposals + low threshold = privileged-call obfuscation surface. - `ServiceFeeDistributor._claimAllForToken` reentrancy review. The two governance upgrade tests pre-existing on origin/main (`test_GovernorSelfUpgrade`, `test_TokenUpgradeViaGovernance`) remain broken; not introduced by this PR.
1 parent 23bb5f6 commit 5201cf0

44 files changed

Lines changed: 1066 additions & 358 deletions

Some content is hidden

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

bindings/CHANGELOG.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,93 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.13.0] - 2026-05-08
11+
12+
### Changed (BREAKING)
13+
14+
- `JobQuoteDetails` now includes `address requester` as the first field, mirroring
15+
the v0.12.0 fix on `QuoteDetails`. The per-job RFQ quote was previously not
16+
bound to a consumer at the EIP-712 typehash level, so any `_permittedCaller`
17+
(or anyone watching the mempool) could lift another caller's signed quote
18+
digest and consume it for themselves. Off-chain signers MUST add `requester`
19+
to the `JobQuoteDetails` typed data; the new typehash string is:
20+
`"JobQuoteDetails(address requester,uint64 serviceId,uint8 jobIndex,uint256 price,uint64 timestamp,uint64 expiry,uint8 confidentiality)"`
21+
- `verifyQuoteBatch` now rejects wildcard `requester == address(0)` quotes
22+
outright. Any operator software that previously emitted wildcard quotes will
23+
fail and must issue per-caller quotes (or batch them via `permittedCallers`
24+
at request time).
25+
- `Types.ServiceRequest.activated` field moved to the END of the struct so a
26+
hypothetical upgrade from a pre-`activated` storage layout cannot
27+
accidentally read a non-zero byte from a different field as
28+
`activated == true`.
29+
30+
### Fixed (security — Round 2)
31+
32+
- **Beacon SSZ endianness (B-01, mainnet blocker)**:
33+
`BeaconChainProofs.getEffectiveBalanceGwei`, `getActivationEpoch`,
34+
`getExitEpoch`, `getWithdrawableEpoch`, and `_extractBalanceFromLeaf` now
35+
perform the correct little-endian byte-swap on SSZ-packed uint64 fields.
36+
Previously they read the LOW 64 bits of a `bytes32` chunk while the
37+
consensus layer packs values into the HIGH 8 bytes of the chunk in
38+
little-endian. Real EigenPod proofs would silently mis-account every
39+
uint64 field — every effective balance, exit epoch, and validator
40+
balance would be returned as 0 (or a byte-swapped wrong value). Tests
41+
passed because the in-tree fixtures mirrored the same wrong packing;
42+
fixtures are now SSZ-correct and there's a regression test pinning the
43+
canonical 32-ETH leaf.
44+
- **Slash dispute dead-zone (Round 2 economic-MEV F4)**: `disputeSlash`'s
45+
window now extends through `executeAfter + TIMESTAMP_BUFFER`, mirroring
46+
`isExecutable`. The previous asymmetry created a deterministic 15-second
47+
window where a sequencer could land an operator's dispute tx (revert,
48+
`DisputeWindowPassed`) and then 15s later anyone could call
49+
`executeSlash` (now eligible). Operator dispute and execute both now use
50+
the same buffer.
51+
- **SLASH_ADMIN self-dispute (Round 2 governance #4)**: a SLASH_ADMIN that
52+
is also the proposer of a slash can no longer self-dispute their own
53+
slash. Without this, a single role-holder could propose, immediately
54+
self-dispute (no bond), and freeze operator stake for the full
55+
`disputeResolutionDeadline` window AND (when treasury == admin) capture
56+
the operator's bond on auto-execution.
57+
- **L2 slash CEI (Round 1 deferred S-1)**: `L2SlashingReceiver` now
58+
applies the slash BEFORE consuming the nonce. If `canSlash` returns
59+
false (paused, unknown operator, etc.) or `slashBps == 0`, the call
60+
reverts so the bridge keeps the message available for retry. Previously
61+
the nonce was consumed first and the slash silently dropped on transient
62+
failure, locking that slash out forever.
63+
- **L2 setMessenger / setSlasher timelock (Round 2 cross-chain C-2)**: both
64+
swaps now require `SENDER_ACTIVATION_DELAY` (2 days) for non-bootstrap
65+
changes. Without this, a compromised owner could hot-swap to a messenger
66+
they control and immediately impersonate any previously-authorised
67+
sender, undercutting the H-4 timelock on `authorizedSenders`. The first
68+
swap (when current is unset) is a bootstrap exemption so deploy scripts
69+
can wire the bridge without a 2-day deadlock.
70+
- **TNTLockFactory delegate-on-init airdrop capture (Round 2 governance
71+
#1, CRITICAL)**: `getOrCreateLock` now requires `msg.sender ==
72+
beneficiary`. Without this gate, a third party could front-run the
73+
victim's first interaction with a lock, supply themselves as
74+
`delegatee`, and persistently capture the victim's voting power for
75+
every future inbound TNT transfer to the deterministic lock address.
76+
- **MBSM grace-period pinning (Round 1 deferred gov H-3)**:
77+
`MBSMRegistry.pinBlueprint` rejects revisions that are already
78+
scheduled for deprecation. Pinning during the grace window meant
79+
`getMBSM` returned `address(0)` the moment `completeDeprecation` ran,
80+
breaking every BSM call for the pinned blueprint.
81+
- **forceRemoveOperator min-operators floor (Round 2 operator-collusion
82+
#7)**: a blueprint manager can no longer evict honest operators below
83+
`minOperators` unless their BSM explicitly opts in via the new
84+
`forceRemoveAllowsBelowMin(serviceId)` hook. The previous unconditional
85+
bypass let a malicious BSM bias the operator set toward sybils.
86+
87+
### Added
88+
89+
- `IBlueprintServiceManager.forceRemoveAllowsBelowMin(uint64) -> bool`
90+
hook. Default implementation in `BlueprintServiceManagerBase` returns
91+
`false`, enforcing the protocol-level minimum.
92+
- `L2SlashingReceiver.activateMessenger()` / `activateSlasher()` for
93+
consuming queued swaps after the timelock elapses.
94+
- `L2SlashingReceiver.SlashingNotPossible(address operator)` error,
95+
emitted when a slash arrives for an operator the slasher cannot act on.
96+
1097
## [0.12.0] - 2026-05-08
1198

1299
### Changed (BREAKING)

bindings/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "tnt-core-bindings"
3-
version = "0.12.0"
3+
version = "0.13.0"
44
edition = "2021"
55
rust-version = "1.81"
66
description = "Rust bindings for TNT Core Solidity contracts (Tangle staking protocol)"

bindings/TNT_CORE_VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
93f8398b43dea3b7a880ccb912cc785811cb254b
1+
23bb5f648b37a2166917451d3e74429725d875e4

bindings/abi/IBlueprintServiceManager.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

bindings/abi/IMultiAssetDelegation.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

bindings/abi/ITangle.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

bindings/abi/ITangleFull.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

bindings/abi/ITangleSlashing.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

bindings/abi/MultiAssetDelegation.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

bindings/abi/OperatorStatusRegistry.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)