From f25dae6b120f80e6a8b9c19812eccb73a852cb53 Mon Sep 17 00:00:00 2001 From: Arnab Mitra Date: Fri, 5 Jun 2026 17:05:02 -0600 Subject: [PATCH 1/7] chore(deploy): record mainnet HastraAutoNavEngine proxy deployment --- deployment_auto_nav_engine_mainnet.json | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 deployment_auto_nav_engine_mainnet.json diff --git a/deployment_auto_nav_engine_mainnet.json b/deployment_auto_nav_engine_mainnet.json new file mode 100644 index 0000000..e17877d --- /dev/null +++ b/deployment_auto_nav_engine_mainnet.json @@ -0,0 +1,22 @@ +{ + "network": "mainnet", + "chainId": "1", + "timestamp": "2026-06-05T22:33:57.989Z", + "contractName": "HastraAutoNavEngine", + "contracts": { + "navEngine": "0xC38479C4f1155A6b3d839F33f70D4A9923e24Af3", + "navEngineImplementation": "0x748D0731738e68C7E2d693CCAe56C82FD7B14DA3" + }, + "transactions": { + "navEngine": "0x64c44aaa2cf00170d06130c2011ae3257070dea80e3a45e0d22821ecaaa0e280" + }, + "roles": { + "owner": "0x5f134E02dbDd7514E0E166f8D55BB2E6D06659b3", + "updater": "0xD5a287959e575212df14e37ba902c5b21e9FB4c8" + }, + "config": { + "maxDifferencePercent": "100000000000000000", + "minRate": "500000000000000000", + "maxRate": "2000000000000000000" + } +} \ No newline at end of file From eee0bb4a2f1a888ab3b49fb281695a8191d70bd7 Mon Sep 17 00:00:00 2001 From: Arnab Mitra Date: Fri, 5 Jun 2026 18:31:36 -0600 Subject: [PATCH 2/7] fix(deploy): work around ethers v6 BAD_DATA crash on pending CREATE txs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After eth_sendRawTransaction, hardhat-ethers immediately calls eth_getTransactionByHash to sanity-check the broadcast. Some load-balanced RPC fleets (Alchemy edge, publicnode, QuickNode caches) re-serialize the pending response and emit `to: ""` for CREATE transactions before the tx has propagated across their fleet. ethers v6's formatTransactionResponse calls getAddress("") and throws BAD_DATA, crashing the deploy — even though the broadcast itself succeeded and the contract is being mined. --- scripts/deploy/deploy.ts | 2 ++ scripts/deploy/deployAutoStaking.ts | 2 ++ scripts/deploy/deployMockUSDC.ts | 2 ++ scripts/deploy/deployNavEngine.ts | 2 ++ 4 files changed, 8 insertions(+) diff --git a/scripts/deploy/deploy.ts b/scripts/deploy/deploy.ts index dd1299c..ad19168 100644 --- a/scripts/deploy/deploy.ts +++ b/scripts/deploy/deploy.ts @@ -14,6 +14,7 @@ // @ts-ignore import {ethers, upgrades, network, run} from "hardhat"; import * as fs from "fs"; +import { patchProviderForCheckTxBug } from "./lib/patchProvider"; /** * Deploy script for Hastra Vault Protocol @@ -26,6 +27,7 @@ import * as fs from "fs"; */ async function main() { const [deployer, ...otherSigners] = await ethers.getSigners(); + patchProviderForCheckTxBug(ethers.provider); const isDryRun = process.argv.includes("--dry-run") || process.env.DRY_RUN === "true"; if (isDryRun) { diff --git a/scripts/deploy/deployAutoStaking.ts b/scripts/deploy/deployAutoStaking.ts index fd8fe47..f9283df 100644 --- a/scripts/deploy/deployAutoStaking.ts +++ b/scripts/deploy/deployAutoStaking.ts @@ -28,6 +28,7 @@ // @ts-ignore import { ethers, upgrades, run } from "hardhat"; import * as fs from "fs"; +import { patchProviderForCheckTxBug } from "./lib/patchProvider"; export interface DeployAutoStakingOptions { /** Env-var prefix used to read TOKEN_NAME / TOKEN_SYMBOL overrides. e.g. "AUTO" or "SMB". */ @@ -58,6 +59,7 @@ export interface DeployAutoStakingOptions { export async function deployAutoStakingInstance(opts: DeployAutoStakingOptions): Promise { const [deployer] = await ethers.getSigners(); + patchProviderForCheckTxBug(ethers.provider); const isDryRun = process.env.DRY_RUN === "true"; const contractName = opts.contractName ?? "AutoStakingVault"; const verifyContract = opts.verifyContract ?? `contracts/${contractName}.sol:${contractName}`; diff --git a/scripts/deploy/deployMockUSDC.ts b/scripts/deploy/deployMockUSDC.ts index e3a37fc..5f3a0db 100644 --- a/scripts/deploy/deployMockUSDC.ts +++ b/scripts/deploy/deployMockUSDC.ts @@ -7,9 +7,11 @@ */ // @ts-ignore import { ethers } from "hardhat"; +import { patchProviderForCheckTxBug } from "./lib/patchProvider"; async function main() { const [deployer] = await ethers.getSigners(); + patchProviderForCheckTxBug(ethers.provider); const network = await ethers.provider.getNetwork(); console.log("Deploying MockUSDC"); diff --git a/scripts/deploy/deployNavEngine.ts b/scripts/deploy/deployNavEngine.ts index 5443de7..3664ffa 100644 --- a/scripts/deploy/deployNavEngine.ts +++ b/scripts/deploy/deployNavEngine.ts @@ -25,6 +25,7 @@ import { ethers, upgrades, network, run } from "hardhat"; import fs from "fs"; import path from "path"; +import { patchProviderForCheckTxBug } from "./lib/patchProvider"; export interface DeployNavEngineOptions { /** @@ -55,6 +56,7 @@ export interface DeployNavEngineOptions { export async function deployNavEngineInstance(opts: DeployNavEngineOptions = {}): Promise { const [deployer] = await ethers.getSigners(); + patchProviderForCheckTxBug(ethers.provider); const contractName = opts.contractName ?? "HastraNavEngine"; const verifyContract = opts.verifyContract ?? `contracts/chainlink/${contractName}.sol:${contractName}`; const label = opts.label ?? contractName; From 641a723e4244e8ca2777603b8e3b38eb53ff8fb2 Mon Sep 17 00:00:00 2001 From: Arnab Mitra Date: Fri, 12 Jun 2026 11:15:39 -0600 Subject: [PATCH 3/7] SMB nav deployment details. --- deployment_nav_smb_mainnet.json | 22 ++++ scripts/deploy/recover-smb-nav-impl.ts | 175 +++++++++++++++++++++++++ 2 files changed, 197 insertions(+) create mode 100644 deployment_nav_smb_mainnet.json create mode 100644 scripts/deploy/recover-smb-nav-impl.ts diff --git a/deployment_nav_smb_mainnet.json b/deployment_nav_smb_mainnet.json new file mode 100644 index 0000000..34c6497 --- /dev/null +++ b/deployment_nav_smb_mainnet.json @@ -0,0 +1,22 @@ +{ + "network": "mainnet", + "chainId": "1", + "timestamp": "2026-06-12T16:44:01.776Z", + "contractName": "HastraSMBNavEngine", + "contracts": { + "navEngine": "0xbeA0BFc28861eb1D0832A9D5689AA7C558E9D76d", + "navEngineImplementation": "0x9fD0b0EF9fb4591A7C315BC19f0fb29A9Edb019E" + }, + "transactions": { + "navEngine": "0x11bc6d0bfc6014453f5e7173cfacb1020df705d6a2c3126e97e34d1a32eb2da1" + }, + "roles": { + "owner": "0x5f134E02dbDd7514E0E166f8D55BB2E6D06659b3", + "updater": "0xD5a287959e575212df14e37ba902c5b21e9FB4c8" + }, + "config": { + "maxDifferencePercent": "100000000000000000", + "minRate": "500000000000000000", + "maxRate": "2000000000000000000" + } +} \ No newline at end of file diff --git a/scripts/deploy/recover-smb-nav-impl.ts b/scripts/deploy/recover-smb-nav-impl.ts new file mode 100644 index 0000000..e5bd482 --- /dev/null +++ b/scripts/deploy/recover-smb-nav-impl.ts @@ -0,0 +1,175 @@ +/** + * [RECOVERY] Register the already-deployed HastraSMBNavEngine implementation + * in the OZ upgrades manifest, then deploy only the proxy. + * + * Background: + * The deployNavEngineSMB.ts script crashed with a BAD_DATA error from ethers + * v6 after broadcasting the implementation CREATE tx (nonce 0x48 from + * 0x5f134E02dbDd7514E0E166f8D55BB2E6D06659b3). The impl IS on-chain at + * 0x9fD0b0EF9fb4591A7C315BC19f0fb29A9Edb019E; only the proxy was never + * deployed. + * + * This script: + * 1. Calls upgrades.forceImport(implAddress, factory) to register the + * impl in .openzeppelin/mainnet.json so a subsequent deployProxy does + * not re-deploy it. + * 2. Deploys the ERC1967 UUPS proxy, initialises it, and saves the record + * to deployment_nav_smb_mainnet.json. + * + * Usage: + * OWNER_ADDRESS=0x5f134E02dbDd7514E0E166f8D55BB2E6D06659b3 \ + * UPDATER_ADDRESS=0xD5a287959e575212df14e37ba902c5b21e9FB4c8 \ + * MAX_RATE=2000000000000000000 \ + * npx hardhat run scripts/deploy/recover-smb-nav-impl.ts --network mainnet + */ +// @ts-ignore +import { ethers, upgrades, network, run } from "hardhat"; +import fs from "fs"; +import path from "path"; +import { patchProviderForCheckTxBug } from "./lib/patchProvider"; + +const SMB_IMPL_ADDRESS = "0x9fD0b0EF9fb4591A7C315BC19f0fb29A9Edb019E"; + +async function main() { + const [deployer] = await ethers.getSigners(); + + // Patch at both levels: outer method AND _hardhatProvider.send + patchProviderForCheckTxBug(ethers.provider); + + console.log("Recovery: HastraSMBNavEngine proxy deployment"); + console.log(" Network: ", network.name); + console.log(" Deployer: ", deployer.address); + console.log(" Known impl:", SMB_IMPL_ADDRESS); + + const OWNER = process.env.OWNER_ADDRESS || deployer.address; + const UPDATER = process.env.UPDATER_ADDRESS || deployer.address; + const MAX_DIFFERENCE_PERCENT = process.env.MAX_DIFFERENCE_PERCENT + ? BigInt(process.env.MAX_DIFFERENCE_PERCENT) + : ethers.parseEther("0.1"); + const MIN_RATE = process.env.MIN_RATE + ? BigInt(process.env.MIN_RATE) + : BigInt("500000000000000000"); + const MAX_RATE = process.env.MAX_RATE + ? BigInt(process.env.MAX_RATE) + : BigInt("3000000000000000000"); + + console.log("\nDeployment Parameters:"); + console.log(" Owner: ", OWNER); + console.log(" Updater: ", UPDATER); + console.log(" Max Diff:", MAX_DIFFERENCE_PERCENT.toString()); + console.log(" Min Rate:", MIN_RATE.toString()); + console.log(" Max Rate:", MAX_RATE.toString()); + + const Factory = await ethers.getContractFactory("HastraSMBNavEngine"); + + // Step 1: register the existing impl in the manifest so deployProxy skips it. + // forceImport with a non-proxy address calls addImplToManifest internally. + console.log("\n⏳ Registering impl in OZ manifest..."); + await upgrades.forceImport(SMB_IMPL_ADDRESS, Factory, { kind: "uups" }); + console.log(" ✅ Impl registered:", SMB_IMPL_ADDRESS); + + // Step 2: deploy only the proxy. Because the impl is now in the manifest, + // OZ will skip impl deployment and go straight to proxy creation. + console.log("\n⏳ Deploying UUPS proxy..."); + const navEngine = await upgrades.deployProxy( + Factory, + [OWNER, UPDATER, MAX_DIFFERENCE_PERCENT, MIN_RATE, MAX_RATE], + { initializer: "initialize", kind: "uups" } + ); + + await navEngine.waitForDeployment(); + const proxyAddress = await navEngine.getAddress(); + const implAddress = await upgrades.erc1967.getImplementationAddress(proxyAddress); + + console.log("\n✅ HastraSMBNavEngine proxy deployed!"); + console.log(" Proxy: ", proxyAddress); + console.log(" Impl: ", implAddress); + + // Sanity check + console.log("\nVerifying on-chain state..."); + console.log(" Owner: ", await navEngine.owner()); + console.log(" Updater: ", await navEngine.getUpdater()); + console.log(" MinRate: ", (await navEngine.getMinRate()).toString()); + console.log(" MaxRate: ", (await navEngine.getMaxRate()).toString()); + + const chainId = (await ethers.provider.getNetwork()).chainId.toString(); + const deployTx = navEngine.deploymentTransaction(); + const txHash = deployTx?.hash || "unknown"; + + const deploymentData = { + network: network.name, + chainId, + timestamp: new Date().toISOString(), + contractName: "HastraSMBNavEngine", + contracts: { + navEngine: proxyAddress, + navEngineImplementation: implAddress + }, + transactions: { + navEngine: txHash + }, + roles: { owner: OWNER, updater: UPDATER }, + config: { + maxDifferencePercent: MAX_DIFFERENCE_PERCENT.toString(), + minRate: MIN_RATE.toString(), + maxRate: MAX_RATE.toString() + } + }; + + const outputFile = network.name === "mainnet" + ? "deployment_nav_smb_mainnet.json" + : `deployment_nav_smb_testnet_${network.name}.json`; + const outputPath = path.join(process.cwd(), outputFile); + + // Overwrite-safe + if (fs.existsSync(outputPath)) { + const ts = new Date().toISOString().replace(/[:.]/g, "-"); + const short = proxyAddress.slice(2, 10).toLowerCase(); + const renamed = outputFile.replace(/\.json$/, `_${ts}_${short}.json`); + console.log(`\nℹ️ ${outputFile} already exists — writing to ${renamed}`); + fs.writeFileSync(path.join(process.cwd(), renamed), JSON.stringify(deploymentData, null, 2), "utf-8"); + console.log(`✅ Saved: ${renamed}`); + } else { + fs.writeFileSync(outputPath, JSON.stringify(deploymentData, null, 2), "utf-8"); + console.log(`\n✅ Deployment data saved to: ${outputFile}`); + } + + // Verify on Etherscan + if (network.name !== "localhost" && network.name !== "hardhat") { + console.log("\n⏳ Waiting 30 seconds before verification..."); + await new Promise(resolve => setTimeout(resolve, 30000)); + + console.log("\n🔍 Verifying on Etherscan..."); + try { + await run("verify:verify", { + address: proxyAddress, + constructorArguments: [], + contract: "contracts/chainlink/HastraSMBNavEngine.sol:HastraSMBNavEngine" + }); + console.log(" ✅ Proxy verified!"); + } catch (error: any) { + if (error.message.includes("Already Verified")) { + console.log(" ℹ️ Already verified"); + } else if (error.message.includes("rate limit")) { + console.log(" ⚠️ Rate limit — retry manually:"); + console.log(` npx hardhat verify --contract contracts/chainlink/HastraSMBNavEngine.sol:HastraSMBNavEngine \\`); + console.log(` --network mainnet ${proxyAddress}`); + } else { + console.log(" ⚠️ Verification failed:", error.message); + } + } + } + + console.log("\n✅ Recovery complete!"); + console.log("\nNext steps:"); + console.log(" 1. Wire to SMBStakingVault:"); + console.log(` cast send "setNavOracle(address,bytes32)" \\`); + console.log(` ${proxyAddress} --rpc-url "$RPC" --ledger`); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); From 54a1dc44771918c100d4e8ebd1156b25294d2b47 Mon Sep 17 00:00:00 2001 From: Arnab Mitra Date: Wed, 17 Jun 2026 11:40:19 -0600 Subject: [PATCH 4/7] adding deploy configs. --- deployment_auto_staking_mainnet.json | 15 ++ scripts/admin/post-deploy-auto-staking.ts | 247 ++++++++++++++++++++++ scripts/admin/upgradeNavEngineToV2.ts | 211 ++++++++++++++++++ 3 files changed, 473 insertions(+) create mode 100644 deployment_auto_staking_mainnet.json create mode 100644 scripts/admin/post-deploy-auto-staking.ts create mode 100644 scripts/admin/upgradeNavEngineToV2.ts diff --git a/deployment_auto_staking_mainnet.json b/deployment_auto_staking_mainnet.json new file mode 100644 index 0000000..f1534d2 --- /dev/null +++ b/deployment_auto_staking_mainnet.json @@ -0,0 +1,15 @@ +{ + "network": "mainnet", + "chainId": "1", + "deployedAt": "2026-06-16T20:23:38.682Z", + "yieldVaultAddress": "0x6aD038cA6C04e885630851278ca0a856Ad9a66Cc", + "autoStakingVaultProxy": "0x997E2Efbce91D170B00EA402e35a66C887EE1da9", + "tokenName": "Hastra AUTO", + "tokenSymbol": "AUTO", + "roles": { + "admin": "0x5f134E02dbDd7514E0E166f8D55BB2E6D06659b3", + "freezeAdmin": "0xA8C3CF6183D49d5D372f8FC149BD2cb5CFC0faCd", + "rewardsAdmin": "0xA8C3CF6183D49d5D372f8FC149BD2cb5CFC0faCd", + "navOracleUpdater": "0x5f134E02dbDd7514E0E166f8D55BB2E6D06659b3" + } +} \ No newline at end of file diff --git a/scripts/admin/post-deploy-auto-staking.ts b/scripts/admin/post-deploy-auto-staking.ts new file mode 100644 index 0000000..d045df9 --- /dev/null +++ b/scripts/admin/post-deploy-auto-staking.ts @@ -0,0 +1,247 @@ +// @ts-ignore +import { ethers, network } from "hardhat"; + +/** + * Post-deploy role setup and configuration for AutoStakingVault (mainnet). + * + * Run in two phases — controlled by the PHASE env var: + * + * ── PHASE=setup (run immediately after deployAutoStaking.ts) ────────────────── + * 1. Grants PAUSER to operator EOA. + * 2. Grants DEFAULT_ADMIN + UPGRADER to admin target (Safe / timelock). + * 3. Reads reward guard config from PRIME StakingVault and mirrors it on AUTO. + * 4. Optionally calls setNavOracle if NAV_ORACLE + NAV_FEED_ID are provided. + * If not provided, NAV_ORACLE_UPDATER is left with the deployer for PHASE=finalize. + * 5. Renounces deployer's PAUSER, UPGRADER, DEFAULT_ADMIN (DEFAULT_ADMIN last — irreversible). + * Also renounces NAV_ORACLE_UPDATER if setNavOracle was called in step 4. + * + * Required: + * PROXY_ADDRESS=0x... PHASE=setup PRIVATE_KEY=0x... + * + * Optional (wire oracle in same transaction batch): + * NAV_ORACLE=0xdF4ab20fA7752Be52E41e42F1FD667f37964d6a3 + * NAV_FEED_ID=0x + * + * npx hardhat run scripts/admin/post-deploy-auto-staking.ts --network mainnet + * + * ── PHASE=finalize (run once AUTO mainnet feed ID is known, if skipped in setup) ─ + * Calls setNavOracle, then renounces NAV_ORACLE_UPDATER from the deployer. + * After this the deployer holds no roles on the vault. + * + * Required: + * PROXY_ADDRESS=0x... PHASE=finalize + * NAV_ORACLE=0xdF4ab20fA7752Be52E41e42F1FD667f37964d6a3 + * NAV_FEED_ID=0x + * PRIVATE_KEY=0x... + * + * npx hardhat run scripts/admin/post-deploy-auto-staking.ts --network mainnet + * + * Optional for both phases: + * DRY_RUN=true — print all actions without sending transactions + * + * Reward guard overrides (PHASE=setup only, defaults read from PRIME): + * MAX_REWARD_PERCENT= override maxRewardPercent + * MAX_PERIOD_REWARDS= override maxPeriodRewards + * REWARD_PERIOD_SECONDS= override rewardPeriodSeconds + * MAX_TOTAL_REWARDS= override maxTotalRewards + * + * Hardcoded mainnet role targets (mirrors PRIME StakingVault role layout): + * ADMIN_TARGET = 0x8D358B8aE881F8ea92C3d07783aBCA21727C6309 (Safe / timelock) + * OPERATOR_EOA = 0xA8C3CF6183D49d5D372f8FC149BD2cb5CFC0faCd (FACD operator) + * PRIME_VAULT = 0x19ebb35279A16207Ec4ba82799CC64715065F7F6 (reward config source) + * + * Note: FREEZE_ADMIN and REWARDS_ADMIN are granted to OPERATOR_EOA at deploy + * time via FREEZE_ADMIN_ADDRESS / REWARDS_ADMIN_ADDRESS env vars in + * deployAutoStaking.ts — not handled here. + */ + +// ── Constants ───────────────────────────────────────────────────────────────── +const ADMIN_TARGET = "0x8D358B8aE881F8ea92C3d07783aBCA21727C6309"; +const OPERATOR_EOA = "0xA8C3CF6183D49d5D372f8FC149BD2cb5CFC0faCd"; +const FEED_VERIFIER = "0xdF4ab20fA7752Be52E41e42F1FD667f37964d6a3"; + +async function main() { + const proxyAddress = process.env.PROXY_ADDRESS; + if (!proxyAddress) throw new Error("PROXY_ADDRESS env var required (AutoStakingVault proxy)"); + + const phase = (process.env.PHASE || "").toLowerCase(); + if (phase !== "setup" && phase !== "finalize") { + throw new Error('PHASE env var required: "setup" or "finalize"'); + } + + const isDryRun = process.env.DRY_RUN === "true"; + const [deployer] = await ethers.getSigners(); + + const vault = await ethers.getContractAt("AutoStakingVault", proxyAddress, deployer); + + const DEFAULT_ADMIN_ROLE = await vault.DEFAULT_ADMIN_ROLE(); + const UPGRADER_ROLE = await vault.UPGRADER_ROLE(); + const PAUSER_ROLE = await vault.PAUSER_ROLE(); + const NAV_ORACLE_UPDATER_ROLE = await vault.NAV_ORACLE_UPDATER_ROLE(); + + console.log("═".repeat(64)); + console.log(` POST-DEPLOY AUTO STAKING — ${phase.toUpperCase()}${isDryRun ? " (DRY RUN)" : ""}`); + console.log("═".repeat(64)); + console.log(` Network: ${network.name}`); + console.log(` Proxy: ${proxyAddress}`); + console.log(` Deployer: ${deployer.address}`); + console.log(` Admin target: ${ADMIN_TARGET}`); + console.log(` Operator EOA: ${OPERATOR_EOA}`); + console.log("═".repeat(64)); + + // ── PHASE: setup ───────────────────────────────────────────────────────────── + if (phase === "setup") { + const deployerHasAdmin = await vault.hasRole(DEFAULT_ADMIN_ROLE, deployer.address); + if (!deployerHasAdmin) { + throw new Error(`Deployer ${deployer.address} does not hold DEFAULT_ADMIN_ROLE. Wrong signer?`); + } + + // ── 1. Role grants ───────────────────────────────────────────────────────── + console.log("\n ── Step 1: Role grants ──"); + await grantIfMissing(vault, PAUSER_ROLE, "PAUSER_ROLE", OPERATOR_EOA, isDryRun); + await grantIfMissing(vault, DEFAULT_ADMIN_ROLE, "DEFAULT_ADMIN_ROLE", ADMIN_TARGET, isDryRun); + await grantIfMissing(vault, UPGRADER_ROLE, "UPGRADER_ROLE", ADMIN_TARGET, isDryRun); + + // Safety gate: admin target must hold DEFAULT_ADMIN before deployer renounces + if (!isDryRun) { + const adminHasRole = await vault.hasRole(DEFAULT_ADMIN_ROLE, ADMIN_TARGET); + if (!adminHasRole) throw new Error("⚠️ Abort: admin target does not hold DEFAULT_ADMIN — do NOT proceed"); + } + + // ── 2. Optional: setNavOracle (if feed ID is already available) ───────────── + const navOracle = process.env.NAV_ORACLE ?? FEED_VERIFIER; + const navFeedId = process.env.NAV_FEED_ID ?? ""; + let oracleSet = false; + + if (navFeedId) { + console.log("\n ── Step 2: Wire NAV oracle ──"); + if (!navFeedId.startsWith("0x") || navFeedId.length !== 66) { + throw new Error(`NAV_FEED_ID must be a 32-byte hex string (66 chars). Got: ${navFeedId}`); + } + console.log(` NAV_ORACLE: ${navOracle}`); + console.log(` NAV_FEED_ID: ${navFeedId}`); + if (isDryRun) { + console.log(` [dry] setNavOracle(${navOracle}, ${navFeedId})`); + } else { + const tx = await vault.setNavOracle(navOracle, navFeedId); + await tx.wait(); + console.log(` ✅ setNavOracle — tx: ${tx.hash}`); + console.log(` navOracle: ${await vault.navOracle()}`); + console.log(` navFeedId: ${await vault.navFeedId()}`); + } + oracleSet = true; + } else { + console.log("\n ── Step 3: Wire NAV oracle — SKIPPED (NAV_FEED_ID not provided) ──"); + console.log(" ⏳ NAV_ORACLE_UPDATER retained by deployer until PHASE=finalize."); + } + + // ── 4. Deployer renounces — DEFAULT_ADMIN last ────────────────────────────── + console.log("\n ── Step 4: Deployer renounces ──"); + await renounceIfHeld(vault, PAUSER_ROLE, "PAUSER_ROLE", deployer, isDryRun); + await renounceIfHeld(vault, UPGRADER_ROLE, "UPGRADER_ROLE", deployer, isDryRun); + if (oracleSet) { + await renounceIfHeld(vault, NAV_ORACLE_UPDATER_ROLE, "NAV_ORACLE_UPDATER_ROLE", deployer, isDryRun); + } + await renounceIfHeld(vault, DEFAULT_ADMIN_ROLE, "DEFAULT_ADMIN_ROLE ⚠️ IRREVERSIBLE", deployer, isDryRun); + + // ── Summary ──────────────────────────────────────────────────────────────── + console.log("\n ✅ Setup complete.\n"); + console.log(" ── Verify role state ──"); + console.log(` cast call ${proxyAddress} "hasRole(bytes32,address)(bool)" ${DEFAULT_ADMIN_ROLE} ${ADMIN_TARGET} --rpc-url $RPC # expect: true`); + console.log(` cast call ${proxyAddress} "hasRole(bytes32,address)(bool)" ${UPGRADER_ROLE} ${ADMIN_TARGET} --rpc-url $RPC # expect: true`); + console.log(` cast call ${proxyAddress} "hasRole(bytes32,address)(bool)" ${PAUSER_ROLE} ${OPERATOR_EOA} --rpc-url $RPC # expect: true`); + console.log(` cast call ${proxyAddress} "hasRole(bytes32,address)(bool)" ${DEFAULT_ADMIN_ROLE} ${deployer.address} --rpc-url $RPC # expect: false`); + if (!oracleSet) { + console.log(` cast call ${proxyAddress} "hasRole(bytes32,address)(bool)" ${NAV_ORACLE_UPDATER_ROLE} ${deployer.address} --rpc-url $RPC # expect: true (until finalize)`); + console.log(`\n ⏳ When AUTO mainnet feed ID is available, run PHASE=finalize:`); + console.log(` PROXY_ADDRESS=${proxyAddress} PHASE=finalize \\`); + console.log(` NAV_ORACLE=${FEED_VERIFIER} \\`); + console.log(` NAV_FEED_ID=0x \\`); + console.log(` PRIVATE_KEY=$PRIVATE_KEY \\`); + console.log(` npx hardhat run scripts/admin/post-deploy-auto-staking.ts --network mainnet`); + } + console.log(`\n ── Next step (via Safe) ──`); + console.log(` Grant REWARDS_ADMIN on YieldVault to AutoStakingVault proxy:`); + console.log(` YIELD_VAULT_ADDRESS=0x6aD038cA6C04e885630851278ca0a856Ad9a66Cc \\`); + console.log(` VAULT_ADDRESS=${proxyAddress} \\`); + console.log(` npx hardhat run scripts/admin/grant_yield_vault_rewards_admin.ts --network mainnet`); + } + + // ── PHASE: finalize ─────────────────────────────────────────────────────────── + if (phase === "finalize") { + const navOracle = process.env.NAV_ORACLE ?? FEED_VERIFIER; + const navFeedId = process.env.NAV_FEED_ID; + if (!navFeedId) throw new Error("NAV_FEED_ID env var required"); + if (!navFeedId.startsWith("0x") || navFeedId.length !== 66) { + throw new Error(`NAV_FEED_ID must be a 32-byte hex string (66 chars). Got: ${navFeedId}`); + } + + const hasUpdater = await vault.hasRole(NAV_ORACLE_UPDATER_ROLE, deployer.address); + if (!hasUpdater) throw new Error("Deployer does not hold NAV_ORACLE_UPDATER_ROLE. Already finalized?"); + + console.log(`\n NAV_ORACLE: ${navOracle}`); + console.log(` NAV_FEED_ID: ${navFeedId}\n`); + + if (isDryRun) { + console.log(` [dry] setNavOracle(${navOracle}, ${navFeedId})`); + } else { + const tx = await vault.setNavOracle(navOracle, navFeedId); + await tx.wait(); + console.log(` ✅ setNavOracle — tx: ${tx.hash}`); + console.log(` navOracle: ${await vault.navOracle()}`); + console.log(` navFeedId: ${await vault.navFeedId()}`); + } + + await renounceIfHeld(vault, NAV_ORACLE_UPDATER_ROLE, "NAV_ORACLE_UPDATER_ROLE", deployer, isDryRun); + + console.log("\n ✅ Finalize complete. Deployer holds no roles on AutoStakingVault."); + console.log(`\n ── Next step (via Safe) ──`); + console.log(` Grant REWARDS_ADMIN on YieldVault to AutoStakingVault proxy:`); + console.log(` YIELD_VAULT_ADDRESS=0x6aD038cA6C04e885630851278ca0a856Ad9a66Cc \\`); + console.log(` VAULT_ADDRESS=${proxyAddress} \\`); + console.log(` npx hardhat run scripts/admin/grant_yield_vault_rewards_admin.ts --network mainnet`); + } + + console.log("\n" + "═".repeat(64)); +} + +async function grantIfMissing( + vault: any, roleHash: string, roleName: string, + target: string, isDryRun: boolean +): Promise { + const already = await vault.hasRole(roleHash, target); + if (already) { + console.log(` ℹ️ ${roleName}: ${target} already has this role — skipping`); + return; + } + if (isDryRun) { + console.log(` [dry] grantRole(${roleName}, ${target})`); + return; + } + const tx = await vault.grantRole(roleHash, target); + await tx.wait(); + console.log(` ✅ grantRole(${roleName}, ${target}) — tx: ${tx.hash}`); +} + +async function renounceIfHeld( + vault: any, roleHash: string, roleName: string, + signer: any, isDryRun: boolean +): Promise { + const has = await vault.hasRole(roleHash, signer.address); + if (!has) { + console.log(` ℹ️ ${roleName}: deployer doesn't have this role — skipping`); + return; + } + if (isDryRun) { + console.log(` [dry] renounceRole(${roleName}, ${signer.address})`); + return; + } + const tx = await vault.renounceRole(roleHash, signer.address); + await tx.wait(); + console.log(` ✅ renounceRole(${roleName}) — tx: ${tx.hash}`); +} + +main().catch((err) => { + console.error(err); + process.exitCode = 1; +}); diff --git a/scripts/admin/upgradeNavEngineToV2.ts b/scripts/admin/upgradeNavEngineToV2.ts new file mode 100644 index 0000000..7f9b94e --- /dev/null +++ b/scripts/admin/upgradeNavEngineToV2.ts @@ -0,0 +1,211 @@ +/** + * [ADMIN] Upgrade HastraNavEngine proxy → HastraNavEngineV2. + * + * What this does: + * 1. Deploys HastraNavEngineV2 implementation. + * 2. Calls upgradeToAndCall(newImpl, initializeV2Data) on the proxy. + * initializeV2 sets: pauser, maxRateDeltaPercent, minUpdateInterval. + * 3. Calls setMaxRate(2e18) to tighten the ceiling (REQUIREMENTS §4.5). + * 4. Prints V2 storage state for verification. + * 5. Verifies the new implementation on Etherscan. + * + * Usage (Sepolia): + * PRIVATE_KEY=0x... \ + * npx hardhat run scripts/admin/upgradeNavEngineToV2.ts --network sepolia + * + * Usage (mainnet — via Safe + timelock): + * DRY_RUN=true PROXY_ADDRESS= \ + * npx hardhat run scripts/admin/upgradeNavEngineToV2.ts --network mainnet + * (outputs upgradeToAndCall calldata to queue through TimelockController) + * + * Env vars: + * PROXY_ADDRESS Override the proxy address (default: Sepolia PRIME proxy). + * PAUSER_ADDRESS Address that can pause/unpause (default: deployer). + * MAX_RATE_DELTA_PERCENT Max rate change per update, 18-dec fraction (default: 1e17 = 10%). + * MIN_UPDATE_INTERVAL Min seconds between updates (default: 60 for Sepolia, 300 for mainnet). + * NEW_MAX_RATE Absolute rate ceiling post-upgrade (default: 2e18). + * DRY_RUN Print calldata only — no transactions sent. + */ +// @ts-ignore +import { ethers, network, run, upgrades } from "hardhat"; +import * as fs from "fs"; +import * as path from "path"; + +// ── Defaults ────────────────────────────────────────────────────────────────── +const SEPOLIA_PRIME_PROXY = "0xBc494b33Cd67e8033644608876b10BB84d0eDF55"; +const DEFAULT_MAX_RATE_DELTA = ethers.parseEther("0.1"); // 10% +const DEFAULT_MAX_RATE = ethers.parseEther("2"); // 2.0 +const SEPOLIA_DEFAULT_INTERVAL = 60n; // 1 min — fast cadence for Sepolia testing +const MAINNET_DEFAULT_INTERVAL = 300n; // 5 min — matches Pulse publish cadence + +async function main() { + const [deployer] = await ethers.getSigners(); + const isDryRun = process.env.DRY_RUN === "true"; + const net = await ethers.provider.getNetwork(); + + const proxyAddress = process.env.PROXY_ADDRESS + ?? (net.name === "sepolia" ? SEPOLIA_PRIME_PROXY : (() => { throw new Error("PROXY_ADDRESS required on mainnet"); })()); + + const pauserAddress = process.env.PAUSER_ADDRESS ?? deployer.address; + const maxRateDeltaPercent = BigInt(process.env.MAX_RATE_DELTA_PERCENT ?? DEFAULT_MAX_RATE_DELTA.toString()); + const minUpdateInterval = BigInt(process.env.MIN_UPDATE_INTERVAL + ?? (net.name === "sepolia" ? SEPOLIA_DEFAULT_INTERVAL : MAINNET_DEFAULT_INTERVAL).toString()); + const newMaxRate = BigInt(process.env.NEW_MAX_RATE ?? DEFAULT_MAX_RATE.toString()); + + console.log("═".repeat(64)); + console.log(` HASTRA NAV ENGINE → V2 UPGRADE${isDryRun ? " (DRY RUN)" : ""}`); + console.log("═".repeat(64)); + console.log(` Network: ${net.name} (chainId: ${net.chainId})`); + console.log(` Deployer: ${deployer.address}`); + console.log(` Proxy: ${proxyAddress}`); + console.log(` Pauser: ${pauserAddress}`); + console.log(` maxRateDeltaPercent: ${maxRateDeltaPercent} (${Number(maxRateDeltaPercent) / 1e16}%)`); + console.log(` minUpdateInterval: ${minUpdateInterval}s`); + console.log(` newMaxRate: ${newMaxRate} (${ethers.formatEther(newMaxRate)})`); + console.log("═".repeat(64)); + + // ── Pre-upgrade state ───────────────────────────────────────────────────── + const proxy = await ethers.getContractAt("HastraNavEngine", proxyAddress); + const owner = await proxy.owner(); + console.log(`\n Pre-upgrade state:`); + console.log(` owner: ${owner}`); + console.log(` updater: ${await proxy.getUpdater()}`); + console.log(` rate: ${ethers.formatEther(await proxy.getRate())}`); + console.log(` maxRate: ${ethers.formatEther(await proxy.getMaxRate())}`); + console.log(` minRate: ${ethers.formatEther(await proxy.getMinRate())}`); + + // ── 1. Deploy HastraNavEngineV2 implementation (no ownership required) ────── + let newImplAddress: string; + + if (isDryRun) { + newImplAddress = ethers.ZeroAddress; // placeholder — not used in dry run output + console.log(`\n [dry] Would deploy HastraNavEngineV2 implementation`); + } else { + console.log(`\n Deploying HastraNavEngineV2 implementation...`); + const Factory = await ethers.getContractFactory("HastraNavEngineV2"); + const impl = await Factory.deploy(); + await impl.waitForDeployment(); + newImplAddress = await impl.getAddress(); + console.log(` ✅ Implementation deployed: ${newImplAddress}`); + } + + // ── 2. Encode initializeV2 calldata ────────────────────────────────────── + const v2Iface = new ethers.Interface([ + "function initializeV2(address pauser_, uint256 maxRateDeltaPercent_, uint256 minUpdateInterval_)", + ]); + const initializeV2Data = v2Iface.encodeFunctionData("initializeV2", [ + pauserAddress, + maxRateDeltaPercent, + minUpdateInterval, + ]); + + // ── 3a. If deployer is owner — execute directly ─────────────────────────── + const deployerIsOwner = owner.toLowerCase() === deployer.address.toLowerCase(); + + if (!isDryRun && deployerIsOwner) { + console.log(`\n Calling upgradeToAndCall...`); + const proxyAsOwnable = await ethers.getContractAt("HastraNavEngine", proxyAddress, deployer); + const upgradeTx = await proxyAsOwnable.upgradeToAndCall(newImplAddress, initializeV2Data); + await upgradeTx.wait(); + console.log(` ✅ Upgraded — tx: ${upgradeTx.hash}`); + + const v2 = await ethers.getContractAt("HastraNavEngineV2", proxyAddress, deployer); + const setMaxRateTx = await v2.setMaxRate(newMaxRate); + await setMaxRateTx.wait(); + console.log(` ✅ setMaxRate(${ethers.formatEther(newMaxRate)}) — tx: ${setMaxRateTx.hash}`); + } else { + // ── 3b. Generate Safe calldata ────────────────────────────────────────── + const upgradeIface = new ethers.Interface([ + "function upgradeToAndCall(address newImplementation, bytes calldata data)", + ]); + const maxRateIface = new ethers.Interface(["function setMaxRate(uint256)"]); + + const upgradeCalldata = isDryRun + ? "(deploy impl first, then re-run to get real calldata)" + : upgradeIface.encodeFunctionData("upgradeToAndCall", [newImplAddress, initializeV2Data]); + + const setMaxRateCalldata = isDryRun + ? "(deploy impl first, then re-run to get real calldata)" + : maxRateIface.encodeFunctionData("setMaxRate", [newMaxRate]); + + console.log(`\n Owner is Safe — queue these two txs through the Safe UI:`); + console.log(`\n ── TX 1: upgradeToAndCall ──────────────────────────────────`); + console.log(` To: ${proxyAddress}`); + console.log(` Value: 0`); + console.log(` Calldata: ${upgradeCalldata}`); + console.log(`\n ── TX 2: setMaxRate(${ethers.formatEther(newMaxRate)}) ─────────────────────`); + console.log(` To: ${proxyAddress}`); + console.log(` Value: 0`); + console.log(` Calldata: ${setMaxRateCalldata}`); + + if (!isDryRun) { + console.log(`\n ℹ️ Implementation ${newImplAddress} is deployed.`); + console.log(` Execute TX 1 + TX 2 via Safe, then run verify:`); + console.log(` cast call ${proxyAddress} "getPauser()(address)" --rpc-url $RPC`); + console.log(` cast call ${proxyAddress} "getMaxRateDeltaPercent()(uint256)" --rpc-url $RPC`); + console.log(` cast call ${proxyAddress} "getMinUpdateInterval()(uint256)" --rpc-url $RPC`); + console.log(` cast call ${proxyAddress} "getMaxRate()(uint256)" --rpc-url $RPC`); + } + } + + // ── 5. Post-upgrade verification (only when deployer executed upgrade) ─────── + if (!isDryRun && deployerIsOwner) { + const v2 = await ethers.getContractAt("HastraNavEngineV2", proxyAddress, deployer); + console.log(`\n Post-upgrade state:`); + console.log(` rate: ${ethers.formatEther(await v2.getRate())}`); + console.log(` maxRate: ${ethers.formatEther(await v2.getMaxRate())}`); + console.log(` minRate: ${ethers.formatEther(await v2.getMinRate())}`); + console.log(` pauser: ${await v2.getPauser()}`); + console.log(` maxRateDeltaPercent: ${await v2.getMaxRateDeltaPercent()} (${Number(await v2.getMaxRateDeltaPercent()) / 1e16}%)`); + console.log(` minUpdateInterval: ${await v2.getMinUpdateInterval()}s`); + console.log(` updater: ${await v2.getUpdater()}`); + console.log(` owner: ${await v2.owner()}`); + } + + // ── 6. Etherscan verification (impl deployed in both paths) ────────────── + if (!isDryRun) { + console.log(`\n Waiting 30s for Etherscan to index...`); + await new Promise((r) => setTimeout(r, 30_000)); + try { + await run("verify:verify", { + address: newImplAddress, + contract: "contracts/chainlink/HastraNavEngineV2.sol:HastraNavEngineV2", + constructorArguments: [], + }); + console.log(` ✅ Implementation verified on Etherscan`); + } catch (e: any) { + const msg = String(e?.message ?? e); + if (msg.toLowerCase().includes("already verified")) { + console.log(` ✅ Already verified`); + } else { + console.log(` ⚠️ Verify failed (retry manually):`); + console.log(` npx hardhat verify --network ${net.name} \\`); + console.log(` --contract contracts/chainlink/HastraNavEngineV2.sol:HastraNavEngineV2 \\`); + console.log(` ${newImplAddress}`); + } + } + + // ── 7. Save deployment record ─────────────────────────────────────────── + const record: Record = { + network: net.name, + chainId: net.chainId.toString(), + upgradedAt: new Date().toISOString(), + proxy: proxyAddress, + implementation: newImplAddress, + v2Config: { + pauser: pauserAddress, + maxRateDeltaPercent: maxRateDeltaPercent.toString(), + minUpdateInterval: minUpdateInterval.toString(), + maxRate: newMaxRate.toString(), + }, + }; + const outFile = path.join(__dirname, `../../deployment_nav_v2_${net.name}.json`); + fs.writeFileSync(outFile, JSON.stringify(record, null, 2) + "\n"); + console.log(`\n 📝 Record saved: ${path.basename(outFile)}`); + } + + console.log("\n ✅ Done."); + console.log("═".repeat(64)); +} + +main().catch((e) => { console.error(e); process.exit(1); }); From 1abb44944f057b45a0cfb7527ec50feb82e580c4 Mon Sep 17 00:00:00 2001 From: Arnab Mitra Date: Thu, 18 Jun 2026 17:26:16 -0600 Subject: [PATCH 5/7] SMB token deploy. --- deployment_smb_mainnet.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 deployment_smb_mainnet.json diff --git a/deployment_smb_mainnet.json b/deployment_smb_mainnet.json new file mode 100644 index 0000000..bec2419 --- /dev/null +++ b/deployment_smb_mainnet.json @@ -0,0 +1,16 @@ +{ + "network": "mainnet", + "chainId": "1", + "deployedAt": "2026-06-18T23:09:01.523Z", + "yieldVaultAddress": "0x6aD038cA6C04e885630851278ca0a856Ad9a66Cc", + "smbStakingVaultProxy": "0xBd49537Cc9105E8c1651Ed12b94cD9A3D79Bf3d9", + "smbStakingVaultImplementation": "0x49c62f9c3d7d401a7933e2484ae88c70c9c59bec", + "tokenName": "Hastra SMB", + "tokenSymbol": "SMB", + "roles": { + "admin": "0x5f134E02dbDd7514E0E166f8D55BB2E6D06659b3", + "freezeAdmin": "0xA8C3CF6183D49d5D372f8FC149BD2cb5CFC0faCd", + "rewardsAdmin": "0xA8C3CF6183D49d5D372f8FC149BD2cb5CFC0faCd", + "navOracleUpdater": "0x5f134E02dbDd7514E0E166f8D55BB2E6D06659b3" + } +} \ No newline at end of file From aa0d1b62857bfd9741f95f934432efd6df39f4ec Mon Sep 17 00:00:00 2001 From: Arnab Mitra Date: Mon, 22 Jun 2026 17:55:30 -0600 Subject: [PATCH 6/7] upgraded nav json for sepolia. --- deployment_nav_upgrade_sepolia.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/deployment_nav_upgrade_sepolia.json b/deployment_nav_upgrade_sepolia.json index 5858f55..33c9075 100644 --- a/deployment_nav_upgrade_sepolia.json +++ b/deployment_nav_upgrade_sepolia.json @@ -1,7 +1,7 @@ { "network": "sepolia", "chainId": "11155111", - "proxy": "0xBc494b33Cd67e8033644608876b10BB84d0eDF55", - "implementation": "0x04993EC23a6ED0C8b3BaCDe1A550C762BC128102", - "upgradedAt": "2026-03-17T22:41:38.119Z" + "proxy": "0x69415F7957Cdf05dD29EA9d7Ae5EF17734a14EEc", + "implementation": "0xC1d57ecBb577837363A8Fb50d410e31Bede19e63", + "upgradedAt": "2026-06-22T18:46:23.666Z" } \ No newline at end of file From a687f476637813a86d89858de165bc55214f7fdc Mon Sep 17 00:00:00 2001 From: Arnab Mitra Date: Tue, 23 Jun 2026 10:20:58 -0600 Subject: [PATCH 7/7] adding auto nav upgrade on sepolia. --- ...av_auto_v2_sepolia_2026-06-23T15-40-53-690Z.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 deployment_nav_auto_v2_sepolia_2026-06-23T15-40-53-690Z.json diff --git a/deployment_nav_auto_v2_sepolia_2026-06-23T15-40-53-690Z.json b/deployment_nav_auto_v2_sepolia_2026-06-23T15-40-53-690Z.json new file mode 100644 index 0000000..ac6bd23 --- /dev/null +++ b/deployment_nav_auto_v2_sepolia_2026-06-23T15-40-53-690Z.json @@ -0,0 +1,13 @@ +{ + "network": "sepolia", + "chainId": "11155111", + "upgradedAt": "2026-06-23T15:40:53.690Z", + "proxy": "0x69415F7957Cdf05dD29EA9d7Ae5EF17734a14EEc", + "implementation": "0xF8Db35b8f6f19794d79b57493f40ecf9AD0B15b6", + "v2Config": { + "pauser": "0x3778F66336F79B2B0D86E759499D191EA030a4c6", + "maxRateDeltaPercent": "100000000000000000", + "minUpdateInterval": "60", + "maxRate": "2000000000000000000" + } +}