From 1f7a75885700c338a2159beef63e36c242720bdb Mon Sep 17 00:00:00 2001 From: Roninxx Date: Fri, 29 May 2026 00:04:04 +0100 Subject: [PATCH] fix(security): gate strategy entry points with onlyVault + harden vault Smart-contract audit found strategy deposit()/withdraw() were callable by anyone, letting funds be drained directly from a strategy (bypassing the vault). Fixes: - AaveStrategy/IdleStrategy/MockYieldStrategy: bind an immutable vault and add an onlyVault modifier on deposit()/withdraw(); constructors now take the vault address (call sites + deploy scripts updated). - FloatVault: inherit ReentrancyGuard; park/withdraw/promote are nonReentrant. - AgentFloatHook: symmetric lpProvider accounting on add/remove (refund capped to actual balance); switch to forceApprove for USDT compatibility. - New regression test test_Revert_DirectStrategyWithdraw; 13/13 pass. Live mainnet remediation: redeployed the hardened AaveStrategy to 0xB433487F82572FF201A2455BF7a06325a7B8bFEa (strategy ID 4) and migrated pooled capital via the trustless on-chain promote() path. Docs updated to the new address and 13/13 test count. Co-Authored-By: Claude Opus 4.7 --- .gitignore | 3 + README.md | 57 +++++++++++-------- agent/src/api.ts | 22 +++++-- agent/src/consolidator.ts | 10 +++- agent/src/watcher.ts | 3 +- contracts/script/Deploy.s.sol | 9 +-- contracts/script/DeployMainnet.s.sol | 5 +- contracts/src/AgentFloatHook.sol | 39 +++++++++---- contracts/src/FloatVault.sol | 9 +-- contracts/src/strategies/AaveStrategy.sol | 14 ++++- contracts/src/strategies/IdleStrategy.sol | 14 ++++- .../src/strategies/MockYieldStrategy.sol | 9 ++- contracts/test/AgentFloatHook.t.sol | 2 +- contracts/test/FlapYieldTaxVault.t.sol | 2 +- contracts/test/FloatVault.t.sol | 28 ++++++++- docs/pitch-deck.md | 10 ++-- docs/submission.md | 12 ++-- docs/voiceover_script.md | 28 +++++++++ web/src/components/HookDiagram.tsx | 20 +++++-- web/src/components/StrategyDeck.tsx | 9 ++- web/src/lib/brain.ts | 20 +++++-- web/src/lib/demo.ts | 8 +-- web/src/lib/vault.ts | 27 ++++++--- 23 files changed, 267 insertions(+), 93 deletions(-) create mode 100644 docs/voiceover_script.md diff --git a/.gitignore b/.gitignore index 5ec3132..f2b3d80 100644 --- a/.gitignore +++ b/.gitignore @@ -64,3 +64,6 @@ brain/ # Twitter banner export artefacts (saved from x.com) web/x banner* *_files/ + +# Large demo video binaries (kept out of git) +*.mp4 diff --git a/README.md b/README.md index a1eab03..682dd6b 100644 --- a/README.md +++ b/README.md @@ -6,19 +6,26 @@ --- -## What happens when you swap +## Why AgentFloat? -``` -1. LP adds liquidity to a v4 pool with AgentFloatHook attached -2. Price moves out of the LP's range → that capital is now idle -3. afterAddLiquidity → the hook parks the idle USDT into the vault → vault supplies it to Aave V3 (earning yield) -4. A swap comes in → beforeSwap → the hook recalls capital just-in-time so the pool can settle -5. In the background: an AI scores yield strategies and proposes better ones; an on-chain - consecutiveWins counter promotes a winner only after it proves itself — no AI can move - real capital on its own -``` +### The Problem: Idle Capital & Collapsed Depth +In concentrated liquidity pools (like Uniswap v4), Liquidity Providers (LPs) choose specific price ranges to deploy their assets. When the market price moves outside this range: +1. **Capital sits idle:** It stops earning trading fees. +2. **Depth collapses:** When a large position goes out of range, the pool's liquidity depth drops, and the effective spread widens. +3. **Arbitrageurs exploit the pool:** Arbitrage bots spot these widened spreads instantly and exploit the price differences before LPs can manually rebalance their positions. + +### The Solution: JIT (Just-In-Time) Recall Hook +AgentFloat solves this using a Uniswap v4 Hook that monitors and routes capital dynamically: +* **Idle Sweeps:** When a position goes out of range, the hook automatically sweeps the idle capital into the `FloatVault` to generate yield in active strategies like Aave V3. +* **Just-In-Time Recall:** The moment a swap starts, the hook's `beforeSwap` callback instantly pulls the capital back into the pool. +* **Spread Protection:** By recalling the capital in the exact transaction of the trade, the pool's full depth is restored. The spread stays tight, traders get perfect execution, and LPs capture yield while waiting. -The hook is the product. The AI is the optimization layer. The chain is the final authority on what touches money. +### The Guardrail: "The AI Thinks, the Chain Decides" +To optimize yield, an off-chain AI continuously simulates and proposes alternative strategies. But the AI has zero permission to move real capital: +* **On-Chain Gatekeeper:** A strategy must win consecutive performance checks against the active strategy directly on-chain. +* **Trustless Promotion:** Once a strategy proves itself, anyone can call `promote()` on-chain to migrate the pool's capital. The AI cannot bypass this rule. + +--- --- @@ -31,7 +38,7 @@ The hook is the product. The AI is the optimization layer. The chain is the fina 5. **Chain-scoped operating modes**: on testnet the AI can deploy new Solidity; on mainnet it's bounded to register/retire/scoring actions against a pre-audited library. 6. **Composable downstream**: a `FlapYieldTaxVaultFactory` lets any Flap-graduated token pipe its tax revenue into AgentFloat to earn Aave yield instead of sitting idle. -12/12 Forge tests passing. Real Aave V3 integration. ~$0.09 to deploy the entire system to X Layer mainnet. +13/13 Forge tests passing (incl. a strategy-drain regression test). Real Aave V3 integration. ~$0.09 to deploy the entire system to X Layer mainnet. --- @@ -42,7 +49,7 @@ The hook is the product. The AI is the optimization layer. The chain is the fina | **AgentFloatHook** | [`0x5Ba6671e8219C34edA373BF95895306929174580`](https://www.oklink.com/xlayer/address/0x5Ba6671e8219C34edA373BF95895306929174580) | 5,101 b · permission bits `0x580` | | **FloatVault** | [`0xbF06de108735332D1EDb81C7A77A750DD428a6f4`](https://www.oklink.com/xlayer/address/0xbF06de108735332D1EDb81C7A77A750DD428a6f4) | 5,115 b | | **FlapYieldTaxVaultFactory** | [`0x87D665B83557365ADf320a439B8a2DFD03c024F8`](https://www.oklink.com/xlayer/address/0x87D665B83557365ADf320a439B8a2DFD03c024F8) | 10,050 b | -| **AaveStrategy** (real Aave V3 USDT) | [`0x4C109f12d2FA55037439b73CE4E9Ee2C1e1656E1`](https://www.oklink.com/xlayer/address/0x4C109f12d2FA55037439b73CE4E9Ee2C1e1656E1) | 2,429 b | +| **AaveStrategy** (real Aave V3 USDT, `onlyVault`-guarded) | [`0xB433487F82572FF201A2455BF7a06325a7B8bFEa`](https://www.oklink.com/xlayer/address/0xB433487F82572FF201A2455BF7a06325a7B8bFEa) | 2,5xx b | | **IdleStrategy** (baseline) | [`0xf292e500459393F5CfaF8fbccFe1426bC3495EEb`](https://www.oklink.com/xlayer/address/0xf292e500459393F5CfaF8fbccFe1426bC3495EEb) | 939 b | Attached to canonical X Layer mainnet infrastructure (no PoolManager or Aave redeploy needed): @@ -56,7 +63,7 @@ Attached to canonical X Layer mainnet infrastructure (no PoolManager or Aave red Total deploy cost: **0.000372 OKB (~$0.09)** at 0.02 gwei. -**Live yield, verifiable now:** the AaveStrategy `0x4C109…` holds a real interest-bearing **aUSDT** position supplied to Aave V3 — capital that flowed through the vault and is earning lending yield block-by-block. Check `aUSDT.balanceOf(0x4C109f12d2FA55037439b73CE4E9Ee2C1e1656E1)` on chain 196. +**Live yield, verifiable now:** the AaveStrategy `0xB433…` holds a real interest-bearing **aUSDT** position supplied to Aave V3 — capital that flowed through the vault and is earning lending yield block-by-block. Check `aUSDT.balanceOf(0xB433487F82572FF201A2455BF7a06325a7B8bFEa)` on chain 196. (The prior strategy `0x4C109…` was superseded by this `onlyVault`-hardened build; funds were migrated trustlessly via the on-chain `promote()` path.) --- @@ -173,19 +180,20 @@ Other guardrails: `max_proposals_per_day`, `max_strategies_registered`, `pinned_ --- -## Test results — 12/12 passing +## Test results — 13/13 passing ``` -Ran 3 test suites: 12 tests passed, 0 failed, 0 skipped +Ran 3 test suites: 13 tests passed, 0 failed, 0 skipped -[PASS] test_Integration_Workflow() (gas: 693560) — full end-to-end path +[PASS] test_Integration_Workflow() (gas: 700264) — full end-to-end path [PASS] test_DecentralizedPromotion_Success() — trustless promote() works [PASS] test_DecentralizedPromotion_WinsResetOnLoss() — counter resets correctly -[PASS] test_ParkAndWithdraw() (gas: 267390) +[PASS] test_ParkAndWithdraw() (gas: 274304) +[PASS] test_Revert_DirectStrategyWithdraw() — strategy funds are onlyVault-gated [PASS] test_PostScore() (gas: 147575) -[PASS] test_Promote() (gas: 403565) -[PASS] test_RegisterStrategy() (gas: 181344) -[PASS] test_Revert_NonPromoterPromote() (gas: 187358) +[PASS] test_Promote() (gas: 410532) +[PASS] test_RegisterStrategy() (gas: 181338) +[PASS] test_Revert_NonPromoterPromote() (gas: 192474) [PASS] FlapYieldTaxVault — ERC20 path [PASS] FlapYieldTaxVault — native OKB → WOKB path [PASS] FlapYieldTaxVault — withdraw with accrued yield @@ -208,7 +216,7 @@ Ran 3 test suites: 12 tests passed, 0 failed, 0 skipped ```bash cd contracts ./setup.sh # installs Foundry deps + builds + runs tests -forge test # 12/12 should pass +forge test # 13/13 should pass ``` ### Mainnet deploy (~$0.09 total) @@ -261,6 +269,9 @@ Built against the Uniswap `v4-security-foundations` threat model. - **`validateHookAddress(this)` enforced in `BaseHook` constructor** — deployed hook address must encode correct permission bits in its low 14 bits; misdeployed hooks revert at construction. Our mainnet hook at `0x5Ba6671e8219C34edA373BF95895306929174580` encodes `0x580` (afterAddLiquidity + afterRemoveLiquidity + beforeSwap). - **`onlyPoolManager` modifier on every external callback** via the canonical `BaseHook` internal-callback pattern (we inlined the canonical source because the installed v4-periphery submodule doesn't ship `src/utils/BaseHook.sol`). +- **`onlyVault` on every strategy entry point** — `deposit()` and `withdraw()` on `AaveStrategy`/`IdleStrategy`/`MockYieldStrategy` revert unless called by their bound vault, so pooled funds can never be drained by a direct external call to a strategy. Covered by `test_Revert_DirectStrategyWithdraw`. +- **`ReentrancyGuard` on the vault** — `park()`, `withdraw()`, and `promote()` are `nonReentrant`. +- **`forceApprove` (zero-then-set)** everywhere the hook grants allowances, for USDT-style tokens that reject non-zero→non-zero `approve`. - **Trustless promotion gate** — `consecutiveWins` mapping lives on-chain, anyone can call `promote()` once threshold is met; promotion is not promoter-gated, scoring is. - **EIP-1153 transient storage** for tracking parked USDT within the swap transaction lifecycle, reducing storage gas on JIT recall. - **Owner-gated strategy registration + score posting** via OpenZeppelin `Ownable`. @@ -296,7 +307,7 @@ agentfloat-hook/ │ │ ├── MineHookSalt.s.sol standalone salt miner │ │ ├── DeployFlapFactory.s.sol Flap factory deploy │ │ └── XLayerMainnet.sol canonical external address constants -│ └── test/ 12 tests, all passing +│ └── test/ 13 tests, all passing │ ├── agent/ Off-chain agent (TypeScript) │ └── src/ diff --git a/agent/src/api.ts b/agent/src/api.ts index f34b8ee..ea9f3eb 100644 --- a/agent/src/api.ts +++ b/agent/src/api.ts @@ -116,11 +116,25 @@ function buildState() { testsTotal: 8, }, contracts: { - vault: process.env.VAULT_ADDRESS || '0x4d33FD7B077c1a23221252c3FFEe4261c8a67c5f', - hook: process.env.HOOK_ADDRESS || '0x3A00B5A2F15bE68AfE5415290ca4D3022e3B3b5F', + vault: + process.env.VAULT_ADDRESS || + (process.env.X_LAYER_CHAIN_ID === '196' + ? '0xbF06de108735332D1EDb81C7A77A750DD428a6f4' + : '0x4d33FD7B077c1a23221252c3FFEe4261c8a67c5f'), + hook: + process.env.HOOK_ADDRESS || + (process.env.X_LAYER_CHAIN_ID === '196' + ? '0x5Ba6671e8219C34edA373BF95895306929174580' + : '0x3A00B5A2F15bE68AfE5415290ca4D3022e3B3b5F'), poolManager: - process.env.POOL_MANAGER_ADDRESS || '0x1BB8824110DF8ED603eBb203C19cC2Ba8FdA8fbe', - explorerBase: 'https://www.oklink.com/xlayer-test', + process.env.POOL_MANAGER_ADDRESS || + (process.env.X_LAYER_CHAIN_ID === '196' + ? '0x360e68faccca8ca495c1b759fd9eee466db9fb32' + : '0x1BB8824110DF8ED603eBb203C19cC2Ba8FdA8fbe'), + explorerBase: + process.env.X_LAYER_CHAIN_ID === '196' + ? 'https://www.oklink.com/xlayer' + : 'https://www.oklink.com/xlayer-test', }, }; } diff --git a/agent/src/consolidator.ts b/agent/src/consolidator.ts index f654791..79eda6f 100644 --- a/agent/src/consolidator.ts +++ b/agent/src/consolidator.ts @@ -12,9 +12,17 @@ import fs from 'fs'; import path from 'path'; import os from 'os'; +import * as dotenv from 'dotenv'; import { loadStrategySpecs, type StrategySpec } from './brain'; +dotenv.config({ path: path.resolve(__dirname, '../.env') }); +dotenv.config({ path: path.resolve(__dirname, '../../.env') }); +dotenv.config(); + const BRAIN_PATH = process.env.BRAIN_PATH || path.join(os.homedir(), 'brain'); +const EXPLORER_BASE = process.env.X_LAYER_CHAIN_ID === '196' + ? 'https://www.oklink.com/xlayer' + : 'https://www.oklink.com/xlayer-test'; // ─── Types ──────────────────────────────────────────────────────────────── @@ -431,7 +439,7 @@ function renderHistory(opts: { lines.push('| Date | From | To | Δ Score (μbps) | Consecutive | Tx |'); lines.push('|------|------|----|----|----|----|'); for (const p of promotions) { - const txDisplay = p.txHash ? `[\`${p.txHash.slice(0, 10)}…\`](https://www.oklink.com/xlayer-test/tx/${p.txHash})` : '—'; + const txDisplay = p.txHash ? `[\`${p.txHash.slice(0, 10)}…\`](${EXPLORER_BASE}/tx/${p.txHash})` : '—'; lines.push( `| ${p.date} | [${p.fromStrategyId}] ${p.fromStrategyName} | [${p.toStrategyId}] ${p.toStrategyName} | ${p.scoreDelta} | ${p.consecutiveEpochs} | ${txDisplay} |`, ); diff --git a/agent/src/watcher.ts b/agent/src/watcher.ts index c4998b1..232f11d 100644 --- a/agent/src/watcher.ts +++ b/agent/src/watcher.ts @@ -10,7 +10,8 @@ import { syncFromBrain } from './store'; import { runDeployer } from './deployer'; export async function startWatcher() { - console.log(`[Watcher] Connecting to X Layer testnet at ${CONFIG.rpcUrl}...`); + const isMainnet = CONFIG.chainId === 196; + console.log(`[Watcher] Connecting to X Layer ${isMainnet ? 'mainnet' : 'testnet'} at ${CONFIG.rpcUrl}...`); const publicClient = createPublicClient({ chain: xLayerTestnetChain, diff --git a/contracts/script/Deploy.s.sol b/contracts/script/Deploy.s.sol index f909933..1289178 100644 --- a/contracts/script/Deploy.s.sol +++ b/contracts/script/Deploy.s.sol @@ -43,7 +43,7 @@ contract DeployScript is Script { console.log("Deployed FloatVault at:", address(vault)); // 4. Deploy Strategies - IdleStrategy idle = new IdleStrategy(usdcAddr); + IdleStrategy idle = new IdleStrategy(usdcAddr, address(vault)); console.log("Deployed IdleStrategy at:", address(idle)); // 5. Register strategies in FloatVault @@ -54,9 +54,10 @@ contract DeployScript is Script { // Aave Pool on X Layer: 0xE3F3Caefdd7180F884c01E57f65Df979Af84f116 // aUSDT0 token on X Layer: 0xF356ae412dB5df43BD3a10746f7ad4e1C4De4297 AaveStrategy aaveStrat = new AaveStrategy( - usdcAddr, - 0xE3F3Caefdd7180F884c01E57f65Df979Af84f116, - 0xF356ae412dB5df43BD3a10746f7ad4e1C4De4297 + usdcAddr, + 0xE3F3Caefdd7180F884c01E57f65Df979Af84f116, + 0xF356ae412dB5df43BD3a10746f7ad4e1C4De4297, + address(vault) ); console.log("Deployed AaveStrategy at:", address(aaveStrat)); vault.registerStrategy(address(aaveStrat), true); // shadow (id = 2) diff --git a/contracts/script/DeployMainnet.s.sol b/contracts/script/DeployMainnet.s.sol index 0887e41..60a9551 100644 --- a/contracts/script/DeployMainnet.s.sol +++ b/contracts/script/DeployMainnet.s.sol @@ -47,14 +47,15 @@ contract DeployMainnet is Script { console.log("FloatVault :", address(vault)); // 2. IdleStrategy (baseline floor -holds USDT, no yield) - IdleStrategy idle = new IdleStrategy(XLayerMainnet.USDT); + IdleStrategy idle = new IdleStrategy(XLayerMainnet.USDT, address(vault)); console.log("IdleStrategy:", address(idle)); // 3. AaveStrategy (real Aave V3 USDT lending market) AaveStrategy aave = new AaveStrategy( XLayerMainnet.USDT, XLayerMainnet.AAVE_POOL, - XLayerMainnet.AUSDT + XLayerMainnet.AUSDT, + address(vault) ); console.log("AaveStrategy:", address(aave)); diff --git a/contracts/src/AgentFloatHook.sol b/contracts/src/AgentFloatHook.sol index 793e001..e9d121f 100644 --- a/contracts/src/AgentFloatHook.sol +++ b/contracts/src/AgentFloatHook.sol @@ -87,7 +87,8 @@ contract AgentFloatHook is BaseHook { if (usdc.allowance(lpProvider, address(this)) >= idleAmount) { usdc.safeTransferFrom(lpProvider, address(this), idleAmount); - usdc.approve(address(vault), idleAmount); + // Zero-then-set to stay compatible with USDT-style tokens + usdc.forceApprove(address(vault), idleAmount); vault.park(idleAmount); // Track deposit @@ -111,20 +112,34 @@ contract AgentFloatHook is BaseHook { ModifyLiquidityParams calldata, BalanceDelta delta, BalanceDelta, - bytes calldata + bytes calldata hookData ) internal override returns (bytes4, BalanceDelta) { - uint256 userDep = userDeposits[sender]; + // Resolve the LP identity symmetrically with _afterAddLiquidity: hookData may + // carry the true LP (when liquidity is routed), otherwise default to sender. + address lpProvider = sender; + if (hookData.length >= 64) { + (lpProvider,) = abi.decode(hookData, (address, uint256)); + } + + uint256 userDep = userDeposits[lpProvider]; if (userDep > 0) { - userDeposits[sender] = 0; - - // Check if the funds are in the vault or already recalled to the hook balance + userDeposits[lpProvider] = 0; + + // Pull whatever is still parked under this hook back from the vault. + // Funds may already have been JIT-recalled into the hook's own balance, + // in which case vault.deposits is short and we top up from that balance. uint256 vaultBal = vault.deposits(address(this)); - if (vaultBal >= userDep) { - vault.withdraw(userDep); + uint256 toWithdraw = vaultBal >= userDep ? userDep : vaultBal; + if (toWithdraw > 0) { + vault.withdraw(toWithdraw); + } + + // Refund the LP, capped at the USDC the hook actually holds (no overdraw). + uint256 hookBal = usdc.balanceOf(address(this)); + uint256 refund = userDep <= hookBal ? userDep : hookBal; + if (refund > 0) { + usdc.safeTransfer(lpProvider, refund); } - - // Return USDC back to the LP provider (sender) - usdc.safeTransfer(sender, userDep); } return (BaseHook.afterRemoveLiquidity.selector, delta); } @@ -165,7 +180,7 @@ contract AgentFloatHook is BaseHook { uint256 persistentDeposits = vault.deposits(address(this)); if (persistentDeposits >= recallAmount) { vault.withdraw(recallAmount); - usdc.approve(address(poolManager), recallAmount); + usdc.forceApprove(address(poolManager), recallAmount); } } diff --git a/contracts/src/FloatVault.sol b/contracts/src/FloatVault.sol index c42b79f..17a8e48 100644 --- a/contracts/src/FloatVault.sol +++ b/contracts/src/FloatVault.sol @@ -4,9 +4,10 @@ pragma solidity ^0.8.24; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "./interfaces/IStrategy.sol"; -contract FloatVault is Ownable { +contract FloatVault is Ownable, ReentrancyGuard { using SafeERC20 for IERC20; IERC20 public immutable usdc; @@ -66,7 +67,7 @@ contract FloatVault is Ownable { } // Agent parks idle USDC into the vault - function park(uint256 amount) external { + function park(uint256 amount) external nonReentrant { require(amount > 0, "Amount must be > 0"); usdc.safeTransferFrom(msg.sender, address(this), amount); deposits[msg.sender] += amount; @@ -90,7 +91,7 @@ contract FloatVault is Ownable { } // Agent withdraws USDC from the vault instantly - function withdraw(uint256 amount) external { + function withdraw(uint256 amount) external nonReentrant { require(amount > 0, "Amount must be > 0"); require(deposits[msg.sender] >= amount, "Insufficient deposit"); @@ -167,7 +168,7 @@ contract FloatVault is Ownable { } // Promote shadow strategy to active - function promote(uint256 strategyId) external { + function promote(uint256 strategyId) external nonReentrant { require(strategyId <= strategyCount && strategyId != 0, "Invalid strategy ID"); StrategyEntry storage newActive = strategies[strategyId]; require(newActive.isShadow, "Strategy must be shadow"); diff --git a/contracts/src/strategies/AaveStrategy.sol b/contracts/src/strategies/AaveStrategy.sol index 83742ba..5846be1 100644 --- a/contracts/src/strategies/AaveStrategy.sol +++ b/contracts/src/strategies/AaveStrategy.sol @@ -39,18 +39,26 @@ contract AaveStrategy is IStrategy { IERC20 public immutable underlying; IAavePool public immutable pool; IERC20 public immutable aToken; + address public immutable vault; string public constant STRATEGY_NAME = "Aave V3 Yield Strategy"; uint256 public storedBalance; uint256 public lastLiquidityIndex; - constructor(address _underlying, address _pool, address _aToken) { + modifier onlyVault() { + require(msg.sender == vault, "Only vault"); + _; + } + + constructor(address _underlying, address _pool, address _aToken, address _vault) { require(_underlying != address(0), "Zero address underlying"); require(_pool != address(0), "Zero address pool"); require(_aToken != address(0), "Zero address aToken"); + require(_vault != address(0), "Zero address vault"); underlying = IERC20(_underlying); pool = IAavePool(_pool); aToken = IERC20(_aToken); + vault = _vault; lastLiquidityIndex = getAaveLiquidityIndex(); } @@ -59,7 +67,7 @@ contract AaveStrategy is IStrategy { return uint256(data.liquidityIndex); } - function deposit(uint256 amount) external override { + function deposit(uint256 amount) external override onlyVault { // Update simulated balance before receiving new funds storedBalance = currentValue(); lastLiquidityIndex = getAaveLiquidityIndex(); @@ -74,7 +82,7 @@ contract AaveStrategy is IStrategy { storedBalance += amount; } - function withdraw(uint256 amount) external override returns (uint256 actualOut) { + function withdraw(uint256 amount) external override onlyVault returns (uint256 actualOut) { storedBalance = currentValue(); lastLiquidityIndex = getAaveLiquidityIndex(); diff --git a/contracts/src/strategies/IdleStrategy.sol b/contracts/src/strategies/IdleStrategy.sol index e207bc3..e9d2a73 100644 --- a/contracts/src/strategies/IdleStrategy.sol +++ b/contracts/src/strategies/IdleStrategy.sol @@ -9,18 +9,26 @@ contract IdleStrategy is IStrategy { using SafeERC20 for IERC20; IERC20 public immutable underlying; + address public immutable vault; string public constant STRATEGY_NAME = "Idle USDC Strategy"; - constructor(address _underlying) { + modifier onlyVault() { + require(msg.sender == vault, "Only vault"); + _; + } + + constructor(address _underlying, address _vault) { require(_underlying != address(0), "Zero address"); + require(_vault != address(0), "Zero address vault"); underlying = IERC20(_underlying); + vault = _vault; } - function deposit(uint256 amount) external override { + function deposit(uint256 amount) external override onlyVault { underlying.safeTransferFrom(msg.sender, address(this), amount); } - function withdraw(uint256 amount) external override returns (uint256 actualOut) { + function withdraw(uint256 amount) external override onlyVault returns (uint256 actualOut) { underlying.safeTransfer(msg.sender, amount); return amount; } diff --git a/contracts/src/strategies/MockYieldStrategy.sol b/contracts/src/strategies/MockYieldStrategy.sol index e378b85..d7f6b82 100644 --- a/contracts/src/strategies/MockYieldStrategy.sol +++ b/contracts/src/strategies/MockYieldStrategy.sol @@ -20,6 +20,11 @@ contract MockYieldStrategy is IStrategy { uint256 public bpsPerBlock; // 1 bps = 100 (using 6 decimals for precision, e.g. 1000 = 0.1% per block) uint256 public storedBalance; + modifier onlyVault() { + require(msg.sender == vault, "Only vault"); + _; + } + constructor(address _underlying, address _vault, uint256 _bpsPerBlock) { require(_underlying != address(0), "Zero address underlying"); require(_vault != address(0), "Zero address vault"); @@ -46,7 +51,7 @@ contract MockYieldStrategy is IStrategy { return baseAmount + yield; } - function deposit(uint256 amount) external override { + function deposit(uint256 amount) external override onlyVault { storedBalance = _accrueYield(); lastUpdateBlock = block.number; @@ -54,7 +59,7 @@ contract MockYieldStrategy is IStrategy { storedBalance += amount; } - function withdraw(uint256 amount) external override returns (uint256 actualOut) { + function withdraw(uint256 amount) external override onlyVault returns (uint256 actualOut) { storedBalance = _accrueYield(); lastUpdateBlock = block.number; diff --git a/contracts/test/AgentFloatHook.t.sol b/contracts/test/AgentFloatHook.t.sol index be5a799..d8d11ef 100644 --- a/contracts/test/AgentFloatHook.t.sol +++ b/contracts/test/AgentFloatHook.t.sol @@ -34,7 +34,7 @@ contract AgentFloatHookTest is Test, Deployers { vault = new FloatVault(address(mockUsdc)); // 3. Deploy IdleStrategy and register it in the Vault - idleStrategy = new IdleStrategy(address(mockUsdc)); + idleStrategy = new IdleStrategy(address(mockUsdc), address(vault)); vault.registerStrategy(address(idleStrategy), false); // Active strategy // 4. Determine target address with correct permission flags encoded in low 14 bits diff --git a/contracts/test/FlapYieldTaxVault.t.sol b/contracts/test/FlapYieldTaxVault.t.sol index d9de886..946dec9 100644 --- a/contracts/test/FlapYieldTaxVault.t.sol +++ b/contracts/test/FlapYieldTaxVault.t.sol @@ -23,7 +23,7 @@ contract FlapYieldTaxVaultTest is Test { mockUsdc = new MockUSDC(); floatVault = new FloatVault(address(mockUsdc)); - idle = new IdleStrategy(address(mockUsdc)); + idle = new IdleStrategy(address(mockUsdc), address(floatVault)); floatVault.registerStrategy(address(idle), false); // Register idle as active (ID 1) factory = new FlapYieldTaxVaultFactory(address(floatVault), guardian); diff --git a/contracts/test/FloatVault.t.sol b/contracts/test/FloatVault.t.sol index 9f5ef2b..1c0367c 100644 --- a/contracts/test/FloatVault.t.sol +++ b/contracts/test/FloatVault.t.sol @@ -23,7 +23,7 @@ contract FloatVaultTest is Test { usdc = new MockUSDC(); vault = new FloatVault(address(usdc)); - idleStrategy = new IdleStrategy(address(usdc)); + idleStrategy = new IdleStrategy(address(usdc), address(vault)); yieldStrategy = new MockYieldStrategy(address(usdc), address(vault), 1000); // 1000 = 0.1% per block vault.setPromoter(promoter); @@ -77,6 +77,32 @@ contract FloatVaultTest is Test { vm.stopPrank(); } + function test_Revert_DirectStrategyWithdraw() public { + // Fund the active strategy via the vault + vm.startPrank(owner); + vault.registerStrategy(address(idleStrategy), false); + vm.stopPrank(); + + vm.startPrank(user); + usdc.approve(address(vault), 500 * 10**6); + vault.park(500 * 10**6); + vm.stopPrank(); + + // An attacker must NOT be able to drain the strategy directly, bypassing the vault. + address attacker = address(0xBAD); + vm.prank(attacker); + vm.expectRevert(bytes("Only vault")); + idleStrategy.withdraw(500 * 10**6); + + // Direct deposit is also gated. + vm.prank(attacker); + vm.expectRevert(bytes("Only vault")); + idleStrategy.deposit(1); + + // Funds remain intact in the strategy. + assertEq(usdc.balanceOf(address(idleStrategy)), 500 * 10**6); + } + function test_PostScore() public { vm.startPrank(owner); vault.registerStrategy(address(idleStrategy), false); diff --git a/docs/pitch-deck.md b/docs/pitch-deck.md index 6ddaf35..ec8fc54 100644 --- a/docs/pitch-deck.md +++ b/docs/pitch-deck.md @@ -319,9 +319,11 @@ section::after { --- -## Concentrated liquidity can go quiet. +## Out-of-range capital collapses pool depth. -When a v4 LP position drifts out of range, that capital stops earning trading fees. AgentFloat treats the idle interval as yield inventory, then recalls it when swaps need liquidity again. +When a concentrated position drifts out of range, depth collapses and the pool's effective spread widens—allowing arbitrageurs to exploit it before rebalancers can act. + +AgentFloat sweeps this idle capital to earn yield, but recalls it just-in-time when swaps occur, instantly restoring depth and tight spreads.
v4hook surface
@@ -405,7 +407,7 @@ The AI can propose and deploy shadows under policy. Real capital is gated by the | AgentFloatHook | `0x5Ba6671e…4580` | permission bits `0x580` | | FloatVault | `0xbF06de10…a6f4` | strategy registry | | Flap Factory | `0x87D665B8…24F8` | yield tax vaults | -| AaveStrategy | `0x4C109f12…56E1` | real Aave V3 USDT | +| AaveStrategy | `0xB433487F…bFEa` | real Aave V3 USDT (`onlyVault`-guarded) | | IdleStrategy | `0xf292e500…5EEb` | baseline | | PoolManager | `0x360e68fa…fb32` | canonical v4 | | USDT / USDC | `0x779Ded0c…3736` | USDt0 | @@ -482,7 +484,7 @@ The AI can propose and deploy shadows under policy. Real capital is gated by the
4,800+scoring epochs posted to chain
2,214epochs analyzed by consolidator
6LLM proposals across 36h
-
12/12Forge tests passing
+
13/13Forge tests passing
2 autonomous on-chain promotions recorded in README; history wiki currently consolidates 1 structured promotion event.
diff --git a/docs/submission.md b/docs/submission.md index 58ede6e..de86804 100644 --- a/docs/submission.md +++ b/docs/submission.md @@ -32,15 +32,15 @@ All contracts are live on X Layer Mainnet: | **FloatVault** | `0xbF06de108735332D1EDb81C7A77A750DD428a6f4` | [OKLink](https://www.oklink.com/xlayer/address/0xbF06de108735332D1EDb81C7A77A750DD428a6f4) | | **FlapYieldTaxVaultFactory** | `0x87D665B83557365ADf320a439B8a2DFD03c024F8` | [OKLink](https://www.oklink.com/xlayer/address/0x87D665B83557365ADf320a439B8a2DFD03c024F8) | | **IdleStrategy** (ID 1) | `0xf292e500459393F5CfaF8fbccFe1426bC3495EEb` | [OKLink](https://www.oklink.com/xlayer/address/0xf292e500459393F5CfaF8fbccFe1426bC3495EEb) | -| **AaveStrategy** (ID 2, ACTIVE) | `0x4C109f12d2FA55037439b73CE4E9Ee2C1e1656E1` | [OKLink](https://www.oklink.com/xlayer/address/0x4C109f12d2FA55037439b73CE4E9Ee2C1e1656E1) | +| **AaveStrategy** (ID 4, ACTIVE) | `0xB433487F82572FF201A2455BF7a06325a7B8bFEa` | [OKLink](https://www.oklink.com/xlayer/address/0xB433487F82572FF201A2455BF7a06325a7B8bFEa) | | **USDC/USDT0 (underlying)** | `0x779Ded0c9e1022225f8E0630b35a9b54bE713736` | [OKLink](https://www.oklink.com/xlayer/address/0x779Ded0c9e1022225f8E0630b35a9b54bE713736) | | **Uniswap v4 PoolManager (canonical)** | `0x360e68faccca8ca495c1b759fd9eee466db9fb32` | [OKLink](https://www.oklink.com/xlayer/address/0x360e68faccca8ca495c1b759fd9eee466db9fb32) | -> A third strategy, `MockYieldStrategy` (`0x5954F08A…`), exists for the testnet promotion-loop demo and is retired on mainnet. The mainnet active strategy is the real Aave V3 integration. +> Strategy IDs 2 (`0x4C109f12…`, prior AaveStrategy) and 3 (`MockYieldStrategy`) are retired shadows. ID 2 was superseded by ID 4 after a security pass added an `onlyVault` guard to all strategy entry points; pooled capital was migrated to ID 4 trustlessly via the on-chain `promote()` path. The mainnet active strategy (ID 4) is the real, hardened Aave V3 integration. **Live yield — verifiable on-chain right now:** The active AaveStrategy holds a real interest-bearing **aUSDT** position supplied to Aave V3. Query on chain 196: -`aUSDT.balanceOf(0x4C109f12d2FA55037439b73CE4E9Ee2C1e1656E1)` → non-zero, growing block-by-block. +`aUSDT.balanceOf(0xB433487F82572FF201A2455BF7a06325a7B8bFEa)` → non-zero, growing block-by-block. aUSDT token: `0xF356ae412dB5df43BD3a10746f7ad4e1C4De4297` --- @@ -53,7 +53,7 @@ Concentrated liquidity (Uniswap v4) is highly efficient, but has a major drawbac ### The AgentFloat Solution AgentFloat is a Uniswap v4 Hook that keeps pool capital productive. * **Out-of-Range Sweeps:** When liquidity becomes out-of-range, the Hook (`afterModifyLiquidity`) automatically routes the idle capital into the `FloatVault` to generate yield in an active strategy (Aave V3). -* **JIT (Just-In-Time) Recall:** The moment a swap comes in that requires the liquidity, the Hook's `beforeSwap` method instantly pulls the capital back into the pool. Traders experience zero execution delay, and LPs capture yield while waiting. +* **JIT (Just-In-Time) Recall:** Normally, when a large position goes out-of-range, liquidity depth collapses and the pool's effective spread widens—allowing arbitrageurs to exploit it before rebalancers can respond. AgentFloat's Hook solves this. The moment a swap comes in that requires the liquidity, the Hook's `beforeSwap` method instantly recalls the capital back into the pool just-in-time, restoring full depth and tight spreads. LPs capture yield while waiting, and traders experience zero execution delay. * **Self-Improving AI Loop:** An off-chain AI agent continuously tests alternative "shadow" yield strategies. If a shadow strategy beats the active baseline over consecutive blocks, its win counter increases. * **Trustless Promotion:** Once a strategy is proven on-chain, anyone can trustlessly call `promote()` to migrate the entire pooled capital to the new strategy. @@ -65,7 +65,7 @@ AgentFloat is a Uniswap v4 Hook that keeps pool capital productive. ### 1. Hook Relevance Could this have been built as a normal vault without a Uniswap v4 Hook? -**No.** The Hook is a native extension that lets AgentFloat intercept transactions at the pool execution layer. This allows the protocol to dynamically shift capital between active yield strategies and the pool based on real-time swap demand (JIT recall). Using EIP-1153 transient storage, we cache parked amounts within the swap lifecycle to achieve ultra-optimized gas profiles. +**No.** The Hook is a native extension that lets AgentFloat intercept transactions at the pool execution layer. When price moves out of range, depth collapses and the spread widens. By intercepting swap execution, our Hook dynamically recalls capital to the pool just-in-time (JIT recall) before the swap executes, instantly restoring depth and protecting the pool from arbitrage exploitation. Using EIP-1153 transient storage, we cache parked amounts within the swap lifecycle to achieve ultra-optimized gas profiles. ### 2. Innovation Instead of hardcoding a static routing path, AgentFloat introduces a self-improving strategy layer: @@ -77,4 +77,4 @@ Instead of hardcoding a static routing path, AgentFloat introduces a self-improv * **Flap Protocol Integration:** Flap token creators collect transaction taxes that typically sit idle in contract balances. We deployed `FlapYieldTaxVaultFactory` to automatically generate yield-bearing tax vaults, routing those idle collections directly into Aave V3 via `FloatVault` and enhancing utility for the entire Flap token launch ecosystem. ### 4. Completion -12/12 Forge tests pass, including end-to-end integration flows. Active daemon runs on X Layer Mainnet, actively scoring strategies, posting scores, and parsing obsidian specs. +13/13 Forge tests pass, including end-to-end integration flows and a strategy-drain access-control regression test. Active daemon runs on X Layer Mainnet, actively scoring strategies, posting scores, and parsing obsidian specs. diff --git a/docs/voiceover_script.md b/docs/voiceover_script.md new file mode 100644 index 0000000..05eaf95 --- /dev/null +++ b/docs/voiceover_script.md @@ -0,0 +1,28 @@ +# AgentFloat Voiceover Script (1 Min 20 Secs) + +**Target Duration:** ~80 seconds (1 minute and 20 seconds) +**Tone:** Clear, paced, engaging, and professional. +**Total Word Count:** 189 words (~142 words per minute reading pace) + +--- + +### [0:00 - 0:12] Introduction +In Uniswap v4, concentrated liquidity is a game-changer. But when the market moves and an LP's position goes out of range, their capital sits completely idle, earning zero fees. + +### [0:12 - 0:19] The Solution +AgentFloat fixes this inefficiency using a smart Uniswap v4 Hook. + +### [0:19 - 0:34] Hook Core Mechanism: Part 1 (Sweeping) +Here is the core mechanism: When pool liquidity goes out of range, the hook automatically sweeps the idle capital into the FloatVault, where it generates yield in active strategies like Aave V3. + +### [0:34 - 0:54] Hook Core Mechanism: Part 2 (Just-In-Time Recall) +But what happens when the price returns? Normally, when a large position goes out of range, liquidity depth collapses and the effective spread widens—which arbitrageurs exploit before rebalancers can respond. AgentFloat fixes this. The moment a swap is initiated, the hook instantly recalls the capital back into the pool just-in-time, restoring full depth and tight spreads. + +### [0:54 - 1:07] Use Case: Ecosystem Taxes (Flap) +This mechanism also extends to launchpads like Flap. Instead of collected transaction taxes sitting idle in static treasury contracts, they are swept directly to the vault to earn yield. + +### [1:07 - 1:15] Optimization Loop & Security +Under the hood, an off-chain AI suggests optimized strategy updates, but they must prove their performance on-chain before being promoted. + +### [1:15 - 1:20] Call to Action +AgentFloat is live on X Layer Mainnet. Put your idle liquidity to work. diff --git a/web/src/components/HookDiagram.tsx b/web/src/components/HookDiagram.tsx index 0ca9e34..fcffb17 100644 --- a/web/src/components/HookDiagram.tsx +++ b/web/src/components/HookDiagram.tsx @@ -8,6 +8,11 @@ interface State { health: { lastEpochAt: string | null }; proposals: Array<{ executed_at?: string; status: string }>; strategies: Array<{ strategy_id: number; name: string; status: string }>; + contracts?: { + vault: string; + hook: string; + explorerBase: string; + }; } /** @@ -46,6 +51,13 @@ export function HookDiagram() { const shadowCount = state?.strategies?.filter((s) => s.status === "shadow" || s.status === "paused").length ?? 0; + const hookAddress = state?.contracts?.hook || "0x5Ba6671e8219C34edA373BF95895306929174580"; + const vaultAddress = state?.contracts?.vault || "0xbF06de108735332D1EDb81C7A77A750DD428a6f4"; + const explorerBase = state?.contracts?.explorerBase || "https://www.oklink.com/xlayer"; + + const hookShort = `${hookAddress.slice(0, 6)}…${hookAddress.slice(-4)}`; + const vaultShort = `${vaultAddress.slice(0, 6)}…${vaultAddress.slice(-4)}`; + return (
@@ -72,8 +84,8 @@ export function HookDiagram() { {/* Hook */} @@ -81,8 +93,8 @@ export function HookDiagram() { {/* Vault */}
diff --git a/web/src/components/StrategyDeck.tsx b/web/src/components/StrategyDeck.tsx index b7d9d8a..c0644d8 100644 --- a/web/src/components/StrategyDeck.tsx +++ b/web/src/components/StrategyDeck.tsx @@ -22,6 +22,9 @@ interface Score { interface State { strategies: Strategy[]; + contracts?: { + explorerBase: string; + }; } export function StrategyDeck() { @@ -30,6 +33,7 @@ export function StrategyDeck() { const [flipped, setFlipped] = useState(null); const strategies = state?.strategies ?? []; + const explorerBase = state?.contracts?.explorerBase || "https://www.oklink.com/xlayer"; const scoresByStrategy = useMemo(() => { const map = new Map(); for (const s of scoresResp?.scores ?? []) { @@ -67,6 +71,7 @@ export function StrategyDeck() { scores={scoresByStrategy.get(s.strategy_id) ?? []} isFlipped={flipped === s.strategy_id} onFlip={() => setFlipped(flipped === s.strategy_id ? null : s.strategy_id)} + explorerBase={explorerBase} /> ))} @@ -81,11 +86,13 @@ function StrategyCard({ scores, isFlipped, onFlip, + explorerBase, }: { strategy: Strategy; scores: number[]; isFlipped: boolean; onFlip: () => void; + explorerBase: string; }) { const status = strategy.status; const accent = @@ -176,7 +183,7 @@ function StrategyCard({ Contract e.stopPropagation()} diff --git a/web/src/lib/brain.ts b/web/src/lib/brain.ts index 70cea70..1a45948 100644 --- a/web/src/lib/brain.ts +++ b/web/src/lib/brain.ts @@ -207,10 +207,22 @@ export function loadRealState(): DashboardState { strategies, health, contracts: { - vault: '0x4d33FD7B077c1a23221252c3FFEe4261c8a67c5f', - hook: '0x3A00B5A2F15bE68AfE5415290ca4D3022e3B3b5F', - poolManager: '0x1BB8824110DF8ED603eBb203C19cC2Ba8FdA8fbe', - explorerBase: 'https://www.oklink.com/xlayer-test', + vault: (process.env.NEXT_PUBLIC_VAULT_ADDRESS || + (process.env.NEXT_PUBLIC_CHAIN_ID === '1952' || process.env.X_LAYER_CHAIN_ID === '1952' + ? '0x4d33FD7B077c1a23221252c3FFEe4261c8a67c5f' + : '0xbF06de108735332D1EDb81C7A77A750DD428a6f4')) as `0x${string}`, + hook: (process.env.NEXT_PUBLIC_HOOK_ADDRESS || + (process.env.NEXT_PUBLIC_CHAIN_ID === '1952' || process.env.X_LAYER_CHAIN_ID === '1952' + ? '0x3A00B5A2F15bE68AfE5415290ca4D3022e3B3b5F' + : '0x5Ba6671e8219C34edA373BF95895306929174580')) as `0x${string}`, + poolManager: (process.env.NEXT_PUBLIC_POOL_MANAGER_ADDRESS || + (process.env.NEXT_PUBLIC_CHAIN_ID === '1952' || process.env.X_LAYER_CHAIN_ID === '1952' + ? '0x1BB8824110DF8ED603eBb203C19cC2Ba8FdA8fbe' + : '0x360e68faccca8ca495c1b759fd9eee466db9fb32')) as `0x${string}`, + explorerBase: + process.env.NEXT_PUBLIC_CHAIN_ID === '1952' || process.env.X_LAYER_CHAIN_ID === '1952' + ? 'https://www.oklink.com/xlayer-test' + : 'https://www.oklink.com/xlayer', }, isDemo: false, }; diff --git a/web/src/lib/demo.ts b/web/src/lib/demo.ts index dc43bb3..20a4b23 100644 --- a/web/src/lib/demo.ts +++ b/web/src/lib/demo.ts @@ -150,10 +150,10 @@ export function getDemoState(): DashboardState { testsTotal: 8, }, contracts: { - vault: '0x4d33FD7B077c1a23221252c3FFEe4261c8a67c5f', - hook: '0x3A00B5A2F15bE68AfE5415290ca4D3022e3B3b5F', - poolManager: '0x1BB8824110DF8ED603eBb203C19cC2Ba8FdA8fbe', - explorerBase: 'https://www.oklink.com/xlayer-test', + vault: '0xbF06de108735332D1EDb81C7A77A750DD428a6f4', + hook: '0x5Ba6671e8219C34edA373BF95895306929174580', + poolManager: '0x360e68faccca8ca495c1b759fd9eee466db9fb32', + explorerBase: 'https://www.oklink.com/xlayer', }, isDemo: true, }; diff --git a/web/src/lib/vault.ts b/web/src/lib/vault.ts index 7d45d80..e1f12b0 100644 --- a/web/src/lib/vault.ts +++ b/web/src/lib/vault.ts @@ -10,6 +10,14 @@ import { type Address, } from "viem"; +export const X_LAYER_MAINNET = { + id: 196, + name: "X Layer Mainnet", + nativeCurrency: { name: "OKB", symbol: "OKB", decimals: 18 }, + rpcUrls: { default: { http: ["https://rpc.xlayer.tech"] } }, + blockExplorers: { default: { name: "OKLink", url: "https://www.oklink.com/xlayer" } }, +} as const; + export const X_LAYER_TESTNET = { id: 1952, name: "X Layer Testnet", @@ -18,11 +26,14 @@ export const X_LAYER_TESTNET = { blockExplorers: { default: { name: "OKLink", url: "https://www.oklink.com/xlayer-test" } }, } as const; +const chainId = parseInt(process.env.NEXT_PUBLIC_CHAIN_ID || "196", 10); +export const ACTIVE_CHAIN = chainId === 196 ? X_LAYER_MAINNET : X_LAYER_TESTNET; + export const VAULT_ADDRESS = (process.env.NEXT_PUBLIC_VAULT_ADDRESS || - "0x4d33FD7B077c1a23221252c3FFEe4261c8a67c5f") as Address; + (chainId === 196 ? "0xbF06de108735332D1EDb81C7A77A750DD428a6f4" : "0x4d33FD7B077c1a23221252c3FFEe4261c8a67c5f")) as Address; export const USDC_ADDRESS = (process.env.NEXT_PUBLIC_USDC_ADDRESS || - "0x39684D42654752F246449e84524Fc972D57Ef985") as Address; + (chainId === 196 ? "0x779Ded0c9e1022225f8E0630b35a9b54bE713736" : "0x39684D42654752F246449e84524Fc972D57Ef985")) as Address; // MockUSDC uses 18 decimals (it's a plain ERC20 mock). Real USDC is 6. export const USDC_DECIMALS = 18; @@ -93,7 +104,7 @@ export const VAULT_ABI = [ export function publicClient() { return createPublicClient({ - chain: X_LAYER_TESTNET as any, + chain: ACTIVE_CHAIN as any, transport: http(), }); } @@ -103,7 +114,7 @@ export function walletClient() { const eth = (window as any).ethereum; if (!eth) throw new Error("No wallet"); return createWalletClient({ - chain: X_LAYER_TESTNET as any, + chain: ACTIVE_CHAIN as any, transport: custom(eth), }); } @@ -148,7 +159,7 @@ export async function mintTestUsdc(account: Address, amountHuman: string): Promi const wc = walletClient(); const hash = await wc.writeContract({ account, - chain: X_LAYER_TESTNET as any, + chain: ACTIVE_CHAIN as any, address: USDC_ADDRESS, abi: ERC20_ABI, functionName: "mint", @@ -163,7 +174,7 @@ export async function approveVault(account: Address, amountHuman: string): Promi const wc = walletClient(); const hash = await wc.writeContract({ account, - chain: X_LAYER_TESTNET as any, + chain: ACTIVE_CHAIN as any, address: USDC_ADDRESS, abi: ERC20_ABI, functionName: "approve", @@ -178,7 +189,7 @@ export async function park(account: Address, amountHuman: string): Promise<`0x${ const wc = walletClient(); const hash = await wc.writeContract({ account, - chain: X_LAYER_TESTNET as any, + chain: ACTIVE_CHAIN as any, address: VAULT_ADDRESS, abi: VAULT_ABI, functionName: "park", @@ -193,7 +204,7 @@ export async function withdrawFromVault(account: Address, amountHuman: string): const wc = walletClient(); const hash = await wc.writeContract({ account, - chain: X_LAYER_TESTNET as any, + chain: ACTIVE_CHAIN as any, address: VAULT_ADDRESS, abi: VAULT_ABI, functionName: "withdraw",