Merge pull request 'fix: feat: Anvil snapshot/revert and ethPerToken helpers (#519)' (#521) from fix/issue-519 into master
This commit is contained in:
commit
3083e24eab
2 changed files with 161 additions and 0 deletions
41
scripts/harb-evaluator/helpers/anvil.ts
Normal file
41
scripts/harb-evaluator/helpers/anvil.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
/**
|
||||
* 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<string> {
|
||||
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.
|
||||
*
|
||||
* ⚠️ anvil_revert is one-shot: the snapshot is consumed on success and the ID
|
||||
* becomes invalid afterward. Callers that need to reuse a checkpoint must
|
||||
* call snapshot() again after each revert.
|
||||
*
|
||||
* Throws if Anvil reports the revert as unsuccessful (e.g. unknown or already-used
|
||||
* snapshot ID).
|
||||
*
|
||||
* @param snapshotId - The hex snapshot ID returned by snapshot().
|
||||
*/
|
||||
export async function revert(rpcUrl: string, snapshotId: string): Promise<void> {
|
||||
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}`);
|
||||
}
|
||||
120
scripts/harb-evaluator/helpers/floor.ts
Normal file
120
scripts/harb-evaluator/helpers/floor.ts
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
/**
|
||||
* Floor price helpers for the red-team agent feedback loop.
|
||||
*
|
||||
* ethPerToken is not a contract view function — it is computed off-chain as
|
||||
* lmTotalEth / adjustedOutstandingSupply, where:
|
||||
* - lmTotalEth = native ETH + WETH held by LiquidityManager
|
||||
* (mirrors ThreePositionStrategy._getEthBalance())
|
||||
* - adjustedSupply = kraiken.outstandingSupply() minus KRK at
|
||||
* feeDestination and stakingPool
|
||||
* (mirrors LiquidityManager._getOutstandingSupply())
|
||||
*/
|
||||
import { Interface } from 'ethers';
|
||||
import { rpcCall } from './rpc.js';
|
||||
|
||||
// Base WETH address — stable across Anvil forks of Base Sepolia.
|
||||
const WETH = '0x4200000000000000000000000000000000000006';
|
||||
|
||||
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
|
||||
|
||||
const KRK_ABI = [
|
||||
'function outstandingSupply() external view returns (uint256)',
|
||||
'function peripheryContracts() external view returns (address, address)',
|
||||
'function balanceOf(address account) external view returns (uint256)',
|
||||
];
|
||||
|
||||
/** feeDestination() is the auto-generated getter for the public storage var on LiquidityManager. */
|
||||
const LM_ABI = ['function feeDestination() external view returns (address)'];
|
||||
|
||||
const ERC20_ABI = ['function balanceOf(address account) external view returns (uint256)'];
|
||||
|
||||
const krkIface = new Interface(KRK_ABI);
|
||||
const lmIface = new Interface(LM_ABI);
|
||||
const erc20Iface = new Interface(ERC20_ABI);
|
||||
|
||||
/**
|
||||
* Read full floor diagnostics for the LiquidityManager / Kraiken pair.
|
||||
*
|
||||
* All reads are issued in at most two parallel rounds to minimise latency:
|
||||
* Round 1: ETH/WETH balances, raw outstanding supply, feeDestination, peripheryContracts
|
||||
* Round 2 (conditional): balanceOf(feeDestination) and/or balanceOf(stakingPool) when non-zero
|
||||
*
|
||||
* outstandingSupply in the return value mirrors LiquidityManager._getOutstandingSupply():
|
||||
* it starts from kraiken.outstandingSupply() then subtracts KRK held at feeDestination
|
||||
* and stakingPool, since neither can be sold into the floor.
|
||||
*
|
||||
* ethPerToken = (lmNativeEth + lmWeth) / adjustedOutstandingSupply.
|
||||
* Returns ethPerToken = 0n when outstandingSupply is zero (uninitialized pool).
|
||||
*
|
||||
* @param lmAddress - LiquidityManager contract address.
|
||||
* @param krkAddress - Kraiken contract address.
|
||||
*/
|
||||
export async function getFloorState(
|
||||
rpcUrl: string,
|
||||
lmAddress: string,
|
||||
krkAddress: string,
|
||||
): Promise<{
|
||||
ethPerToken: bigint;
|
||||
lmEthBalance: bigint;
|
||||
lmWethBalance: bigint;
|
||||
outstandingSupply: bigint;
|
||||
}> {
|
||||
// Round 1: five reads in parallel.
|
||||
const [lmEthHex, lmWethHex, rawSupplyHex, feeDestHex, peripheryHex] = (await Promise.all([
|
||||
rpcCall(rpcUrl, 'eth_getBalance', [lmAddress, 'latest']),
|
||||
rpcCall(rpcUrl, 'eth_call', [{ to: WETH, data: erc20Iface.encodeFunctionData('balanceOf', [lmAddress]) }, 'latest']),
|
||||
rpcCall(rpcUrl, 'eth_call', [{ to: krkAddress, data: krkIface.encodeFunctionData('outstandingSupply', []) }, 'latest']),
|
||||
rpcCall(rpcUrl, 'eth_call', [{ to: lmAddress, data: lmIface.encodeFunctionData('feeDestination', []) }, 'latest']),
|
||||
rpcCall(rpcUrl, 'eth_call', [{ to: krkAddress, data: krkIface.encodeFunctionData('peripheryContracts', []) }, 'latest']),
|
||||
])) as [string, string, string, string, string];
|
||||
|
||||
const [lmWethRaw] = erc20Iface.decodeFunctionResult('balanceOf', lmWethHex);
|
||||
const [rawSupply] = krkIface.decodeFunctionResult('outstandingSupply', rawSupplyHex);
|
||||
const [feeDestination] = lmIface.decodeFunctionResult('feeDestination', feeDestHex);
|
||||
// peripheryContracts() returns (liquidityManager, stakingPool) — we only need the second.
|
||||
const [, stakingPool] = krkIface.decodeFunctionResult('peripheryContracts', peripheryHex);
|
||||
|
||||
const lmEthBalance = BigInt(lmEthHex);
|
||||
const lmWethBalance = BigInt(lmWethRaw);
|
||||
|
||||
// Round 2: subtract excluded KRK balances (matches _getOutstandingSupply logic).
|
||||
const isZero = (addr: string) => addr.toLowerCase() === ZERO_ADDRESS;
|
||||
const excluded: string[] = [];
|
||||
if (!isZero(feeDestination as string)) excluded.push(feeDestination as string);
|
||||
if (!isZero(stakingPool as string)) excluded.push(stakingPool as string);
|
||||
|
||||
let supply = BigInt(rawSupply);
|
||||
if (excluded.length > 0) {
|
||||
const balHexes = (await Promise.all(
|
||||
excluded.map(addr =>
|
||||
rpcCall(rpcUrl, 'eth_call', [{ to: krkAddress, data: krkIface.encodeFunctionData('balanceOf', [addr]) }, 'latest']),
|
||||
),
|
||||
)) as string[];
|
||||
for (const hex of balHexes) {
|
||||
const [bal] = krkIface.decodeFunctionResult('balanceOf', hex);
|
||||
supply -= BigInt(bal);
|
||||
}
|
||||
}
|
||||
|
||||
const lmTotalEth = lmEthBalance + lmWethBalance;
|
||||
// Scale by 1e18 (WAD) before dividing so the result is in wei-per-token
|
||||
// rather than always 0n. Example: 100 ETH / 1M KRK = 1e20 * 1e18 / 1e24 = 1e14 wei/token.
|
||||
const ethPerToken = supply === 0n ? 0n : (lmTotalEth * 10n ** 18n) / supply;
|
||||
|
||||
return { ethPerToken, lmEthBalance, lmWethBalance, outstandingSupply: supply };
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the current floor price (ethPerToken) from LM state.
|
||||
*
|
||||
* Delegates to getFloorState(); callers that need multiple fields should call
|
||||
* getFloorState() directly to avoid redundant RPC calls.
|
||||
*
|
||||
* @param krkAddress - Kraiken contract address.
|
||||
* @param lmAddress - LiquidityManager contract address.
|
||||
* @returns ethPerToken in wei, 0n when outstanding supply is zero.
|
||||
*/
|
||||
export async function getEthPerToken(rpcUrl: string, krkAddress: string, lmAddress: string): Promise<bigint> {
|
||||
const { ethPerToken } = await getFloorState(rpcUrl, lmAddress, krkAddress);
|
||||
return ethPerToken;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue