diff --git a/scripts/harb-evaluator/scenarios/passive-confidence/no-dilution.spec.ts b/scripts/harb-evaluator/scenarios/passive-confidence/no-dilution.spec.ts new file mode 100644 index 0000000..c563977 --- /dev/null +++ b/scripts/harb-evaluator/scenarios/passive-confidence/no-dilution.spec.ts @@ -0,0 +1,140 @@ +/** + * Holdout scenario: passive-confidence / no-dilution + * + * Verifies that a passive holder's balance is not diluted when a new buyer enters. + * Two wallets buy KRK sequentially. The first buyer's balance must remain exactly + * the same after the second buyer purchases. The second buyer should receive fewer + * tokens per ETH due to price impact from the AMM curve. + * + * Uses Anvil accounts 4 and 5 (distinct from e2e tests which use account 0). + */ +import { expect, test } from '@playwright/test'; +import { Wallet } from 'ethers'; +import { createWalletContext } from '../../../../tests/setup/wallet-provider'; +import { getStackConfig } from '../../../../tests/setup/stack'; +import { connectWallet, getKrkBalance } from '../../helpers/wallet'; +import { buyKrk } from '../../helpers/swap'; + +// Anvil account 4 +const PK_A = '0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a'; +const ADDRESS_A = new Wallet(PK_A).address; // 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 + +// Anvil account 5 +const PK_B = '0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba'; +const ADDRESS_B = new Wallet(PK_B).address; // 0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc + +test('passive holders are not diluted', async ({ browser }) => { + const config = getStackConfig(); + + // ── 0. Verify both accounts start with 0 KRK ────────────────────────── + const krkInitialA = await getKrkBalance(config.rpcUrl, config.contracts.Kraiken, ADDRESS_A); + const krkInitialB = await getKrkBalance(config.rpcUrl, config.contracts.Kraiken, ADDRESS_B); + + console.log(`[TEST] Wallet A initial KRK: ${krkInitialA}`); + console.log(`[TEST] Wallet B initial KRK: ${krkInitialB}`); + + expect(krkInitialA).toBe(0n); + expect(krkInitialB).toBe(0n); + console.log('[TEST] ✅ Both wallets start with 0 KRK'); + + // ── 1. Wallet A buys 1 ETH worth of KRK ──────────────────────────────── + console.log('[TEST] Creating wallet context for Wallet A...'); + const ctxA = await createWalletContext(browser, { + privateKey: PK_A, + rpcUrl: config.rpcUrl, + }); + const pageA = await ctxA.newPage(); + + pageA.on('console', msg => console.log(`[BROWSER A] ${msg.type()}: ${msg.text()}`)); + pageA.on('pageerror', err => console.log(`[BROWSER ERROR A] ${err.message}`)); + + try { + console.log('[TEST] Loading web app for Wallet A...'); + await pageA.goto(`${config.webAppUrl}/app/`, { waitUntil: 'domcontentloaded' }); + await expect(pageA.locator('.navbar-title').first()).toBeVisible({ timeout: 30_000 }); + + console.log('[TEST] Connecting Wallet A...'); + await connectWallet(pageA); + + console.log('[TEST] Wallet A buying 1 ETH of KRK...'); + await buyKrk(pageA, '1'); + + const krkBalanceA = await getKrkBalance(config.rpcUrl, config.contracts.Kraiken, ADDRESS_A); + console.log(`[TEST] Wallet A KRK balance after buy: ${krkBalanceA}`); + expect(krkBalanceA).toBeGreaterThan(0n); + console.log('[TEST] ✅ Wallet A received KRK'); + + // ── 2. Record A's balance and close context ─────────────────────────── + const krkAfterFirstBuy = krkBalanceA; + console.log(`[TEST] Recorded Wallet A balance: ${krkAfterFirstBuy}`); + } finally { + await ctxA.close(); + console.log('[TEST] Closed Wallet A context'); + } + + // Re-query A's balance after closing context to ensure we have the final value + const krkAfterFirstBuy = await getKrkBalance(config.rpcUrl, config.contracts.Kraiken, ADDRESS_A); + console.log(`[TEST] Wallet A final balance after first buy: ${krkAfterFirstBuy}`); + + // ── 3. Wallet B buys 5 ETH worth of KRK ──────────────────────────────── + console.log('[TEST] Creating wallet context for Wallet B...'); + const ctxB = await createWalletContext(browser, { + privateKey: PK_B, + rpcUrl: config.rpcUrl, + }); + const pageB = await ctxB.newPage(); + + pageB.on('console', msg => console.log(`[BROWSER B] ${msg.type()}: ${msg.text()}`)); + pageB.on('pageerror', err => console.log(`[BROWSER ERROR B] ${err.message}`)); + + try { + console.log('[TEST] Loading web app for Wallet B...'); + await pageB.goto(`${config.webAppUrl}/app/`, { waitUntil: 'domcontentloaded' }); + await expect(pageB.locator('.navbar-title').first()).toBeVisible({ timeout: 30_000 }); + + console.log('[TEST] Connecting Wallet B...'); + await connectWallet(pageB); + + console.log('[TEST] Wallet B buying 5 ETH of KRK...'); + await buyKrk(pageB, '5'); + + const krkBalanceB = await getKrkBalance(config.rpcUrl, config.contracts.Kraiken, ADDRESS_B); + console.log(`[TEST] Wallet B KRK balance after buy: ${krkBalanceB}`); + expect(krkBalanceB).toBeGreaterThan(0n); + console.log('[TEST] ✅ Wallet B received KRK'); + } finally { + await ctxB.close(); + console.log('[TEST] Closed Wallet B context'); + } + + // ── 4. Verify A's balance unchanged (no dilution) ────────────────────── + const krkBalanceAFinal = await getKrkBalance(config.rpcUrl, config.contracts.Kraiken, ADDRESS_A); + console.log(`[TEST] Wallet A balance after Wallet B buy: ${krkBalanceAFinal}`); + console.log(`[TEST] Expected: ${krkAfterFirstBuy}`); + + expect(krkBalanceAFinal).toBe(krkAfterFirstBuy); + console.log('[TEST] ✅ Wallet A balance unchanged — no dilution'); + + // ── 5. Verify B got fewer tokens per ETH due to price impact ─────────── + const krkBalanceBFinal = await getKrkBalance(config.rpcUrl, config.contracts.Kraiken, ADDRESS_B); + + const tokensPerEthA = krkAfterFirstBuy / 1n; // Bought with 1 ETH + const tokensPerEthB = krkBalanceBFinal / 5n; // Bought with 5 ETH + + console.log(`[TEST] Wallet A tokens per ETH: ${tokensPerEthA}`); + console.log(`[TEST] Wallet B tokens per ETH: ${tokensPerEthB}`); + + expect(tokensPerEthB).toBeLessThan(tokensPerEthA); + console.log('[TEST] ✅ Wallet B got fewer tokens per ETH (price impact verified)'); + + // ── Summary ───────────────────────────────────────────────────────────── + console.log(''); + console.log('═══════════════════════════════════════════════════════════'); + console.log(' PASSIVE CONFIDENCE: NO DILUTION TEST RESULTS'); + console.log('═══════════════════════════════════════════════════════════'); + console.log(` Wallet A (1 ETH): ${krkAfterFirstBuy} KRK (${tokensPerEthA} per ETH)`); + console.log(` Wallet B (5 ETH): ${krkBalanceBFinal} KRK (${tokensPerEthB} per ETH)`); + console.log(` A's balance after: ${krkBalanceAFinal} KRK (unchanged ✓)`); + console.log(` Price impact: ${((1n - tokensPerEthB * 10000n / tokensPerEthA) / 100n)}% worse for B`); + console.log('═══════════════════════════════════════════════════════════'); +});