harb/scripts/harb-evaluator/scenarios/sovereign-exit/always-leave.spec.ts
openhands 74a043262d feat(holdout): Add reasonable slippage assertion to always-leave scenario
- Modified sellAllKrk helper to return WETH delta received
- Added assertion: WETH received >= 90% of ETH spent (0.09 ETH minimum)
- Added log showing actual slippage percentage
- This proves 'always leave' with reasonable slippage, not just exit ability
2026-03-03 19:45:46 +00:00

80 lines
4 KiB
TypeScript

/**
* Holdout scenario: sovereign-exit / always-leave
*
* Verifies the core protocol invariant: a user can ALWAYS exit their position
* by buying KRK through the in-app swap widget and then selling it back.
*
* Reuses tests/setup/ infrastructure and the shared helpers in
* scripts/harb-evaluator/helpers/ — no inline wallet, swap, or balance logic.
*
* Account 0 from the Anvil test mnemonic is used (same as e2e tests).
* Deploy scripts also use Account 0, but each test run gets a fresh Anvil stack,
* so no collision occurs.
*/
import { expect, test } from '@playwright/test';
import { parseEther, Wallet } from 'ethers';
import { createWalletContext } from '../../../../tests/setup/wallet-provider';
import { getStackConfig } from '../../../../tests/setup/stack';
import { connectWallet, getKrkBalance } from '../../helpers/wallet';
import { buyKrk, sellAllKrk } from '../../helpers/swap';
// Anvil account 0 — same as e2e tests (deploy uses it but state is reset per stack)
const PK = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80';
const ACCOUNT_ADDRESS = new Wallet(PK).address;
test('I can always leave', async ({ browser }) => {
const config = getStackConfig();
const ctx = await createWalletContext(browser, {
privateKey: PK,
rpcUrl: config.rpcUrl,
});
const page = await ctx.newPage();
page.on('console', msg => console.log(`[BROWSER] ${msg.type()}: ${msg.text()}`));
page.on('pageerror', err => console.log(`[BROWSER ERROR] ${err.message}`));
try {
// ── 1. Load the web app ──────────────────────────────────────────────
console.log('[TEST] Loading web app...');
await page.goto(`${config.webAppUrl}/app/`, { waitUntil: 'domcontentloaded' });
await expect(page.locator('.navbar-title').first()).toBeVisible({ timeout: 30_000 });
// ── 2. Connect wallet via the UI ─────────────────────────────────────
console.log('[TEST] Connecting wallet...');
await connectWallet(page);
// ── 3. Buy KRK via the get-krk page swap widget ───────────────────────
const krkBefore = await getKrkBalance(config.rpcUrl, config.contracts.Kraiken, ACCOUNT_ADDRESS);
console.log(`[TEST] KRK balance before buy: ${krkBefore}`);
await buyKrk(page, '0.1');
const krkAfterBuy = await getKrkBalance(config.rpcUrl, config.contracts.Kraiken, ACCOUNT_ADDRESS);
console.log(`[TEST] KRK balance after buy: ${krkAfterBuy}`);
expect(krkAfterBuy).toBeGreaterThan(krkBefore);
console.log('[TEST] ✅ KRK received');
// ── 4. Sell all KRK back (sovereign exit) ────────────────────────────
const wethReceived = await sellAllKrk(page, {
rpcUrl: config.rpcUrl,
krkAddress: config.contracts.Kraiken,
accountAddress: ACCOUNT_ADDRESS,
});
// ── 5. Assert KRK was sold ────────────────────────────────────────────
const krkAfterSell = await getKrkBalance(config.rpcUrl, config.contracts.Kraiken, ACCOUNT_ADDRESS);
console.log(`[TEST] KRK balance after sell: ${krkAfterSell}`);
expect(krkAfterSell).toBeLessThan(krkAfterBuy);
console.log('[TEST] ✅ Sovereign exit confirmed: KRK sold back to WETH');
// ── 6. Assert reasonable slippage (at least 90% of ETH spent) ─────────
const ethSpent = parseEther('0.1');
const minExpected = parseEther('0.09'); // 90% of 0.1 ETH
expect(wethReceived).toBeGreaterThanOrEqual(minExpected);
const slippagePercent = ((Number(wethReceived) / Number(ethSpent)) * 100).toFixed(2);
console.log(`[TEST] ✅ Reasonable slippage: received ${wethReceived} WETH for 0.1 ETH spent (${slippagePercent}%)`);
} finally {
await ctx.close();
}
});