Pre-deploy review pass 2: 8 findings on indexer / deploy scripts / frontend (3 medium, 5 low) #138

Closed
opened 2026-05-04 08:44:27 +00:00 by Brouie · 12 comments
Brouie commented 2026-05-04 08:44:27 +00:00 (Migrated from gitlab.com)

second pass on the broader stack — indexer, deploy scripts, frontend, env templates, test coverage. dedup'd against the audit MD, my prior #126-#131, and the #116-#125 audit batch. these are NEW.

ordered by severity descending.


Finding 1 — Indexer: 3 events emitted by audit-batch contracts are undecoded (Medium)

indexer/src/decoder.rs has no sol! definitions and no decode arms for:

  • TimeCurve.PodiumResidualRecipientSet(address indexed recipient) — contracts/src/TimeCurve.sol:187. M-01 fix introduced this setter to direct unallocated podium residuals.
  • TimeCurveBuyRouter.EthRescued(address indexed to, uint256 amount) — contracts/src/TimeCurveBuyRouter.sol:69. M-02/#117 fix.
  • TimeCurveBuyRouter.Erc20Rescued(address indexed token, address indexed to, uint256 amount) — contracts/src/TimeCurveBuyRouter.sol:70. M-02/#117 fix. NB: lowercase casing differs from FeeRouter.ERC20Rescued (uppercase) — different topic0s, only the FeeRouter variant is decoded.

undecoded events fall to DecodedEvent::Unknown { topic0 } and are dropped by persist.rs. no idx_ table, no row in reorg.rs rollback list.

consequence: M-01 closure note frames residual routing as governed-with-audit-trail; M-02 closure same shape for ETH/ERC20 sweeps. without decoded events, the audit trail does not exist on the indexer side. ops/compliance dashboards waiting on these events get nothing. PodiumResidualRecipientSet matters most because owner can re-point the residual sink mid-sale or post-end with no on-chain delay.

mitigation: add sol! defs for the 3 events, add decode arms with the right topic0s, add 3 new idx_ tables, add INSERT branches in persist.rs, add the table names to rollback_after's array.


Finding 2 — Indexer: per-block ingestion is non-transactional, crash leaves ghost events on reorg (Medium)

indexer/src/ingestion.rs:144-162 runs three sqlx calls sequentially without a wrapping transaction:

for lg in &logs {
    ...
    persist_decoded_log(pool, &decoded).await?;   // per-event sqlx
}
upsert_indexed_block(pool, next, block_hash).await?;   // separate sqlx
save_chain_pointer(pool, &pointer).await?;             // separate sqlx

if the indexer crashes (panic, RPC error, OOM, container kill, host reboot) after persisting some/all events for block N but before save_chain_pointer, the chain_pointer stays at N-1 while events from N are already in DB.

race: on restart, next_block_to_process returns N. RPC call fetches new canonical block N. if a reorg occurred during the crash window, parent of new N is still N-1's hash (parent doesn't change on a same-height reorg) — reorg detection at ingestion.rs:115 does NOT fire. new (now-canonical) N's logs persist. ON CONFLICT DO NOTHING prevents (tx_hash, log_index) duplicates, but orphaned tx_hashes from the old reorged-out N stay in DB indefinitely as ghost rows. since primary keys are (tx_hash, log_index), the rollback logic keyed on block_number > ancestor never sees them again unless a deeper reorg happens later.

at MegaETH ~1s block time the window is narrow but real. docs/REORG_STRATEGY.md invariant 'reorged events must never stick' is violated for the crash-during-block-N case.

mitigation: wrap per-block work in a transaction:

let mut tx = pool.begin().await?;
for lg in &logs { persist_decoded_log_tx(&mut tx, &decoded).await?; }
upsert_indexed_block_tx(&mut tx, next, block_hash).await?;
save_chain_pointer_tx(&mut tx, &pointer).await?;
tx.commit().await?;

restart then sees N fully committed or not at all.


Finding 3 — Deploy script has no chainid guard, can deploy full dev stack to mainnet on RPC misconfig (Medium)

contracts/script/DeployDev.s.sol and 3 sibling dev scripts (DeployKumbayaAnvilFixtures.s.sol, AnvilSameBlockDrill.s.sol, SimulateAnvilRichState.s.sol) have no require(block.chainid == ..., ...) guard.

DeployDev.s.sol:run() unconditionally:

  • L53: deploys MockReserveCl8y — has public unauthenticated mint(address, uint256), anyone can mint unlimited supply
  • L151: tc.startSaleAt(block.timestamp + 300) — sale auto-starts in 5 minutes
  • L153-154: flips setCharmRedemptionEnabled(true) and setReservePodiumPayoutsEnabled(true) — opens issue-#55 final-signoff gates immediately
  • L171-172: presaleVesting.setClaimsEnabled(true) and startVesting() — auto-enables vesting
  • L174: tc.setDoubPresaleVesting(...) — wires +15% CHARM weight

consequence: an operator running forge script script/DeployDev.s.sol --broadcast --rpc-url <mainnet_rpc> with a real PRIVATE_KEY env (forgot to swap to prod script; CI smoke test against wrong RPC; staging job's RPC env points at mainnet by mistake) deploys a complete dev stack to mainnet — publicly-mintable reserve, 5min auto-start sale, claim gates open, vesting started, deployer holding all admin/upgrade keys. recovery = manual cleanup. the OZ pattern around L168-170 (grantRole(MINTER_ROLE, deployer); mint(...); revokeRole) also leaves the deployer with permanent burn-anyone power on Doubloon if broadcast aborts between grant and revoke (compounds with #131 finding 1).

mitigation: add at top of every run():

require(
    block.chainid == 31337 || block.chainid == 6342 || block.chainid == 6343,
    "DeployDev: dev/testnet only"
);

cheap defense, catches the misconfigured-RPC class before any tx broadcasts.


Finding 4 — Indexer .env.example uses a trivial placeholder DB password and ships testnet defaults without warnings (Low)

indexer/.env.example ships with a trivially-grep-able placeholder string as the postgres password in the DATABASE_URL template. operators using copy-this-and-fill workflows commonly forget to swap weak placeholder credentials. additionally RPC_URL and CHAIN_ID default to MegaETH testnet without comment annotation — increases mis-pointed deploy risk.

mitigation:

  • change the placeholder password to an obviously-must-replace string like CHANGE_ME_BEFORE_DEPLOY
  • add startup assertion in config.rs that refuses well-known placeholder passwords when INDEXER_PRODUCTION=1
  • add a comment block above RPC_URL/CHAIN_ID warning that defaults are testnet-only and must change before mainnet

Finding 5 — Frontend issues 9 maxUint256 infinite approvals across the user flow, multiplying H-01 blast radius (Low–Medium)

inventoried infinite approvals:

  • useTimeCurveArenaModel.tsx:2227, 2243 — WETH/USDM → SwapRouter
  • useTimeCurveArenaModel.tsx:2278, 2340 — CL8Y → TimeCurve (×2 paths)
  • useTimeCurveSaleSession.ts:985, 1001 — WETH/USDM → SwapRouter
  • useTimeCurveSaleSession.ts:1036 — CL8Y → TimeCurve
  • ReferralRegisterSection.tsx:219 — CL8Y → ReferralRegistry
  • timeCurveKumbayaSingleTx.ts:97 — token → router

= 9 distinct infinite-approval grants accumulated by a typical user across a sale.

race / MEV exposure: TimeCurve, ReferralRegistry, FeeRouter, RabbitTreasury are UUPS proxies. per audit H-01, a compromised owner can install a new TimeCurve impl whose buy(charmWad) re-uses the existing CL8Y allowance to drain the wallet (new logic transfers full allowance to attacker before the buy). the frontend's maxUint256 choice multiplies H-01 blast radius from 'amount in flight at moment of compromise' to 'full balance of every previously-connecting wallet'. no revokeApproval UX, no 'use exact amount' toggle.

mitigation: change defaults to approve(spender, exactAmount) (next call re-approves; user signs once per buy). for the high-volume CL8Y → TimeCurve path, allow opt-in infinite via checkbox with one-line disclosure: 'this gives the contract permission to spend any amount, forever; if the contract is later upgraded by its admin, that authority can be re-used'.


Finding 6 — submitBuy multi-step flow has account-switch race (fund-redirection vector) (Low–Medium)

useTimeCurveSaleSession.ts:861-1050 (submitBuy callback). address is captured at click time (L867), reused unchanged across multiple writeContractAsync + waitForTransactionReceipt awaits.

specific reuse points:

  • L978: args: [address, k.config.swapRouter] (WETH allowance read)
  • L994: args: [address, k.config.swapRouter] (USDM allowance read)
  • L1016: recipient: address (Kumbaya exactOutput swap recipient)
  • L1029: args: [address, tc] (CL8Y allowance read)

between awaits, the user can disconnect + reconnect with a different wallet (wallets like Rabby switch accounts mid-tx). subsequent writeContractAsync calls are signed by the NEW wallet, but the in-callback address variable still points to the OLD wallet.

concrete consequence: if user switches wallets between the WETH approve step and the swap step:

  • swap is signed by NEW wallet (NEW wallet pays gas + spends NEW allowance)
  • swap's recipient is hardcoded OLD wallet's address
  • bought asset (CL8Y) goes to OLD wallet
  • NEW wallet's CL8Y allowance to TimeCurve check (L1029) reads NEW wallet's allowance (OK), but the subsequent buy sends NEW wallet's CL8Y for OLD wallet's CHARM credit — OR reverts depending on which side the wallet was on for which step

not lost funds in the strict sense (each tx is signed by its actual owner) but coherent fund-tracking breaks; refund only via weth.withdraw from the wallet that holds the WETH; error surface is confusing (friendlyRevertFromUnknown doesn't disambiguate this).

mitigation: snapshot address once at top, then on each await re-read useAccount().address and abort with a recoverable error if it changed. same for chainId. on disconnect, gate any pending steps behind a re-confirmation modal.


Finding 7 — PresaleVestingPage renders raw wagmi error message, can leak RPC API key (Low)

frontend/src/pages/PresaleVestingPage.tsx:269:

{claimError ? <StatusMessage variant="error">{claimError.message}</StatusMessage> : null}

raw error.message from wagmi/viem is rendered to DOM. wagmi error messages can include the configured RPC URL, gas estimation details, and revert-data payloads.

consequence: on a deployment using a paid RPC with API key in the URL (common pattern for MegaETH partners — Alchemy / QuickNode / Helius style), the URL leaks to:

  • anyone with browser devtools / network access during a failed claim
  • screenshots taken to debug or support
  • error reporting tools (Sentry, Datadog) that capture DOM text
  • screen sharing during demos / support calls

friendlyRevertFromUnknown exists in this codebase (used by submitBuy) but is not applied here.

mitigation: route through friendlyRevertFromUnknown consistently. add a redaction pass that strips import.meta.env.VITE_RPC_URL substring before display. apply same to other write surfaces (referrals register, time curve buy) where errors render to user.


Finding 8 — Multiple critical attack scenarios have no negative-test coverage (Low aggregate)

surfaces from #131 + this pass that have no regression-pinning test:

a. Doubloon.burn(arbitrary_from) from a non-RabbitTreasury MINTER_ROLE holder (#131 finding 1). add test_burn_drains_arbitrary_holder_via_secondary_minter that grants MINTER_ROLE to a fresh address, has it burn another wallet's DOUB, asserts success.

b. WarBow steal '2x rule' fresh-wallet bypass (#131 finding 4). test_warbow_steal_drains_ten_percent_and_burns_one_reserve exercises the bypass (bob has abp=0) but doesn't label it. add test_warbow_steal_fresh_wallet_drains_high_bp_victim_for_one_cl8y documenting the economic vulnerability as a known invariant.

c. WarBow revenge collusion shielding (#131 finding 5). add test_warbow_revenge_overwritten_by_second_stealer_shields_first exercising A→V→B→V→V.warbowRevenge(B) succeeds while A is unrevengable.

d. setBuyFeeRoutingEnabled mid-sale freeze (#131 finding 2). add test_buy_fee_routing_freeze_after_buy_blocks_warbow_defense simulating owner flip during active podium battle, asserts victims cannot guard/revenge until timer expires.

e. DoubPresaleVesting excess DOUB lock (#131 finding 7). add test_excess_doub_above_total_allocated_is_unrecoverable transferring extra DOUB post-startVesting, asserts no path drains it after all beneficiaries claim.

f. Indexer reorg crash-recovery (this finding 2). integration test that persists block N events, kills indexer process or aborts the future, restarts, asserts second run produces no orphan rows.

per-gap severity is low. aggregate of 'critical adversarial behaviors not regression-pinned' is medium-relevance for a contract going to mainnet tonight.

mitigation: add the 6 tests above. they (a) lock current behavior as regression baseline, (b) make future fixes obvious, (c) document attack surface in code that runs in CI.


Pre-deploy relevance

  • deploy-tonight scope: findings 1, 2, 3 (indexer must work day 1; deploy scripts are one wrong RPC away).
  • post-deploy hardening: findings 4, 5, 6, 7, 8.

cc @PlasticDigits

second pass on the broader stack — indexer, deploy scripts, frontend, env templates, test coverage. dedup'd against the audit MD, my prior #126-#131, and the #116-#125 audit batch. these are NEW. ordered by severity descending. --- ## Finding 1 — Indexer: 3 events emitted by audit-batch contracts are undecoded (Medium) `indexer/src/decoder.rs` has no `sol!` definitions and no decode arms for: - `TimeCurve.PodiumResidualRecipientSet(address indexed recipient)` — `contracts/src/TimeCurve.sol:187`. M-01 fix introduced this setter to direct unallocated podium residuals. - `TimeCurveBuyRouter.EthRescued(address indexed to, uint256 amount)` — `contracts/src/TimeCurveBuyRouter.sol:69`. M-02/#117 fix. - `TimeCurveBuyRouter.Erc20Rescued(address indexed token, address indexed to, uint256 amount)` — `contracts/src/TimeCurveBuyRouter.sol:70`. M-02/#117 fix. NB: lowercase casing differs from `FeeRouter.ERC20Rescued` (uppercase) — different topic0s, only the FeeRouter variant is decoded. undecoded events fall to `DecodedEvent::Unknown { topic0 }` and are dropped by `persist.rs`. no idx_ table, no row in `reorg.rs` rollback list. **consequence**: M-01 closure note frames residual routing as governed-with-audit-trail; M-02 closure same shape for ETH/ERC20 sweeps. without decoded events, the audit trail does not exist on the indexer side. ops/compliance dashboards waiting on these events get nothing. `PodiumResidualRecipientSet` matters most because owner can re-point the residual sink mid-sale or post-end with no on-chain delay. **mitigation**: add `sol!` defs for the 3 events, add decode arms with the right topic0s, add 3 new `idx_` tables, add INSERT branches in `persist.rs`, add the table names to `rollback_after`'s array. --- ## Finding 2 — Indexer: per-block ingestion is non-transactional, crash leaves ghost events on reorg (Medium) `indexer/src/ingestion.rs:144-162` runs three sqlx calls sequentially without a wrapping transaction: ``` for lg in &logs { ... persist_decoded_log(pool, &decoded).await?; // per-event sqlx } upsert_indexed_block(pool, next, block_hash).await?; // separate sqlx save_chain_pointer(pool, &pointer).await?; // separate sqlx ``` if the indexer crashes (panic, RPC error, OOM, container kill, host reboot) after persisting some/all events for block N but before `save_chain_pointer`, the chain_pointer stays at N-1 while events from N are already in DB. **race**: on restart, `next_block_to_process` returns N. RPC call fetches new canonical block N. if a reorg occurred during the crash window, parent of new N is still N-1's hash (parent doesn't change on a same-height reorg) — reorg detection at `ingestion.rs:115` does NOT fire. new (now-canonical) N's logs persist. `ON CONFLICT DO NOTHING` prevents `(tx_hash, log_index)` duplicates, but orphaned tx_hashes from the old reorged-out N stay in DB indefinitely as ghost rows. since primary keys are `(tx_hash, log_index)`, the rollback logic keyed on `block_number > ancestor` never sees them again unless a deeper reorg happens later. at MegaETH ~1s block time the window is narrow but real. `docs/REORG_STRATEGY.md` invariant 'reorged events must never stick' is violated for the crash-during-block-N case. **mitigation**: wrap per-block work in a transaction: ```rust let mut tx = pool.begin().await?; for lg in &logs { persist_decoded_log_tx(&mut tx, &decoded).await?; } upsert_indexed_block_tx(&mut tx, next, block_hash).await?; save_chain_pointer_tx(&mut tx, &pointer).await?; tx.commit().await?; ``` restart then sees N fully committed or not at all. --- ## Finding 3 — Deploy script has no chainid guard, can deploy full dev stack to mainnet on RPC misconfig (Medium) `contracts/script/DeployDev.s.sol` and 3 sibling dev scripts (`DeployKumbayaAnvilFixtures.s.sol`, `AnvilSameBlockDrill.s.sol`, `SimulateAnvilRichState.s.sol`) have no `require(block.chainid == ..., ...)` guard. `DeployDev.s.sol:run()` unconditionally: - L53: deploys `MockReserveCl8y` — has public unauthenticated `mint(address, uint256)`, anyone can mint unlimited supply - L151: `tc.startSaleAt(block.timestamp + 300)` — sale auto-starts in 5 minutes - L153-154: flips `setCharmRedemptionEnabled(true)` and `setReservePodiumPayoutsEnabled(true)` — opens issue-#55 final-signoff gates immediately - L171-172: `presaleVesting.setClaimsEnabled(true)` and `startVesting()` — auto-enables vesting - L174: `tc.setDoubPresaleVesting(...)` — wires +15% CHARM weight **consequence**: an operator running `forge script script/DeployDev.s.sol --broadcast --rpc-url <mainnet_rpc>` with a real `PRIVATE_KEY` env (forgot to swap to prod script; CI smoke test against wrong RPC; staging job's RPC env points at mainnet by mistake) deploys a complete dev stack to mainnet — publicly-mintable reserve, 5min auto-start sale, claim gates open, vesting started, deployer holding all admin/upgrade keys. recovery = manual cleanup. the OZ pattern around L168-170 (`grantRole(MINTER_ROLE, deployer); mint(...); revokeRole`) also leaves the deployer with permanent burn-anyone power on Doubloon if broadcast aborts between grant and revoke (compounds with #131 finding 1). **mitigation**: add at top of every `run()`: ```solidity require( block.chainid == 31337 || block.chainid == 6342 || block.chainid == 6343, "DeployDev: dev/testnet only" ); ``` cheap defense, catches the misconfigured-RPC class before any tx broadcasts. --- ## Finding 4 — Indexer .env.example uses a trivial placeholder DB password and ships testnet defaults without warnings (Low) `indexer/.env.example` ships with a trivially-grep-able placeholder string as the postgres password in the `DATABASE_URL` template. operators using copy-this-and-fill workflows commonly forget to swap weak placeholder credentials. additionally `RPC_URL` and `CHAIN_ID` default to MegaETH testnet without comment annotation — increases mis-pointed deploy risk. **mitigation**: - change the placeholder password to an obviously-must-replace string like `CHANGE_ME_BEFORE_DEPLOY` - add startup assertion in `config.rs` that refuses well-known placeholder passwords when `INDEXER_PRODUCTION=1` - add a comment block above RPC_URL/CHAIN_ID warning that defaults are testnet-only and must change before mainnet --- ## Finding 5 — Frontend issues 9 `maxUint256` infinite approvals across the user flow, multiplying H-01 blast radius (Low–Medium) inventoried infinite approvals: - `useTimeCurveArenaModel.tsx:2227, 2243` — WETH/USDM → SwapRouter - `useTimeCurveArenaModel.tsx:2278, 2340` — CL8Y → TimeCurve (×2 paths) - `useTimeCurveSaleSession.ts:985, 1001` — WETH/USDM → SwapRouter - `useTimeCurveSaleSession.ts:1036` — CL8Y → TimeCurve - `ReferralRegisterSection.tsx:219` — CL8Y → ReferralRegistry - `timeCurveKumbayaSingleTx.ts:97` — token → router = 9 distinct infinite-approval grants accumulated by a typical user across a sale. **race / MEV exposure**: TimeCurve, ReferralRegistry, FeeRouter, RabbitTreasury are UUPS proxies. per audit H-01, a compromised owner can install a new TimeCurve impl whose `buy(charmWad)` re-uses the existing CL8Y allowance to drain the wallet (new logic transfers full allowance to attacker before the buy). the frontend's `maxUint256` choice multiplies H-01 blast radius from 'amount in flight at moment of compromise' to 'full balance of every previously-connecting wallet'. no `revokeApproval` UX, no 'use exact amount' toggle. **mitigation**: change defaults to `approve(spender, exactAmount)` (next call re-approves; user signs once per buy). for the high-volume CL8Y → TimeCurve path, allow opt-in infinite via checkbox with one-line disclosure: 'this gives the contract permission to spend any amount, forever; if the contract is later upgraded by its admin, that authority can be re-used'. --- ## Finding 6 — submitBuy multi-step flow has account-switch race (fund-redirection vector) (Low–Medium) `useTimeCurveSaleSession.ts:861-1050` (`submitBuy` callback). `address` is captured at click time (L867), reused unchanged across multiple `writeContractAsync` + `waitForTransactionReceipt` awaits. specific reuse points: - L978: `args: [address, k.config.swapRouter]` (WETH allowance read) - L994: `args: [address, k.config.swapRouter]` (USDM allowance read) - L1016: `recipient: address` (Kumbaya `exactOutput` swap recipient) - L1029: `args: [address, tc]` (CL8Y allowance read) between awaits, the user can disconnect + reconnect with a different wallet (wallets like Rabby switch accounts mid-tx). subsequent `writeContractAsync` calls are signed by the NEW wallet, but the in-callback `address` variable still points to the OLD wallet. **concrete consequence**: if user switches wallets between the WETH approve step and the swap step: - swap is signed by NEW wallet (NEW wallet pays gas + spends NEW allowance) - swap's `recipient` is hardcoded OLD wallet's address - bought asset (CL8Y) goes to OLD wallet - NEW wallet's CL8Y allowance to TimeCurve check (L1029) reads NEW wallet's allowance (OK), but the subsequent buy sends NEW wallet's CL8Y for OLD wallet's CHARM credit — OR reverts depending on which side the wallet was on for which step **not lost funds in the strict sense** (each tx is signed by its actual owner) but coherent fund-tracking breaks; refund only via `weth.withdraw` from the wallet that holds the WETH; error surface is confusing (`friendlyRevertFromUnknown` doesn't disambiguate this). **mitigation**: snapshot `address` once at top, then on each `await` re-read `useAccount().address` and abort with a recoverable error if it changed. same for `chainId`. on disconnect, gate any pending steps behind a re-confirmation modal. --- ## Finding 7 — PresaleVestingPage renders raw wagmi error message, can leak RPC API key (Low) `frontend/src/pages/PresaleVestingPage.tsx:269`: ```tsx {claimError ? <StatusMessage variant="error">{claimError.message}</StatusMessage> : null} ``` raw `error.message` from wagmi/viem is rendered to DOM. wagmi error messages can include the configured RPC URL, gas estimation details, and revert-data payloads. **consequence**: on a deployment using a paid RPC with API key in the URL (common pattern for MegaETH partners — Alchemy / QuickNode / Helius style), the URL leaks to: - anyone with browser devtools / network access during a failed claim - screenshots taken to debug or support - error reporting tools (Sentry, Datadog) that capture DOM text - screen sharing during demos / support calls `friendlyRevertFromUnknown` exists in this codebase (used by submitBuy) but is not applied here. **mitigation**: route through `friendlyRevertFromUnknown` consistently. add a redaction pass that strips `import.meta.env.VITE_RPC_URL` substring before display. apply same to other write surfaces (referrals register, time curve buy) where errors render to user. --- ## Finding 8 — Multiple critical attack scenarios have no negative-test coverage (Low aggregate) surfaces from #131 + this pass that have no regression-pinning test: a. `Doubloon.burn(arbitrary_from)` from a non-RabbitTreasury MINTER_ROLE holder (#131 finding 1). add `test_burn_drains_arbitrary_holder_via_secondary_minter` that grants MINTER_ROLE to a fresh address, has it burn another wallet's DOUB, asserts success. b. WarBow steal '2x rule' fresh-wallet bypass (#131 finding 4). `test_warbow_steal_drains_ten_percent_and_burns_one_reserve` exercises the bypass (bob has `abp=0`) but doesn't label it. add `test_warbow_steal_fresh_wallet_drains_high_bp_victim_for_one_cl8y` documenting the economic vulnerability as a known invariant. c. WarBow revenge collusion shielding (#131 finding 5). add `test_warbow_revenge_overwritten_by_second_stealer_shields_first` exercising A→V→B→V→`V.warbowRevenge(B)` succeeds while A is unrevengable. d. `setBuyFeeRoutingEnabled` mid-sale freeze (#131 finding 2). add `test_buy_fee_routing_freeze_after_buy_blocks_warbow_defense` simulating owner flip during active podium battle, asserts victims cannot guard/revenge until timer expires. e. DoubPresaleVesting excess DOUB lock (#131 finding 7). add `test_excess_doub_above_total_allocated_is_unrecoverable` transferring extra DOUB post-startVesting, asserts no path drains it after all beneficiaries claim. f. Indexer reorg crash-recovery (this finding 2). integration test that persists block N events, kills indexer process or aborts the future, restarts, asserts second run produces no orphan rows. per-gap severity is low. aggregate of 'critical adversarial behaviors not regression-pinned' is medium-relevance for a contract going to mainnet tonight. **mitigation**: add the 6 tests above. they (a) lock current behavior as regression baseline, (b) make future fixes obvious, (c) document attack surface in code that runs in CI. --- ## Pre-deploy relevance - **deploy-tonight scope**: findings 1, 2, 3 (indexer must work day 1; deploy scripts are one wrong RPC away). - **post-deploy hardening**: findings 4, 5, 6, 7, 8. cc @PlasticDigits
PlasticDigits commented 2026-05-04 08:51:53 +00:00 (Migrated from gitlab.com)

mentioned in issue #139

mentioned in issue #139
PlasticDigits commented 2026-05-04 08:51:56 +00:00 (Migrated from gitlab.com)

mentioned in issue #140

mentioned in issue #140
PlasticDigits commented 2026-05-04 08:51:58 +00:00 (Migrated from gitlab.com)

mentioned in issue #141

mentioned in issue #141
PlasticDigits commented 2026-05-04 08:52:01 +00:00 (Migrated from gitlab.com)

mentioned in issue #142

mentioned in issue #142
PlasticDigits commented 2026-05-04 08:52:21 +00:00 (Migrated from gitlab.com)

mentioned in issue #143

mentioned in issue #143
PlasticDigits commented 2026-05-04 08:52:24 +00:00 (Migrated from gitlab.com)

mentioned in issue #144

mentioned in issue #144
PlasticDigits commented 2026-05-04 08:52:27 +00:00 (Migrated from gitlab.com)

mentioned in issue #145

mentioned in issue #145
PlasticDigits commented 2026-05-04 08:52:44 +00:00 (Migrated from gitlab.com)

mentioned in issue #146

mentioned in issue #146
PlasticDigits commented 2026-05-04 08:53:12 +00:00 (Migrated from gitlab.com)

Spawned 8 detailed follow-ups with references, recommended fixes, acceptance criteria, and verification checklists for each Finding 1–8 from this pre-deploy review pass:

Parent triage can track deploy-tonight scope via #139–#141; #142–#146 align with post-deploy hardening per the pass-2 note. Closing this umbrella now that work is decomposed.

Spawned **8** detailed follow-ups with references, recommended fixes, acceptance criteria, and verification checklists for each Finding 1–8 from this pre-deploy review pass: - Finding 1 → #139 - Finding 2 → #140 - Finding 3 → #141 - Finding 4 → #142 - Finding 5 → #143 - Finding 6 → #144 - Finding 7 → #145 - Finding 8 → #146 Parent triage can track **deploy-tonight** scope via #139–#141; **#142–#146** align with post-deploy hardening per the pass-2 note. Closing this umbrella now that work is decomposed.
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-05-04 08:53:26 +00:00
Brouie commented 2026-05-05 02:21:46 +00:00 (Migrated from gitlab.com)

mentioned in issue #148

mentioned in issue #148
Brouie commented 2026-05-05 02:49:22 +00:00 (Migrated from gitlab.com)

mentioned in issue #149

mentioned in issue #149
Brouie commented 2026-05-06 05:53:21 +00:00 (Migrated from gitlab.com)

mentioned in issue #168

mentioned in issue #168
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
code/yieldomega#138
No description provided.