Merge pull request 'feat(holdout): add passive-confidence/no-dilution scenario' (#437) from feat/holdout-no-dilution into master
This commit is contained in:
commit
4066ea6db4
2 changed files with 146 additions and 3 deletions
|
|
@ -92,8 +92,10 @@ export interface SellConfig {
|
||||||
* and polls eth_getFilterLogs until the event arrives, ensuring the swap has been
|
* and polls eth_getFilterLogs until the event arrives, ensuring the swap has been
|
||||||
* mined on-chain before returning. Otherwise, just waits for the UI state transition
|
* mined on-chain before returning. Otherwise, just waits for the UI state transition
|
||||||
* (caller is responsible for verification).
|
* (caller is responsible for verification).
|
||||||
|
*
|
||||||
|
* @param screenshotPrefix - Optional prefix for screenshot filenames (e.g., 'walletA', 'walletB')
|
||||||
*/
|
*/
|
||||||
export async function buyKrk(page: Page, ethAmount: string, opts?: BuyKrkOptions): Promise<void> {
|
export async function buyKrk(page: Page, ethAmount: string, opts?: BuyKrkOptions, screenshotPrefix = 'holdout'): Promise<void> {
|
||||||
console.log(`[swap] Buying KRK with ${ethAmount} ETH via get-krk page...`);
|
console.log(`[swap] Buying KRK with ${ethAmount} ETH via get-krk page...`);
|
||||||
await navigateSPA(page, '/app/get-krk');
|
await navigateSPA(page, '/app/get-krk');
|
||||||
await expect(page.getByRole('heading', { name: 'Get $KRK Tokens' })).toBeVisible({ timeout: 10_000 });
|
await expect(page.getByRole('heading', { name: 'Get $KRK Tokens' })).toBeVisible({ timeout: 10_000 });
|
||||||
|
|
@ -125,7 +127,7 @@ export async function buyKrk(page: Page, ethAmount: string, opts?: BuyKrkOptions
|
||||||
console.log(`[swap] Filter created: ${filterId}`);
|
console.log(`[swap] Filter created: ${filterId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.screenshot({ path: 'test-results/holdout-before-buy.png' });
|
await page.screenshot({ path: `test-results/${screenshotPrefix}-before-buy.png` });
|
||||||
console.log('[swap] Clicking Buy KRK...');
|
console.log('[swap] Clicking Buy KRK...');
|
||||||
await buyButton.click();
|
await buyButton.click();
|
||||||
|
|
||||||
|
|
@ -152,6 +154,7 @@ export async function buyKrk(page: Page, ethAmount: string, opts?: BuyKrkOptions
|
||||||
console.log(`[swap] Transfer event received (${logs.length} log(s))`);
|
console.log(`[swap] Transfer event received (${logs.length} log(s))`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line no-restricted-syntax -- Polling with timeout: eth_getFilterLogs is HTTP-only polling (not push). See AGENTS.md #Engineering Principles.
|
||||||
await new Promise(r => setTimeout(r, 200));
|
await new Promise(r => setTimeout(r, 200));
|
||||||
}
|
}
|
||||||
// Clean up filter
|
// Clean up filter
|
||||||
|
|
@ -161,7 +164,7 @@ export async function buyKrk(page: Page, ethAmount: string, opts?: BuyKrkOptions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.screenshot({ path: 'test-results/holdout-after-buy.png' });
|
await page.screenshot({ path: `test-results/${screenshotPrefix}-after-buy.png` });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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', undefined, 'walletA');
|
||||||
|
|
||||||
|
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', undefined, 'walletB');
|
||||||
|
|
||||||
|
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: ${((10000n - tokensPerEthB * 10000n / tokensPerEthA) / 100n)}% worse for B`);
|
||||||
|
console.log('═══════════════════════════════════════════════════════════');
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue