From da6717279f15bf33ee1b3658b402d74402edd384 Mon Sep 17 00:00:00 2001 From: Jayesh Yadav Date: Sun, 14 Jun 2026 23:06:33 +0530 Subject: [PATCH] feat(script): add Arbitrum deploy and strategy-registration scripts DeployArbitrum: deploys all 15 facets, assembles the Diamond + ERC-4626 Vault in one diamondCut, sets base risk policy (idle reserve, curator, optional fees), and wires strategies with stable Arbitrum defaults for Aave V3 and Compound III. Morpho and Pendle are opt-in via env so the script never reverts on a missing market address. Reads the deployer key from PRIVATE_KEY_ARB (falling back to PRIVATE_KEY) tolerating a missing 0x prefix. Two-step ownership handoff via FINAL_OWNER. RegisterStrategies: registers Morpho and/or Pendle on an already-deployed vault (no redeploy), owner-gated, skips anything already registered so it is safe to re-run, and sets no allocation (left to the curator). Both verified end-to-end against a live Arbitrum fork. --- script/DeployArbitrum.s.sol | 487 ++++++++++++++++++++++++++++++++ script/RegisterStrategies.s.sol | 136 +++++++++ 2 files changed, 623 insertions(+) create mode 100644 script/DeployArbitrum.s.sol create mode 100644 script/RegisterStrategies.s.sol diff --git a/script/DeployArbitrum.s.sol b/script/DeployArbitrum.s.sol new file mode 100644 index 0000000..289cc6a --- /dev/null +++ b/script/DeployArbitrum.s.sol @@ -0,0 +1,487 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import { Script } from "forge-std/Script.sol"; +import { console2 } from "forge-std/console2.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { Vault } from "../src/Vault.sol"; +import { IDiamond } from "../src/interfaces/IDiamond.sol"; +import { IDiamondCut } from "../src/interfaces/IDiamondCut.sol"; +import { IDiamondLoupe } from "../src/interfaces/IDiamondLoupe.sol"; +import { IERC173 } from "../src/interfaces/IERC173.sol"; +import { LibAllocator } from "../src/libraries/LibAllocator.sol"; + +import { DiamondCutFacet } from "../src/facets/DiamondCutFacet.sol"; +import { DiamondLoupeFacet } from "../src/facets/DiamondLoupeFacet.sol"; +import { OwnershipFacet } from "../src/facets/OwnershipFacet.sol"; +import { AllocatorFacet } from "../src/facets/AllocatorFacet.sol"; +import { FeeFacet } from "../src/facets/FeeFacet.sol"; +import { GuardFacet } from "../src/facets/GuardFacet.sol"; +import { HarvestFacet } from "../src/facets/HarvestFacet.sol"; +import { LockFacet } from "../src/facets/LockFacet.sol"; +import { RolesFacet } from "../src/facets/RolesFacet.sol"; +import { WithdrawQueueFacet } from "../src/facets/WithdrawQueueFacet.sol"; +import { IdleStrategyFacet } from "../src/facets/strategies/IdleStrategyFacet.sol"; +import { AaveStrategyFacet } from "../src/facets/strategies/AaveStrategyFacet.sol"; +import { CompoundV3StrategyFacet } from "../src/facets/strategies/CompoundV3StrategyFacet.sol"; +import { MorphoStrategyFacet } from "../src/facets/strategies/MorphoStrategyFacet.sol"; +import { PendlePtStrategyFacet } from "../src/facets/strategies/PendlePtStrategyFacet.sol"; + +import { IAavePool } from "../src/interfaces/external/IAavePool.sol"; +import { IComet } from "../src/interfaces/external/IComet.sol"; +import { IMorpho } from "../src/interfaces/external/IMorpho.sol"; +import { IPendleRouter } from "../src/interfaces/external/IPendleRouter.sol"; +import { IPPrincipalToken } from "../src/interfaces/external/IPPrincipalToken.sol"; +import { IPYLpOracle } from "../src/interfaces/external/IPYLpOracle.sol"; + +/// @title DeployArbitrum +/// @notice Full Vault Router deployment for Arbitrum One: deploys every facet, +/// assembles the Diamond + ERC-4626 Vault in one `diamondCut`, then wires +/// the strategies and base risk policy that the deployer (initial owner) +/// can set. +/// +/// @dev Strategy wiring is opt-in per protocol and reads its external addresses +/// from the environment, with stable Arbitrum defaults baked in for Aave V3 +/// and Compound III (both native-USDC markets). Morpho and Pendle stay off +/// unless their (venue-specific, sometimes expiring) market addresses are +/// supplied, so the script never reverts on a missing address. +/// +/// Ownership: the Vault is deployed with the broadcaster as initial owner so +/// this script can run the owner-gated config. If `FINAL_OWNER` is set and +/// differs, a two-step `transferOwnership` is initiated at the end; the new +/// owner must call `acceptOwnership()` separately. +/// +/// Required env: +/// PRIVATE_KEY_ARB deployer key (broadcaster + initial owner); `0x` prefix +/// optional. Falls back to PRIVATE_KEY if unset. +/// +/// Optional env (with defaults): +/// USDC underlying asset (default: Arbitrum native USDC) +/// VAULT_NAME ERC-20 name (default: "Vault Router USDC") +/// VAULT_SYMBOL ERC-20 symbol (default: "vrUSDC") +/// CURATOR low-privilege rebalancer (default: deployer) +/// FINAL_OWNER owner to hand off to (default: keep deployer) +/// FEE_RECIPIENT performance/mgmt fee sink (default: unset, fees off) +/// IDLE_RESERVE_BPS idle floor in bps (default: 1000 = 10%) +/// PERFORMANCE_FEE_BPS perf fee in bps (default: 0) +/// MANAGEMENT_FEE_BPS mgmt fee in bps (default: 0) +/// REGISTER_STRATEGIES "false" to skip all wiring (default: true) +/// AAVE_POOL / AAVE_ATOKEN (default: Arbitrum Aave V3 USDCn market) +/// COMET (default: Arbitrum Compound III cUSDCv3) +/// MORPHO_VAULT (default: unset -> Morpho not registered) +/// PENDLE_ROUTER / PENDLE_MARKET / PENDLE_PT (default: unset -> Pendle not registered) +/// PENDLE_ORACLE / PENDLE_TWAP / PENDLE_SLIPPAGE_BPS (optional Pendle tuning) +contract DeployArbitrum is Script { + // ----------------------------------------------------------------------- + // Arbitrum One default addresses (overridable via env) + // ----------------------------------------------------------------------- + address internal constant ARB_USDC = 0xaf88d065e77c8cC2239327C5EDb3A432268e5831; // native USDC + address internal constant ARB_AAVE_POOL = 0x794a61358D6845594F94dc1DB02A252b5b4814aD; // Aave V3 Pool + address internal constant ARB_AUSDC = 0x724dc807b04555b71ed48a6896b6F41593b8C637; // aArbUSDCn + address internal constant ARB_COMET = 0x9c4ec768c28520B50860ea7a15bd7213a9fF58bf; // Compound III cUSDCv3 + + bytes32 internal constant AAVE_ID = bytes32("aave"); + bytes32 internal constant COMPOUND_ID = bytes32("compound"); + bytes32 internal constant MORPHO_ID = bytes32("morpho"); + bytes32 internal constant PENDLE_ID = bytes32("pendle"); + + // Facet handles kept as fields so config helpers can reach them by selector. + Vault internal vault; + + function run() external returns (Vault) { + uint256 pk = _loadDeployerKey(); + address deployer = vm.addr(pk); + + IERC20 usdc = IERC20(vm.envOr("USDC", ARB_USDC)); + string memory name = vm.envOr("VAULT_NAME", string("Vault Router USDC")); + string memory symbol = vm.envOr("VAULT_SYMBOL", string("vrUSDC")); + + vm.startBroadcast(pk); + + // 1-2. Deploy every facet and assemble the cut. + IDiamond.FacetCut[] memory cuts = _deployAndBuildCuts(); + + // 3. Deploy the Vault (initial owner = deployer so this script can configure). + vault = new Vault(usdc, name, symbol, deployer, cuts, address(0), ""); + console2.log("Vault (Diamond) deployed at:", address(vault)); + + // 4. Base risk policy. + uint16 idleReserveBps = uint16(vm.envOr("IDLE_RESERVE_BPS", uint256(1000))); + AllocatorFacet(address(vault)).setIdleReserve(idleReserveBps); + + address curator = vm.envOr("CURATOR", deployer); + RolesFacet(address(vault)).setCurator(curator, true); + console2.log("Curator set:", curator); + + _configureFees(); + + // 5. Strategy wiring (opt-in per protocol). + if (vm.envOr("REGISTER_STRATEGIES", true)) { + _wireAave(); + _wireCompound(); + _wireMorpho(); + _wirePendle(); + } + + // 6. Optional ownership handoff (two-step). + address finalOwner = vm.envOr("FINAL_OWNER", deployer); + if (finalOwner != deployer) { + IERC173(address(vault)).transferOwnership(finalOwner); + console2.log("Ownership transfer initiated to (must acceptOwnership):", finalOwner); + } + + vm.stopBroadcast(); + return vault; + } + + /// @dev Loads the deployer key from `PRIVATE_KEY_ARB` (falling back to + /// `PRIVATE_KEY`) and tolerates a value with or without the `0x` prefix — + /// `vm.envUint` requires the prefix and reverts without it. Parsed via + /// `parseBytes32` so a raw 64-hex-char key works either way. + function _loadDeployerKey() internal view returns (uint256) { + string memory raw = vm.envOr("PRIVATE_KEY_ARB", vm.envOr("PRIVATE_KEY", string(""))); + require(bytes(raw).length != 0, "set PRIVATE_KEY_ARB (or PRIVATE_KEY)"); + bytes memory b = bytes(raw); + bool prefixed = b.length >= 2 && b[0] == 0x30 && (b[1] == 0x78 || b[1] == 0x58); // "0x" / "0X" + if (!prefixed) raw = string.concat("0x", raw); + return uint256(vm.parseBytes32(raw)); + } + + /// @dev Deploys all 15 facets and returns the assembled cut. A single reused + /// `f` local keeps the stack shallow (avoids stack-too-deep); each facet + /// address is logged inline as it is deployed. + function _deployAndBuildCuts() internal returns (IDiamond.FacetCut[] memory cuts) { + cuts = new IDiamond.FacetCut[](15); + address f; + + console2.log("--- Infra facets ---"); + f = address(new DiamondCutFacet()); + console2.log("DiamondCutFacet: ", f); + cuts[0] = _cut(f, _diamondCutSelectors()); + f = address(new DiamondLoupeFacet()); + console2.log("DiamondLoupeFacet: ", f); + cuts[1] = _cut(f, _diamondLoupeSelectors()); + f = address(new OwnershipFacet()); + console2.log("OwnershipFacet: ", f); + cuts[2] = _cut(f, _ownershipSelectors()); + f = address(new AllocatorFacet()); + console2.log("AllocatorFacet: ", f); + cuts[3] = _cut(f, _allocatorSelectors()); + f = address(new FeeFacet()); + console2.log("FeeFacet: ", f); + cuts[4] = _cut(f, _feeSelectors()); + f = address(new GuardFacet()); + console2.log("GuardFacet: ", f); + cuts[5] = _cut(f, _guardSelectors()); + f = address(new HarvestFacet()); + console2.log("HarvestFacet: ", f); + cuts[6] = _cut(f, _harvestSelectors()); + f = address(new LockFacet()); + console2.log("LockFacet: ", f); + cuts[7] = _cut(f, _lockSelectors()); + f = address(new RolesFacet()); + console2.log("RolesFacet: ", f); + cuts[8] = _cut(f, _rolesSelectors()); + f = address(new WithdrawQueueFacet()); + console2.log("WithdrawQueueFacet: ", f); + cuts[9] = _cut(f, _withdrawQueueSelectors()); + + console2.log("--- Strategy facets ---"); + f = address(new IdleStrategyFacet()); + console2.log("IdleStrategyFacet: ", f); + cuts[10] = _cut(f, _idleSelectors()); + f = address(new AaveStrategyFacet()); + console2.log("AaveStrategyFacet: ", f); + cuts[11] = _cut(f, _aaveSelectors()); + f = address(new CompoundV3StrategyFacet()); + console2.log("CompoundV3StrategyFacet: ", f); + cuts[12] = _cut(f, _compoundSelectors()); + f = address(new MorphoStrategyFacet()); + console2.log("MorphoStrategyFacet: ", f); + cuts[13] = _cut(f, _morphoSelectors()); + f = address(new PendlePtStrategyFacet()); + console2.log("PendlePtStrategyFacet: ", f); + cuts[14] = _cut(f, _pendleSelectors()); + } + + // ----------------------------------------------------------------------- + // Config helpers + // ----------------------------------------------------------------------- + + function _configureFees() internal { + address feeRecipient = vm.envOr("FEE_RECIPIENT", address(0)); + if (feeRecipient == address(0)) { + console2.log("Fees: no recipient set, performance/management fees stay off"); + return; + } + FeeFacet(address(vault)).setFeeRecipient(feeRecipient); + uint16 perfBps = uint16(vm.envOr("PERFORMANCE_FEE_BPS", uint256(0))); + uint16 mgmtBps = uint16(vm.envOr("MANAGEMENT_FEE_BPS", uint256(0))); + if (perfBps != 0) FeeFacet(address(vault)).setPerformanceFee(perfBps); + if (mgmtBps != 0) FeeFacet(address(vault)).setManagementFee(mgmtBps); + console2.log("Fee recipient set:", feeRecipient); + } + + function _wireAave() internal { + address pool = vm.envOr("AAVE_POOL", ARB_AAVE_POOL); + address aToken = vm.envOr("AAVE_ATOKEN", ARB_AUSDC); + if (pool == address(0) || aToken == address(0)) return; + AaveStrategyFacet(address(vault)).aaveSetConfig(IAavePool(pool), IERC20(aToken)); + AllocatorFacet(address(vault)).registerStrategy(AAVE_ID, _strategyConfig(_aaveStrategySelectors())); + console2.log("Strategy registered: aave (pool / aToken)", pool, aToken); + } + + function _wireCompound() internal { + address comet = vm.envOr("COMET", ARB_COMET); + if (comet == address(0)) return; + CompoundV3StrategyFacet(address(vault)).compoundSetConfig(IComet(comet)); + AllocatorFacet(address(vault)).registerStrategy(COMPOUND_ID, _strategyConfig(_compoundStrategySelectors())); + console2.log("Strategy registered: compound (comet)", comet); + } + + function _wireMorpho() internal { + address mVault = vm.envOr("MORPHO_VAULT", address(0)); + if (mVault == address(0)) { + console2.log("Strategy skipped: morpho (set MORPHO_VAULT to enable)"); + return; + } + MorphoStrategyFacet(address(vault)).MorphoSetVaultConfig(IMorpho(mVault)); + AllocatorFacet(address(vault)).registerStrategy(MORPHO_ID, _strategyConfig(_morphoStrategySelectors())); + console2.log("Strategy registered: morpho (metamorpho vault)", mVault); + } + + function _wirePendle() internal { + address router = vm.envOr("PENDLE_ROUTER", address(0)); + address market = vm.envOr("PENDLE_MARKET", address(0)); + address pt = vm.envOr("PENDLE_PT", address(0)); + if (router == address(0) || market == address(0) || pt == address(0)) { + console2.log("Strategy skipped: pendle (set PENDLE_ROUTER/MARKET/PT to enable)"); + return; + } + PendlePtStrategyFacet(address(vault)).pendleSetConfig(IPendleRouter(router), market, IPPrincipalToken(pt)); + + address oracle = vm.envOr("PENDLE_ORACLE", address(0)); + if (oracle != address(0)) { + uint32 twap = uint32(vm.envOr("PENDLE_TWAP", uint256(900))); + PendlePtStrategyFacet(address(vault)).pendleSetOracle(IPYLpOracle(oracle), twap); + } + uint256 slippage = vm.envOr("PENDLE_SLIPPAGE_BPS", uint256(0)); + if (slippage != 0) PendlePtStrategyFacet(address(vault)).pendleSetSlippage(uint16(slippage)); + + AllocatorFacet(address(vault)).registerStrategy(PENDLE_ID, _strategyConfig(_pendleStrategySelectors())); + console2.log("Strategy registered: pendle (router / market)", router, market); + } + + // ----------------------------------------------------------------------- + // Selector tables (compiler-checked against each facet's ABI) + // ----------------------------------------------------------------------- + + function _cut(address facet, bytes4[] memory selectors) internal pure returns (IDiamond.FacetCut memory) { + return + IDiamond.FacetCut({ + facetAddress: facet, action: IDiamond.FacetCutAction.Add, functionSelectors: selectors + }); + } + + function _diamondCutSelectors() internal pure returns (bytes4[] memory s) { + s = new bytes4[](1); + s[0] = IDiamondCut.diamondCut.selector; + } + + function _diamondLoupeSelectors() internal pure returns (bytes4[] memory s) { + s = new bytes4[](4); + s[0] = IDiamondLoupe.facets.selector; + s[1] = IDiamondLoupe.facetFunctionSelectors.selector; + s[2] = IDiamondLoupe.facetAddresses.selector; + s[3] = IDiamondLoupe.facetAddress.selector; + } + + function _ownershipSelectors() internal pure returns (bytes4[] memory s) { + s = new bytes4[](4); + s[0] = IERC173.owner.selector; + s[1] = IERC173.transferOwnership.selector; + s[2] = OwnershipFacet.pendingOwner.selector; + s[3] = OwnershipFacet.acceptOwnership.selector; + } + + function _allocatorSelectors() internal pure returns (bytes4[] memory s) { + s = new bytes4[](22); + s[0] = AllocatorFacet.registerStrategy.selector; + s[1] = AllocatorFacet.removeStrategy.selector; + s[2] = AllocatorFacet.setAllocation.selector; + s[3] = AllocatorFacet.setIdleReserve.selector; + s[4] = AllocatorFacet.setStrategyCap.selector; + s[5] = AllocatorFacet.setGlobalStrategyCap.selector; + s[6] = AllocatorFacet.setMaxRebalanceDelta.selector; + s[7] = AllocatorFacet.quarantineStrategy.selector; + s[8] = AllocatorFacet.releaseStrategy.selector; + s[9] = AllocatorFacet.quarantineFailedStrategy.selector; + s[10] = AllocatorFacet.rebalance.selector; + s[11] = AllocatorFacet.strategies.selector; + s[12] = AllocatorFacet.strategyConfig.selector; + s[13] = AllocatorFacet.targetAllocation.selector; + s[14] = AllocatorFacet.idleReserveBps.selector; + s[15] = AllocatorFacet.strategyTotalAssets.selector; + s[16] = AllocatorFacet.idleAssets.selector; + s[17] = AllocatorFacet.strategyCap.selector; + s[18] = AllocatorFacet.globalStrategyCap.selector; + s[19] = AllocatorFacet.lastRebalanceBlock.selector; + s[20] = AllocatorFacet.maxRebalanceDelta.selector; + s[21] = AllocatorFacet.isQuarantined.selector; + } + + function _feeSelectors() internal pure returns (bytes4[] memory s) { + s = new bytes4[](8); + s[0] = FeeFacet.setFeeRecipient.selector; + s[1] = FeeFacet.setPerformanceFee.selector; + s[2] = FeeFacet.setManagementFee.selector; + s[3] = FeeFacet.feeRecipient.selector; + s[4] = FeeFacet.performanceFeeBps.selector; + s[5] = FeeFacet.managementFeeBps.selector; + s[6] = FeeFacet.highWaterMark.selector; + s[7] = FeeFacet.lastFeeAccrual.selector; + } + + function _guardSelectors() internal pure returns (bytes4[] memory s) { + s = new bytes4[](7); + s[0] = GuardFacet.setMaxSharePriceDelta.selector; + s[1] = GuardFacet.pause.selector; + s[2] = GuardFacet.unpause.selector; + s[3] = GuardFacet.guardCheckpoint.selector; + s[4] = GuardFacet.paused.selector; + s[5] = GuardFacet.maxSharePriceDeltaBps.selector; + s[6] = GuardFacet.lastSharePrice.selector; + } + + function _harvestSelectors() internal pure returns (bytes4[] memory s) { + s = new bytes4[](2); + s[0] = HarvestFacet.harvest.selector; + s[1] = HarvestFacet.harvestAll.selector; + } + + function _lockSelectors() internal pure returns (bytes4[] memory s) { + s = new bytes4[](3); + s[0] = LockFacet.setShareLockPeriod.selector; + s[1] = LockFacet.shareLockPeriod.selector; + s[2] = LockFacet.lockedUntil.selector; + } + + function _rolesSelectors() internal pure returns (bytes4[] memory s) { + s = new bytes4[](2); + s[0] = RolesFacet.setCurator.selector; + s[1] = RolesFacet.isCurator.selector; + } + + function _withdrawQueueSelectors() internal pure returns (bytes4[] memory s) { + s = new bytes4[](3); + s[0] = WithdrawQueueFacet.nextWithdrawRequestId.selector; + s[1] = WithdrawQueueFacet.pendingWithdrawShares.selector; + s[2] = WithdrawQueueFacet.withdrawRequest.selector; + } + + function _idleSelectors() internal pure returns (bytes4[] memory s) { + s = new bytes4[](1); + s[0] = IdleStrategyFacet.idleTotalAssets.selector; + } + + function _aaveSelectors() internal pure returns (bytes4[] memory s) { + s = new bytes4[](7); + s[0] = AaveStrategyFacet.aaveSetConfig.selector; + s[1] = AaveStrategyFacet.aaveTotalAssets.selector; + s[2] = AaveStrategyFacet.aaveDeposit.selector; + s[3] = AaveStrategyFacet.aaveWithdraw.selector; + s[4] = AaveStrategyFacet.aaveHarvest.selector; + s[5] = AaveStrategyFacet.aavePool.selector; + s[6] = AaveStrategyFacet.aaveAToken.selector; + } + + function _compoundSelectors() internal pure returns (bytes4[] memory s) { + s = new bytes4[](6); + s[0] = CompoundV3StrategyFacet.compoundSetConfig.selector; + s[1] = CompoundV3StrategyFacet.compoundTotalAssets.selector; + s[2] = CompoundV3StrategyFacet.compoundDeposit.selector; + s[3] = CompoundV3StrategyFacet.compoundWithdraw.selector; + s[4] = CompoundV3StrategyFacet.compoundHarvest.selector; + s[5] = CompoundV3StrategyFacet.compoundComet.selector; + } + + function _morphoSelectors() internal pure returns (bytes4[] memory s) { + s = new bytes4[](6); + s[0] = MorphoStrategyFacet.MorphoSetVaultConfig.selector; + s[1] = MorphoStrategyFacet.morphoTotalAssets.selector; + s[2] = MorphoStrategyFacet.morphoDeposit.selector; + s[3] = MorphoStrategyFacet.morphoWithdraw.selector; + s[4] = MorphoStrategyFacet.morphoHarvest.selector; + s[5] = MorphoStrategyFacet.morphoVault.selector; + } + + function _pendleSelectors() internal pure returns (bytes4[] memory s) { + s = new bytes4[](14); + s[0] = PendlePtStrategyFacet.pendleSetConfig.selector; + s[1] = PendlePtStrategyFacet.pendleSetOracle.selector; + s[2] = PendlePtStrategyFacet.pendleSetSlippage.selector; + s[3] = PendlePtStrategyFacet.pendleTotalAssets.selector; + s[4] = PendlePtStrategyFacet.pendleDeposit.selector; + s[5] = PendlePtStrategyFacet.pendleWithdraw.selector; + s[6] = PendlePtStrategyFacet.pendleHarvest.selector; + s[7] = PendlePtStrategyFacet.pendleRouter.selector; + s[8] = PendlePtStrategyFacet.pendleMarket.selector; + s[9] = PendlePtStrategyFacet.pendlePT.selector; + s[10] = PendlePtStrategyFacet.pendleOracle.selector; + s[11] = PendlePtStrategyFacet.pendleTwapDuration.selector; + s[12] = PendlePtStrategyFacet.pendleIsExpired.selector; + s[13] = PendlePtStrategyFacet.pendleExpiry.selector; + } + + // ----------------------------------------------------------------------- + // Per-strategy StrategyConfig (active is set true inside registerStrategy) + // ----------------------------------------------------------------------- + + function _strategyConfig(bytes4[4] memory sel) internal pure returns (LibAllocator.StrategyConfig memory) { + return LibAllocator.StrategyConfig({ + totalAssetsSelector: sel[0], + depositSelector: sel[1], + withdrawSelector: sel[2], + harvestSelector: sel[3], + capBps: 0, + active: false + }); + } + + function _aaveStrategySelectors() internal pure returns (bytes4[4] memory) { + return [ + AaveStrategyFacet.aaveTotalAssets.selector, + AaveStrategyFacet.aaveDeposit.selector, + AaveStrategyFacet.aaveWithdraw.selector, + AaveStrategyFacet.aaveHarvest.selector + ]; + } + + function _compoundStrategySelectors() internal pure returns (bytes4[4] memory) { + return [ + CompoundV3StrategyFacet.compoundTotalAssets.selector, + CompoundV3StrategyFacet.compoundDeposit.selector, + CompoundV3StrategyFacet.compoundWithdraw.selector, + CompoundV3StrategyFacet.compoundHarvest.selector + ]; + } + + function _morphoStrategySelectors() internal pure returns (bytes4[4] memory) { + return [ + MorphoStrategyFacet.morphoTotalAssets.selector, + MorphoStrategyFacet.morphoDeposit.selector, + MorphoStrategyFacet.morphoWithdraw.selector, + MorphoStrategyFacet.morphoHarvest.selector + ]; + } + + function _pendleStrategySelectors() internal pure returns (bytes4[4] memory) { + return [ + PendlePtStrategyFacet.pendleTotalAssets.selector, + PendlePtStrategyFacet.pendleDeposit.selector, + PendlePtStrategyFacet.pendleWithdraw.selector, + PendlePtStrategyFacet.pendleHarvest.selector + ]; + } +} diff --git a/script/RegisterStrategies.s.sol b/script/RegisterStrategies.s.sol new file mode 100644 index 0000000..0c12846 --- /dev/null +++ b/script/RegisterStrategies.s.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import { Script } from "forge-std/Script.sol"; +import { console2 } from "forge-std/console2.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { AllocatorFacet } from "../src/facets/AllocatorFacet.sol"; +import { MorphoStrategyFacet } from "../src/facets/strategies/MorphoStrategyFacet.sol"; +import { PendlePtStrategyFacet } from "../src/facets/strategies/PendlePtStrategyFacet.sol"; +import { LibAllocator } from "../src/libraries/LibAllocator.sol"; + +import { IMorpho } from "../src/interfaces/external/IMorpho.sol"; +import { IPendleRouter } from "../src/interfaces/external/IPendleRouter.sol"; +import { IPPrincipalToken } from "../src/interfaces/external/IPPrincipalToken.sol"; +import { IPYLpOracle } from "../src/interfaces/external/IPYLpOracle.sol"; + +/// @title RegisterStrategies +/// @notice Registers the Morpho and/or Pendle strategies on an ALREADY-DEPLOYED +/// Vault Router diamond. Use this after `DeployArbitrum` (which deploys +/// and cuts in every facet but only registers the strategies that have a +/// baked-in default market, i.e. Aave and Compound). +/// +/// @dev Owner-gated calls, so the broadcaster must be the vault's current owner. +/// Each strategy is wired only if its env address(es) are present, and is +/// skipped (not reverted) if it is already registered, so the script is safe +/// to re-run. No allocation is set — that stays a curator decision via +/// `setAllocation` + `rebalance`. +/// +/// Required env: +/// PRIVATE_KEY_ARB owner key (broadcaster); `0x` prefix optional, falls back to PRIVATE_KEY +/// VAULT deployed Vault (diamond) address +/// +/// Optional env (a strategy is registered only when its inputs are set): +/// MORPHO_VAULT Metamorpho ERC-4626 USDC vault +/// PENDLE_ROUTER / PENDLE_MARKET / PENDLE_PT Pendle PT market inputs +/// PENDLE_ORACLE / PENDLE_TWAP / PENDLE_SLIPPAGE_BPS optional Pendle tuning +contract RegisterStrategies is Script { + bytes32 internal constant MORPHO_ID = bytes32("morpho"); + bytes32 internal constant PENDLE_ID = bytes32("pendle"); + + address internal vault; + + function run() external { + uint256 pk = _loadDeployerKey(); + vault = vm.envAddress("VAULT"); + console2.log("Vault (diamond):", vault); + + vm.startBroadcast(pk); + _wireMorpho(); + _wirePendle(); + vm.stopBroadcast(); + } + + function _wireMorpho() internal { + address mVault = vm.envOr("MORPHO_VAULT", address(0)); + if (mVault == address(0)) { + console2.log("morpho: skipped (set MORPHO_VAULT to enable)"); + return; + } + if (_isRegistered(MORPHO_ID)) { + console2.log("morpho: already registered, skipping"); + return; + } + MorphoStrategyFacet(vault).MorphoSetVaultConfig(IMorpho(mVault)); + AllocatorFacet(vault).registerStrategy(MORPHO_ID, _morphoConfig()); + console2.log("morpho: registered (metamorpho vault)", mVault); + } + + function _wirePendle() internal { + address router = vm.envOr("PENDLE_ROUTER", address(0)); + address market = vm.envOr("PENDLE_MARKET", address(0)); + address pt = vm.envOr("PENDLE_PT", address(0)); + if (router == address(0) || market == address(0) || pt == address(0)) { + console2.log("pendle: skipped (set PENDLE_ROUTER/MARKET/PT to enable)"); + return; + } + if (_isRegistered(PENDLE_ID)) { + console2.log("pendle: already registered, skipping"); + return; + } + PendlePtStrategyFacet(vault).pendleSetConfig(IPendleRouter(router), market, IPPrincipalToken(pt)); + + address oracle = vm.envOr("PENDLE_ORACLE", address(0)); + if (oracle != address(0)) { + uint32 twap = uint32(vm.envOr("PENDLE_TWAP", uint256(900))); + PendlePtStrategyFacet(vault).pendleSetOracle(IPYLpOracle(oracle), twap); + } + uint256 slippage = vm.envOr("PENDLE_SLIPPAGE_BPS", uint256(0)); + if (slippage != 0) PendlePtStrategyFacet(vault).pendleSetSlippage(uint16(slippage)); + + AllocatorFacet(vault).registerStrategy(PENDLE_ID, _pendleConfig()); + console2.log("pendle: registered (router / market)", router, market); + } + + // ----------------------------------------------------------------------- + // Helpers + // ----------------------------------------------------------------------- + + function _isRegistered(bytes32 id) internal view returns (bool) { + return AllocatorFacet(vault).strategyConfig(id).active; + } + + function _morphoConfig() internal pure returns (LibAllocator.StrategyConfig memory) { + return LibAllocator.StrategyConfig({ + totalAssetsSelector: MorphoStrategyFacet.morphoTotalAssets.selector, + depositSelector: MorphoStrategyFacet.morphoDeposit.selector, + withdrawSelector: MorphoStrategyFacet.morphoWithdraw.selector, + harvestSelector: MorphoStrategyFacet.morphoHarvest.selector, + capBps: 0, + active: false + }); + } + + function _pendleConfig() internal pure returns (LibAllocator.StrategyConfig memory) { + return LibAllocator.StrategyConfig({ + totalAssetsSelector: PendlePtStrategyFacet.pendleTotalAssets.selector, + depositSelector: PendlePtStrategyFacet.pendleDeposit.selector, + withdrawSelector: PendlePtStrategyFacet.pendleWithdraw.selector, + harvestSelector: PendlePtStrategyFacet.pendleHarvest.selector, + capBps: 0, + active: false + }); + } + + /// @dev Loads the owner key from `PRIVATE_KEY_ARB` (falling back to + /// `PRIVATE_KEY`), tolerating a value with or without the `0x` prefix. + function _loadDeployerKey() internal view returns (uint256) { + string memory raw = vm.envOr("PRIVATE_KEY_ARB", vm.envOr("PRIVATE_KEY", string(""))); + require(bytes(raw).length != 0, "set PRIVATE_KEY_ARB (or PRIVATE_KEY)"); + bytes memory b = bytes(raw); + bool prefixed = b.length >= 2 && b[0] == 0x30 && (b[1] == 0x78 || b[1] == 0x58); // "0x" / "0X" + if (!prefixed) raw = string.concat("0x", raw); + return uint256(vm.parseBytes32(raw)); + } +}