Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
# ─── TESTNET (current default) ────────────────────────────────────────
X_LAYER_RPC_URL=https://testrpc.xlayer.tech/terigon
X_LAYER_CHAIN_ID=1952

# ─── MAINNET (for production deployment) ──────────────────────────────
# Switch by setting these and reusing X_LAYER_RPC_URL/CHAIN_ID variables
# X_LAYER_RPC_URL=https://rpc.xlayer.tech
# X_LAYER_CHAIN_ID=196
# Canonical mainnet infra (no need to redeploy):
# PoolManager = 0x360E68faCCca8cA495c1B759Fd9EEe466dB9FB32
# Aave Pool = 0xE3F3Caefdd7180F884c01E57f65Df979Af84f116
# USDT = 0x779Ded0c9e1022225f8E0630b35a9b54bE713736
# aUSDT = 0xF356ae412dB5df43BD3a10746f7ad4e1C4De4297
# After mainnet deploy, fill in:
# VAULT_ADDRESS=
# HOOK_ADDRESS=
# HOOK_SALT=

# Address that will be set as the promoter on FloatVault (optional, defaults to DEPLOYER)
PROMOTER_ADDRESS=
DEPLOYER_ADDRESS=
DEPLOYER_PRIVATE_KEY=
PROMOTER_PRIVATE_KEY=
USDC_ADDRESS=
Expand Down
10 changes: 10 additions & 0 deletions agent/src/approver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,16 @@ function decide(rec: ProposalRecord, mode: OperatingMode, ctx: { todayCount: num

// ── Hard guardrails (apply in every mode except `watch`) ─────────────
if (mode.mode !== 'watch') {
// Chain-scoped allowlist — mainnet bounds the AI to register-from-library actions
const chainId = process.env.X_LAYER_CHAIN_ID || '0';
const allowedForChain = mode.chain_actions_allowed?.[chainId];
if (allowedForChain && allowedForChain.length > 0 && !allowedForChain.includes(action)) {
return {
newStatus: 'rejected_by_rule',
reason: `action_type "${action}" not in chain_actions_allowed for chain ${chainId} (mainnet bounds AI to library register/retire/scoring)`,
};
}

if (mode.blocked_action_types.includes(action)) {
return { newStatus: 'rejected_by_rule', reason: `action_type "${action}" is in blocked_action_types` };
}
Expand Down
20 changes: 18 additions & 2 deletions agent/src/brain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ export function loadStrategySpecs(): StrategySpec[] {
const parsed = matter(raw);
const data = parsed.data;

// Skip retired strategies
if (data.status === 'retired') continue;
// Do not skip retired strategies, so they are kept in sync in the SQLite DB
// and displayed correctly on the dashboard.

// Validate required fields
if (typeof data.strategy_id !== 'number' || !data.contract_address) {
Expand Down Expand Up @@ -167,6 +167,15 @@ export interface OperatingMode {
blocked_action_types: string[];
require_security_audit_for: string[];
veto_window_hours: Record<string, number>;
/**
* Chain-scoped allowlist. The agent reads `process.env.X_LAYER_CHAIN_ID` and
* applies the matching entry. Action types not listed are blocked on that chain.
*
* The intent: on mainnet, the AI can register/un-register strategies from a
* pre-audited library, but cannot deploy new Solidity. New strategy contracts
* require human signoff + audit before being added to the library.
*/
chain_actions_allowed: Record<string, string[]>;
sourceFile: string;
}

Expand All @@ -181,6 +190,13 @@ const DEFAULT_MODE: Omit<OperatingMode, 'sourceFile'> = {
blocked_action_types: [],
require_security_audit_for: ['new_strategy'],
veto_window_hours: {},
// chain "0" or absent = no chain-scoped restriction (testnet default permissive)
chain_actions_allowed: {
// X Layer mainnet (chain 196) — only safe actions on real money
'196': ['register_strategy', 'retire', 'scoring_change', 'no_action'],
// X Layer testnet (chain 1952) — everything goes for experimentation
'1952': ['parameter_variant', 'new_strategy', 'scoring_change', 'retire', 'no_action', 'register_strategy'],
},
};

/**
Expand Down
2 changes: 1 addition & 1 deletion agent/src/consolidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ function computeStats(entries: ScoreEntry[], specs: StrategySpec[]): StrategySta
stats.push({
spec: specsById.get(strategyId),
strategyId,
strategyName: strategyEntries[0].strategyName,
strategyName: specsById.get(strategyId)?.name || strategyEntries[strategyEntries.length - 1].strategyName,
totalObservations: strategyEntries.length,
firstSeen: strategyEntries[0].timestamp,
lastSeen: strategyEntries[strategyEntries.length - 1].timestamp,
Expand Down
13 changes: 8 additions & 5 deletions agent/src/deployer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,13 +157,16 @@ export async function runDeployer(): Promise<number> {
const specFilename = `mock-yield-strategy-v${strategyId}.md`;
const specPath = path.join(SPECS_DIR, specFilename);
const expectedApyBps = Number(bpsPerBlock) / 2; // approximation for expected apy bps based on rate
const isMainnet = CONFIG.chainId === 196;
const networkName = isMainnet ? 'x-layer-mainnet' : 'x-layer-testnet';
const explorerUrl = xLayerTestnetChain.blockExplorers.default.url;

const specContent = `---
name: agentfloat-strategy-mock-yield-v${strategyId}
strategy_id: ${strategyId}
contract_address: "${newStrategyAddress}"
network: x-layer-testnet
chain_id: 1952
network: ${networkName}
chain_id: ${CONFIG.chainId}
status: shadow
is_shadow: true
expected_apy_bps: ${expectedApyBps}
Expand All @@ -179,7 +182,7 @@ Parameter variant deployed autonomously by the AgentFloat deploy loop.
## Parameters
- **bpsPerBlock**: ${bpsPerBlock} (contract scale)
- **Source Proposal**: [${filename}](./proposals/${filename})
- **Tx Hash**: [${deployHash}](https://www.oklink.com/xlayer-test/tx/${deployHash})
- **Tx Hash**: [${deployHash}](${explorerUrl}/tx/${deployHash})
`;
fs.writeFileSync(specPath, specContent);
console.log(`[Deployer] Wrote strategy spec to ${specPath}`);
Expand All @@ -203,9 +206,9 @@ Parameter variant deployed autonomously by the AgentFloat deploy loop.
- **Type:** parameter_variant
- **Strategy ID:** ${strategyId}
- **Strategy Name:** Mock Yield Strategy (v${strategyId})
- **Deployed Address:** [${newStrategyAddress}](https://www.oklink.com/xlayer-test/address/${newStrategyAddress})
- **Deployed Address:** [${newStrategyAddress}](${explorerUrl}/address/${newStrategyAddress})
- **Constructor Arg (bpsPerBlock):** ${bpsPerBlock}
- **Tx:** [${deployHash}](https://www.oklink.com/xlayer-test/tx/${deployHash})
- **Tx:** [${deployHash}](${explorerUrl}/tx/${deployHash})
- **Time:** ${new Date().toISOString()}
`;
fs.appendFileSync(journalPath, journalEntry);
Expand Down
12 changes: 8 additions & 4 deletions agent/src/scorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ export interface StrategyResult {
score: bigint;
}

// Custom X Layer testnet chain definition
// Custom X Layer chain definition (dynamically configured via env)
const isMainnet = CONFIG.chainId === 196;
export const xLayerTestnetChain = {
id: 1952,
name: 'X Layer Testnet',
id: CONFIG.chainId,
name: isMainnet ? 'X Layer Mainnet' : 'X Layer Testnet',
nativeCurrency: {
decimals: 18,
name: 'OKB',
Expand All @@ -30,7 +31,10 @@ export const xLayerTestnetChain = {
public: { http: [CONFIG.rpcUrl] },
},
blockExplorers: {
default: { name: 'OKLink', url: 'https://www.oklink.com/xlayer-test' },
default: {
name: 'OKLink',
url: isMainnet ? 'https://www.oklink.com/xlayer' : 'https://www.oklink.com/xlayer-test'
},
},
};

Expand Down
29 changes: 24 additions & 5 deletions contracts/script/Deploy.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import "../src/FloatVault.sol";
import "../src/AgentFloatHook.sol";
import "../src/strategies/IdleStrategy.sol";
import "../src/strategies/MockYieldStrategy.sol";
import "../src/strategies/AaveStrategy.sol";
import "../test/mocks/MockUSDC.sol";
import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";
import {PoolManager} from "v4-core/src/PoolManager.sol";
Expand Down Expand Up @@ -45,17 +46,35 @@ contract DeployScript is Script {
IdleStrategy idle = new IdleStrategy(usdcAddr);
console.log("Deployed IdleStrategy at:", address(idle));

MockYieldStrategy yieldStrat = new MockYieldStrategy(usdcAddr, address(vault), 1000); // 10 bps per block
console.log("Deployed MockYieldStrategy at:", address(yieldStrat));

// 5. Register strategies in FloatVault
vault.registerStrategy(address(idle), false); // active (id = 1)
vault.registerStrategy(address(yieldStrat), true); // shadow (id = 2)

if (block.chainid == 196) {
// Deploy AaveStrategy
// Aave Pool on X Layer: 0xE3F3Caefdd7180F884c01E57f65Df979Af84f116
// aUSDT0 token on X Layer: 0xF356ae412dB5df43BD3a10746f7ad4e1C4De4297
AaveStrategy aaveStrat = new AaveStrategy(
usdcAddr,
0xE3F3Caefdd7180F884c01E57f65Df979Af84f116,
0xF356ae412dB5df43BD3a10746f7ad4e1C4De4297
);
console.log("Deployed AaveStrategy at:", address(aaveStrat));
vault.registerStrategy(address(aaveStrat), true); // shadow (id = 2)

// Deploy MockYieldStrategy as secondary shadow
MockYieldStrategy mockStrat = new MockYieldStrategy(usdcAddr, address(vault), 200); // 2 bps per block
console.log("Deployed MockYieldStrategy at:", address(mockStrat));
vault.registerStrategy(address(mockStrat), true); // shadow (id = 3)
} else {
MockYieldStrategy yieldStrat = new MockYieldStrategy(usdcAddr, address(vault), 1000); // 10 bps per block
console.log("Deployed MockYieldStrategy at:", address(yieldStrat));
vault.registerStrategy(address(yieldStrat), true); // shadow (id = 2)
}
console.log("Registered strategies in FloatVault");

// 6. Mine hook salt & deploy AgentFloatHook
bytes32 salt = mineHookSalt(
deployer,
0x4e59b44847b379578588920cA78FbF26c0B4956C,
poolManagerAddr,
address(vault)
);
Expand Down
27 changes: 27 additions & 0 deletions contracts/script/DeployFlapFactory.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "forge-std/Script.sol";
import "../src/flap/FlapYieldTaxVaultFactory.sol";

contract DeployFlapFactoryScript is Script {
function run() public {
uint256 deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY");
address deployer = vm.addr(deployerPrivateKey);

address floatVault = vm.envOr("VAULT_ADDRESS", address(0));
require(floatVault != address(0), "Zero vault address");

vm.startBroadcast(deployerPrivateKey);

// Deploy FlapYieldTaxVaultFactory using the deployer address as the guardian backup
FlapYieldTaxVaultFactory factory = new FlapYieldTaxVaultFactory(
floatVault,
deployer
);

console.log("Deployed FlapYieldTaxVaultFactory at:", address(factory));

vm.stopBroadcast();
}
}
83 changes: 83 additions & 0 deletions contracts/script/DeployMainnet.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "forge-std/Script.sol";
import {FloatVault} from "../src/FloatVault.sol";
import {AgentFloatHook} from "../src/AgentFloatHook.sol";
import {IdleStrategy} from "../src/strategies/IdleStrategy.sol";
import {AaveStrategy} from "../src/strategies/AaveStrategy.sol";
import {Hooks} from "v4-core/src/libraries/Hooks.sol";
import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";
import {XLayerMainnet} from "./XLayerMainnet.sol";

/// @title Deploy to X Layer mainnet
/// @notice Attaches AgentFloat to the canonical Uniswap v4 PoolManager on X Layer.
/// Uses real USDT (the asset Aave reserves on X Layer) and the live Aave V3 Pool.
/// Mines a hook salt offline first -set HOOK_SALT in env before running.
///
/// One-shot deploy:
/// forge script script/DeployMainnet.s.sol --rpc-url $X_LAYER_MAINNET_RPC \
/// --broadcast --legacy --priority-gas-price 50000000
///
/// Expected cost: ~8M gas at ~0.1 gwei ≈ 0.0008 OKB ≈ $0.20.
contract DeployMainnet is Script {
function run() public {
require(block.chainid == XLayerMainnet.CHAIN_ID, "Wrong chain -this script targets X Layer mainnet (196)");

uint256 deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY");
address deployer = vm.addr(deployerPrivateKey);
address promoter = vm.envOr("PROMOTER_ADDRESS", deployer);
bytes32 hookSalt = bytes32(vm.envBytes32("HOOK_SALT"));

console.log("=== AgentFloat - X Layer Mainnet Deploy ===");
console.log("Deployer :", deployer);
console.log("Promoter :", promoter);
console.log("PoolManager :", XLayerMainnet.POOL_MANAGER);
console.log("USDT :", XLayerMainnet.USDT);
console.log("Aave Pool :", XLayerMainnet.AAVE_POOL);
console.log("---");

vm.startBroadcast(deployerPrivateKey);

// 1. FloatVault (vault holds USDT, distributes to strategies)
FloatVault vault = new FloatVault(XLayerMainnet.USDT);
if (promoter != deployer) {
vault.setPromoter(promoter);
}
console.log("FloatVault :", address(vault));

// 2. IdleStrategy (baseline floor -holds USDT, no yield)
IdleStrategy idle = new IdleStrategy(XLayerMainnet.USDT);
console.log("IdleStrategy:", address(idle));

// 3. AaveStrategy (real Aave V3 USDT lending market)
AaveStrategy aave = new AaveStrategy(
XLayerMainnet.USDT,
XLayerMainnet.AAVE_POOL,
XLayerMainnet.AUSDT
);
console.log("AaveStrategy:", address(aave));

// 4. AgentFloatHook -deployed via CREATE2 at the mined salt
// The salt must encode permission flags (afterAddLiquidity + afterRemoveLiquidity + beforeSwap)
AgentFloatHook hook = new AgentFloatHook{salt: hookSalt}(
IPoolManager(XLayerMainnet.POOL_MANAGER),
vault
);
console.log("Hook :", address(hook));

// 5. Register strategies on the vault.
// Idle starts active (floor); Aave starts as shadow until it earns promotion.
vault.registerStrategy(address(idle), false); // false = NOT shadow → becomes active
vault.registerStrategy(address(aave), true); // true = shadow
console.log("Strategies registered");

vm.stopBroadcast();

console.log("");
console.log("=== Done ===");
console.log("Add to ~/brain/skills/agentfloat-strategies/<name>.md");
console.log("Update agent/.env with mainnet addresses + X_LAYER_CHAIN_ID=196");
console.log("Restart agent; chain_actions_allowed[196] bounds AI to register/retire/scoring");
}
}
57 changes: 57 additions & 0 deletions contracts/script/MineHookSalt.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "forge-std/Script.sol";
import {AgentFloatHook} from "../src/AgentFloatHook.sol";
import {FloatVault} from "../src/FloatVault.sol";
import {Hooks} from "v4-core/src/libraries/Hooks.sol";
import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";
import {XLayerMainnet} from "./XLayerMainnet.sol";

/// @title Mine a CREATE2 salt for AgentFloatHook
/// @notice Run this BEFORE DeployMainnet — outputs HOOK_SALT to set in env.
///
/// forge script script/MineHookSalt.s.sol --rpc-url $X_LAYER_MAINNET_RPC
///
/// Outputs the salt and predicted hook address. Set HOOK_SALT in .env before running DeployMainnet.
contract MineHookSalt is Script {
uint160 constant REQUIRED_FLAGS =
uint160(Hooks.AFTER_ADD_LIQUIDITY_FLAG)
| uint160(Hooks.AFTER_REMOVE_LIQUIDITY_FLAG)
| uint160(Hooks.BEFORE_SWAP_FLAG);
uint160 constant FLAG_MASK = uint160((1 << 14) - 1);

function run() public view {
address deployer = vm.envAddress("DEPLOYER_ADDRESS");

// The FloatVault address will be deterministic from the deployer's nonce on mainnet.
// For simulation here, we assume the FloatVault has been deployed at the address
// computed from the deployer's nonce at the time of the actual mainnet broadcast.
// To make this practical, deploy in two transactions OR mine after FloatVault is known.

address mockVault = vm.envOr("VAULT_ADDRESS", address(0));
require(mockVault != address(0), "Set VAULT_ADDRESS in env (deploy FloatVault first OR pass predicted address)");

bytes memory creationCode = abi.encodePacked(
type(AgentFloatHook).creationCode,
abi.encode(XLayerMainnet.POOL_MANAGER, mockVault)
);

for (uint256 i = 0; i < 1_000_000; i++) {
bytes32 salt = bytes32(i);
address predicted = computeCreate2Address(deployer, salt, keccak256(creationCode));
if ((uint160(predicted) & FLAG_MASK) == REQUIRED_FLAGS) {
console.log("Found valid salt after", i, "attempts");
console.logBytes32(salt);
console.log("Predicted hook address:", predicted);
return;
}
}
revert("No valid salt found in 1M attempts; widen search");
}

function computeCreate2Address(address deployer, bytes32 salt, bytes32 codeHash) internal pure returns (address) {
bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), deployer, salt, codeHash));
return address(uint160(uint256(hash)));
}
}
Loading