diff --git a/scripts/harb-evaluator/helpers/anvil.ts b/scripts/harb-evaluator/helpers/anvil.ts new file mode 100644 index 0000000..a87efb7 --- /dev/null +++ b/scripts/harb-evaluator/helpers/anvil.ts @@ -0,0 +1,36 @@ +/** + * Anvil snapshot/revert helpers for the red-team agent feedback loop. + * + * snapshot() and revert() use Anvil's proprietary RPC methods to save and + * restore chain state, allowing scenarios to run mutating actions and then + * reset the fork cleanly. + * + * mineBlocks is re-exported from recenter.ts so callers can import both + * snapshot helpers and block-mining from a single module. + */ +import { rpcCall } from './rpc.js'; +export { mineBlocks } from './recenter.js'; + +/** + * Take an Anvil chain snapshot. + * + * @returns The snapshot ID (hex string) to pass to revert(). + */ +export async function snapshot(rpcUrl: string): Promise { + const id = (await rpcCall(rpcUrl, 'anvil_snapshot', [])) as string; + console.log(`[anvil] Snapshot taken: ${id}`); + return id; +} + +/** + * Revert the chain to a previously taken snapshot. + * + * Throws if Anvil reports the revert as unsuccessful (e.g. unknown snapshot ID). + * + * @param snapshotId - The hex snapshot ID returned by snapshot(). + */ +export async function revert(rpcUrl: string, snapshotId: string): Promise { + const success = (await rpcCall(rpcUrl, 'anvil_revert', [snapshotId])) as boolean; + if (!success) throw new Error(`[anvil] revert failed for snapshot ${snapshotId}`); + console.log(`[anvil] Reverted to snapshot: ${snapshotId}`); +} diff --git a/scripts/harb-evaluator/helpers/floor.ts b/scripts/harb-evaluator/helpers/floor.ts new file mode 100644 index 0000000..029579a --- /dev/null +++ b/scripts/harb-evaluator/helpers/floor.ts @@ -0,0 +1,79 @@ +/** + * Floor price helpers for the red-team agent feedback loop. + * + * Reads ethPerToken and related floor diagnostics from the Kraiken contract + * and supporting on-chain state via direct JSON-RPC calls. + */ +import { Interface } from 'ethers'; +import { rpcCall } from './rpc.js'; + +// Base WETH address — stable across Anvil forks of Base Sepolia. +const WETH = '0x4200000000000000000000000000000000000006'; + +const KRK_ABI = [ + 'function ethPerToken() external view returns (uint256)', + 'function outstandingSupply() external view returns (uint256)', +]; +const ERC20_ABI = ['function balanceOf(address account) external view returns (uint256)']; + +const krkIface = new Interface(KRK_ABI); +const erc20Iface = new Interface(ERC20_ABI); + +/** + * Read the current floor price from the Kraiken contract. + * + * @param krkAddress - Kraiken contract address. + * @param _lmAddress - LiquidityManager address (reserved for future fallback computation). + * @returns ethPerToken in wei. + */ +export async function getEthPerToken(rpcUrl: string, krkAddress: string, _lmAddress: string): Promise { + const calldata = krkIface.encodeFunctionData('ethPerToken', []); + const result = (await rpcCall(rpcUrl, 'eth_call', [{ to: krkAddress, data: calldata }, 'latest'])) as string; + const [value] = krkIface.decodeFunctionResult('ethPerToken', result); + return BigInt(value); +} + +/** + * Read full floor diagnostics for the LiquidityManager / Kraiken pair. + * + * All four reads are issued in parallel to minimise round-trip latency. + * + * @param lmAddress - LiquidityManager contract address. + * @param krkAddress - Kraiken contract address. + * @returns {ethPerToken} floor price in wei + * @returns {lmEthBalance} native ETH held by the LiquidityManager + * @returns {lmWethBalance} WETH held by the LiquidityManager + * @returns {outstandingSupply} total KRK outstanding supply + */ +export async function getFloorState( + rpcUrl: string, + lmAddress: string, + krkAddress: string, +): Promise<{ + ethPerToken: bigint; + lmEthBalance: bigint; + lmWethBalance: bigint; + outstandingSupply: bigint; +}> { + const ethPerTokenCalldata = krkIface.encodeFunctionData('ethPerToken', []); + const outstandingSupplyCalldata = krkIface.encodeFunctionData('outstandingSupply', []); + const wethBalanceCalldata = erc20Iface.encodeFunctionData('balanceOf', [lmAddress]); + + const [ethPerTokenHex, lmEthBalanceHex, lmWethBalanceHex, outstandingSupplyHex] = (await Promise.all([ + rpcCall(rpcUrl, 'eth_call', [{ to: krkAddress, data: ethPerTokenCalldata }, 'latest']), + rpcCall(rpcUrl, 'eth_getBalance', [lmAddress, 'latest']), + rpcCall(rpcUrl, 'eth_call', [{ to: WETH, data: wethBalanceCalldata }, 'latest']), + rpcCall(rpcUrl, 'eth_call', [{ to: krkAddress, data: outstandingSupplyCalldata }, 'latest']), + ])) as [string, string, string, string]; + + const [ethPerToken] = krkIface.decodeFunctionResult('ethPerToken', ethPerTokenHex); + const [lmWethBalance] = erc20Iface.decodeFunctionResult('balanceOf', lmWethBalanceHex); + const [outstandingSupply] = krkIface.decodeFunctionResult('outstandingSupply', outstandingSupplyHex); + + return { + ethPerToken: BigInt(ethPerToken), + lmEthBalance: BigInt(lmEthBalanceHex), + lmWethBalance: BigInt(lmWethBalance), + outstandingSupply: BigInt(outstandingSupply), + }; +}