Merge pull request 'fix: Backtesting #2: Foundry script skeleton + Uniswap V3 shadow pool deployment (#316)' (#332) from fix/issue-316 into master
This commit is contained in:
commit
00037dc713
3 changed files with 180 additions and 0 deletions
107
onchain/script/backtesting/BacktestRunner.s.sol
Normal file
107
onchain/script/backtesting/BacktestRunner.s.sol
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import { Script } from "forge-std/Script.sol";
|
||||
import { console2 } from "forge-std/console2.sol";
|
||||
import { MockToken } from "./MockToken.sol";
|
||||
import { ShadowPool, ShadowPoolDeployer } from "./ShadowPoolDeployer.sol";
|
||||
|
||||
/**
|
||||
* @title BacktestRunner
|
||||
* @notice Entry point for backtesting. Deploys a UniswapV3 shadow pool that mirrors the
|
||||
* AERO/WETH 1% pool configuration, initialised at the price from the event cache.
|
||||
*
|
||||
* Usage:
|
||||
* forge script script/backtesting/BacktestRunner.s.sol \
|
||||
* --rpc-url http://localhost:8545 --broadcast \
|
||||
* [BACKTEST_EVENTS_FILE=/path/to/events.jsonl] \
|
||||
* [INITIAL_SQRT_PRICE_X96=<uint160>]
|
||||
*
|
||||
* Price resolution order (first that succeeds wins):
|
||||
* 1. Environment variable INITIAL_SQRT_PRICE_X96
|
||||
* 2. sqrtPriceX96 field from the first line of $BACKTEST_EVENTS_FILE
|
||||
* 3. Default: 2^96 (tick = 0, price 1:1)
|
||||
*/
|
||||
contract BacktestRunner is Script {
|
||||
/// @dev 2^96 — corresponds to tick 0 (price ratio 1:1).
|
||||
uint160 internal constant DEFAULT_SQRT_PRICE_X96 = 79_228_162_514_264_337_593_543_950_336;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Price resolution helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @notice Parse sqrtPriceX96 from the first line of a JSON Lines events file.
|
||||
* @dev Must be `external` so it can be called via `this.` inside a try/catch.
|
||||
* fetch-events.ts serialises BigInt values as decimal strings, so we use
|
||||
* parseJsonString then parseUint rather than parseJsonUint.
|
||||
*/
|
||||
function _parseSqrtPriceFromFile(string memory eventsFile) external view returns (uint160) {
|
||||
string memory line = vm.readLine(eventsFile);
|
||||
string memory sqrtStr = vm.parseJsonString(line, ".sqrtPriceX96");
|
||||
return uint160(vm.parseUint(sqrtStr));
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Resolve the initial sqrtPriceX96 without broadcasting any transaction.
|
||||
* @dev Call this BEFORE vm.startBroadcast() — the `this.` external call must not
|
||||
* be included in the broadcast.
|
||||
*/
|
||||
function _resolveSqrtPrice() internal view returns (uint160) {
|
||||
// 1. Explicit env override.
|
||||
try vm.envUint("INITIAL_SQRT_PRICE_X96") returns (uint256 val) {
|
||||
if (val != 0) return uint160(val);
|
||||
} catch {}
|
||||
|
||||
// 2. First Swap event in the events cache.
|
||||
try vm.envString("BACKTEST_EVENTS_FILE") returns (string memory eventsFile) {
|
||||
try this._parseSqrtPriceFromFile(eventsFile) returns (uint160 val) {
|
||||
if (val != 0) return val;
|
||||
} catch {}
|
||||
} catch {}
|
||||
|
||||
// 3. Fallback default.
|
||||
return DEFAULT_SQRT_PRICE_X96;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Main entry point
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
function run() external {
|
||||
// Resolve price before broadcast so the self-call is not recorded.
|
||||
uint160 sqrtPriceX96 = _resolveSqrtPrice();
|
||||
|
||||
vm.startBroadcast();
|
||||
|
||||
// Deploy mock ERC20 tokens (18 decimals, matching AERO and WETH).
|
||||
MockToken tokenA = new MockToken("Mock AERO", "mAERO", 18);
|
||||
MockToken tokenB = new MockToken("Mock WETH", "mWETH", 18);
|
||||
|
||||
// Mint an initial supply to the broadcaster for future liquidity seeding.
|
||||
address sender = msg.sender;
|
||||
tokenA.mint(sender, 1_000_000 ether);
|
||||
tokenB.mint(sender, 1_000_000 ether);
|
||||
|
||||
// Deploy factory + pool and initialise at the resolved price.
|
||||
ShadowPool memory sp = ShadowPoolDeployer.deploy(address(tokenA), address(tokenB), sqrtPriceX96);
|
||||
|
||||
vm.stopBroadcast();
|
||||
|
||||
// Query pool state (view calls, no broadcast needed).
|
||||
(uint160 slot0SqrtPrice, int24 tick,,,,,) = sp.pool.slot0();
|
||||
uint128 liquidity = sp.pool.liquidity();
|
||||
int24 tickSpacing = sp.pool.tickSpacing();
|
||||
|
||||
console2.log("=== Backtesting Shadow Pool Deployment ===");
|
||||
console2.log("Factory: ", address(sp.factory));
|
||||
console2.log("Pool: ", address(sp.pool));
|
||||
console2.log("Token0: ", sp.token0);
|
||||
console2.log("Token1: ", sp.token1);
|
||||
console2.log("Fee tier: ", uint256(ShadowPoolDeployer.SHADOW_FEE));
|
||||
console2.log("Tick spacing: ", int256(tickSpacing));
|
||||
console2.log("sqrtPriceX96: ", uint256(slot0SqrtPrice));
|
||||
console2.log("Initial tick: ", int256(tick));
|
||||
console2.log("Liquidity: ", uint256(liquidity));
|
||||
}
|
||||
}
|
||||
24
onchain/script/backtesting/MockToken.sol
Normal file
24
onchain/script/backtesting/MockToken.sol
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import { ERC20 } from "@openzeppelin/token/ERC20/ERC20.sol";
|
||||
|
||||
/**
|
||||
* @title MockToken
|
||||
* @notice Minimal ERC20 with open mint for backtesting only. No access control.
|
||||
*/
|
||||
contract MockToken is ERC20 {
|
||||
uint8 private _dec;
|
||||
|
||||
constructor(string memory name, string memory symbol, uint8 decimals_) ERC20(name, symbol) {
|
||||
_dec = decimals_;
|
||||
}
|
||||
|
||||
function decimals() public view override returns (uint8) {
|
||||
return _dec;
|
||||
}
|
||||
|
||||
function mint(address to, uint256 amount) external {
|
||||
_mint(to, amount);
|
||||
}
|
||||
}
|
||||
49
onchain/script/backtesting/ShadowPoolDeployer.sol
Normal file
49
onchain/script/backtesting/ShadowPoolDeployer.sol
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import { IUniswapV3Factory } from "@uniswap-v3-core/interfaces/IUniswapV3Factory.sol";
|
||||
import { IUniswapV3Pool } from "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
|
||||
import { UniswapHelpers } from "../../src/helpers/UniswapHelpers.sol";
|
||||
|
||||
struct ShadowPool {
|
||||
IUniswapV3Factory factory;
|
||||
IUniswapV3Pool pool;
|
||||
address token0;
|
||||
address token1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @title ShadowPoolDeployer
|
||||
* @notice Deploys a fresh UniswapV3Factory and pool for backtesting.
|
||||
* SHADOW_FEE = 10 000 (1%) matches the AERO/WETH pool on Base.
|
||||
*/
|
||||
library ShadowPoolDeployer {
|
||||
uint24 internal constant SHADOW_FEE = 10_000;
|
||||
|
||||
/**
|
||||
* @notice Deploy a new UniswapV3 factory + pool and initialize it.
|
||||
* @param tokenA One of the two mock tokens (any order).
|
||||
* @param tokenB The other mock token.
|
||||
* @param sqrtPriceX96 Initial sqrt price (Q64.96).
|
||||
* @return sp ShadowPool struct with factory, pool, token0, token1 addresses.
|
||||
*/
|
||||
function deploy(
|
||||
address tokenA,
|
||||
address tokenB,
|
||||
uint160 sqrtPriceX96
|
||||
)
|
||||
internal
|
||||
returns (ShadowPool memory sp)
|
||||
{
|
||||
sp.factory = UniswapHelpers.deployUniswapFactory();
|
||||
|
||||
address poolAddr = sp.factory.createPool(tokenA, tokenB, SHADOW_FEE);
|
||||
sp.pool = IUniswapV3Pool(poolAddr);
|
||||
|
||||
sp.pool.initialize(sqrtPriceX96);
|
||||
|
||||
// Resolve canonical token ordering from the pool.
|
||||
sp.token0 = sp.pool.token0();
|
||||
sp.token1 = sp.pool.token1();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue