diff --git a/skills/bid-in-auction.md b/skills/bid-in-auction.md index 332aae8..3c99866 100644 --- a/skills/bid-in-auction.md +++ b/skills/bid-in-auction.md @@ -22,6 +22,8 @@ Sniper auction participation involves significant financial risk: 7. **Smart contract risk**: The auction contracts are unaudited for this specific deployment. Bugs or misconfigurations could result in loss of funds. +8. **Standing WETH approvals are dangerous**: A lingering WETH allowance to `SniperUtilV2` (`0x2B6cd5Be183c388Dd0074d53c52317df1414cd9f`) is the surface a "drain via standing approval" exploit targets — a sibling Clanker fork was drained this way in 2026-05. **As of this SDK version, `bidInAuction` approves only the exact `amountIn` per bid** (no multiplier), so each bid consumes the allowance fully and leaves zero residual. If you bid through an older SDK that approved a multiplier, **revoke your WETH allowance to `SniperUtilV2` before bidding again**. Never grant a max/unlimited WETH approval to these contracts. + **As an agent, you MUST:** - Clearly present these risks to the user before their first auction bid - Ask for explicit confirmation (e.g., "I understand the risks of auction sniping and want to proceed") @@ -98,7 +100,7 @@ Before bidding, understand these mechanics: - **`bidAmount`** (msg.value): ETH sent to the auction as your bid. Goes to protocol/LP. - **`amountIn`** (WETH transfer): The actual swap input. Pulled from your WETH balance via `transferFrom`. **This is separate from the bid.** -The SDK **automatically wraps ETH → WETH and approves the SniperUtilV2** if your WETH balance or allowance is insufficient. You just need enough total ETH. +The SDK **automatically wraps ETH → WETH and approves the SniperUtilV2 for the exact `amountIn`** (no multiplier — the bid's `transferFrom` consumes the full allowance, so nothing standing survives). You just need enough total ETH. ### Gas Price = Bid Encoding The bid amount is encoded in the transaction's gas price: `bidAmount = (tx.gasprice - gasPeg) × paymentPerGasUnit`. Both `maxFeePerGas` **and** `maxPriorityFeePerGas` must be set to the calculated value, otherwise Base's EIP-1559 will compute a lower effective gas price. @@ -334,7 +336,7 @@ interface BidInAuctionResult { | Max rounds | 5 | Total auction rounds per token | | Blocks between auctions | 2 | Rounds occur every 2 blocks | | Blocks before first auction | 2 | First auction = deploy block + 2 | -| Payment per gas unit | 0.0001 ETH (1e14 wei) | Converts gas delta to bid ETH | +| Payment per gas unit | 0.0001 ETH (1e14 wei) | Converts gas delta to bid ETH. *May be set to 0 by the auction owner as a security mitigation — when 0, the required bid is also 0 (auctions function as a free gas-price race; the bid-payment path is neutered).* | | Starting fee | 800,000 (80%) | Fee at auction start | | Ending fee | 400,000 (40%) | Fee floor after decay | | Decay period | 20 seconds | Time for fee to decay from start to end | diff --git a/src/client.ts b/src/client.ts index 960ab9e..68893b1 100644 --- a/src/client.ts +++ b/src/client.ts @@ -992,7 +992,21 @@ export class LiquidSDK { await this.publicClient.waitForTransactionReceipt({ hash: wrapTx }); } - // ── Auto-approve SniperUtilV2 for WETH if needed ────────────────── + // ── Auto-approve SniperUtilV2 for WETH (EXACT amount — no standing) ── + // Approves only what this bid needs. The bid's `transferFrom` consumes + // the full allowance, so nothing survives the bid → no standing WETH + // allowance for a "drain via standing approval" exploit to target. + // (Defense-in-depth after a sibling Clanker fork was drained via that + // pattern in 2026-05. The underlying protocol fix is owner setting + // `paymentPerGasUnit = 0` on the auction contract.) + // + // Trade-off: prior versions approved `amountIn * 10n` so 9 subsequent + // bids needed no approve. Sniper bots that previously relied on that + // pre-approval should call `WETH.approve(SNIPER_UTIL_V2, amountIn)` + // ahead of the auction window (and accept the brief standing-allowance + // window), OR start `bidInAuction` ~1 block earlier so the approve + // confirms in time. Existing residual allowances from older SDK + // versions are NOT touched here — holders should revoke them. const allowance = (await this.publicClient.readContract({ address: weth, abi: ERC20Abi, @@ -1005,7 +1019,7 @@ export class LiquidSDK { address: weth, abi: ERC20Abi, functionName: "approve", - args: [ADDRESSES.SNIPER_UTIL_V2, params.amountIn * 10n], + args: [ADDRESSES.SNIPER_UTIL_V2, params.amountIn], chain: base, account, });