Severity: HIGH · Contract · boundless-events + boundless-profile
Surfaced by the Scout + Almanax scans of e1f17937. Closes Almanax XOR-derived child op_ids allow crafted collisions, Global op_id allows cross-flow replay DoS, and Global op_id namespace enables id squatting DoS; also reframes the three Scout incorrect_exponentiation CRITICAL flags (the ^= is intentional XOR, not a botched power operator — the real defect is malleability).
Files
contracts/events/src/idempotency.rs:55-69 — derive_child, derive_child_indexed
contracts/profile/src/bootstrap.rs:36-48 — bootstrap_self
contracts/profile/src/storage.rs:189-200 — global OpSeen namespace
Attack path (verified against code)
- Events derives child op_ids for its cross-contract profile calls by XORing a 1-byte tag (and sub-index) into
parent[0]/parent[1]. XOR is reversible.
bootstrap_self lets any caller mark an arbitrary op_id seen in a global namespace keyed only by op_id (not caller/kind): user.require_auth() passes for the attacker's own address, then mark_seen(op_id).
- Attacker observes a pending
select_winners/grant tx carrying the parent op_id, recomputes the deterministic child ids, and front-runs bootstrap_self(attacker, child_id).
- The events tx's profile call hits
require_unseen → reverts OpAlreadySeen. Winner payout / reputation / earnings mutation is blocked. OpSeen is temporary() so it expires, but the attack is cheap and repeatable → sustained griefing of payouts.
Fix
- Replace XOR derivation with a collision-resistant, non-malleable construction:
env.crypto().sha256(parent ‖ [op_tag] ‖ [sub_idx] ‖ callee_contract_id) reduced to BytesN<32>, using the callee contract id as a domain separator.
- Namespace the profile
OpSeen set so an unprivileged bootstrap_self cannot collide with events-originated op_ids: key by (caller_domain, op_id), or require that privileged mutation op_ids are only settable by the configured events contract.
- Deleting the XOR path also clears the Scout
incorrect_exponentiation flags.
Tests
- Two distinct parents that XOR-collide must now derive distinct children.
- Attacker calls
bootstrap_self with a crafted child id; a subsequent legitimate events-orchestrated select_winners still succeeds.
- True replay of the same op_id stays rejected.
Severity: HIGH · Contract ·
boundless-events+boundless-profileSurfaced by the Scout + Almanax scans of
e1f17937. Closes AlmanaxXOR-derived child op_ids allow crafted collisions,Global op_id allows cross-flow replay DoS, andGlobal op_id namespace enables id squatting DoS; also reframes the three Scoutincorrect_exponentiationCRITICAL flags (the^=is intentional XOR, not a botched power operator — the real defect is malleability).Files
contracts/events/src/idempotency.rs:55-69—derive_child,derive_child_indexedcontracts/profile/src/bootstrap.rs:36-48—bootstrap_selfcontracts/profile/src/storage.rs:189-200— globalOpSeennamespaceAttack path (verified against code)
parent[0]/parent[1]. XOR is reversible.bootstrap_selflets any caller mark an arbitraryop_idseen in a global namespace keyed only by op_id (not caller/kind):user.require_auth()passes for the attacker's own address, thenmark_seen(op_id).select_winners/grant tx carrying the parentop_id, recomputes the deterministic child ids, and front-runsbootstrap_self(attacker, child_id).require_unseen→ revertsOpAlreadySeen. Winner payout / reputation / earnings mutation is blocked.OpSeenistemporary()so it expires, but the attack is cheap and repeatable → sustained griefing of payouts.Fix
env.crypto().sha256(parent ‖ [op_tag] ‖ [sub_idx] ‖ callee_contract_id)reduced toBytesN<32>, using the callee contract id as a domain separator.OpSeenset so an unprivilegedbootstrap_selfcannot collide with events-originated op_ids: key by(caller_domain, op_id), or require that privileged mutation op_ids are only settable by the configured events contract.incorrect_exponentiationflags.Tests
bootstrap_selfwith a crafted child id; a subsequent legitimate events-orchestratedselect_winnersstill succeeds.