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)); + } +}