diff --git a/solidity/foundry.toml b/solidity/foundry.toml index c1a9e15068..4213003a7a 100644 --- a/solidity/foundry.toml +++ b/solidity/foundry.toml @@ -7,7 +7,7 @@ test = 'test' cache_path = 'forge-cache' allow_paths = ["../node_modules"] solc_version = '0.8.22' -evm_version= 'paris' +evm_version= 'shanghai' optimizer = true optimizer_runs = 999_999 fs_permissions = [ diff --git a/solidity/script/avs/DeployNetwork.s.sol b/solidity/script/avs/DeployNetwork.s.sol new file mode 100644 index 0000000000..222fffbecd --- /dev/null +++ b/solidity/script/avs/DeployNetwork.s.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +import "forge-std/Script.sol"; + +import {ProxyAdmin} from "../../contracts/upgrade/ProxyAdmin.sol"; +import {TransparentUpgradeableProxy} from "../../contracts/upgrade/TransparentUpgradeableProxy.sol"; + +import {Network} from "./Network.sol"; + +contract DeployNetwork is Script { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + + address networkRegistry = vm.envAddress("NETWORK_REGISTRY"); + + address proxyAdmin = vm.envAddress("PROXY_ADMIN"); + address safe = vm.envAddress("SAFE"); + + uint256 constant MIN_DELAY = 3 days; + + function run() external { + address deployer = vm.addr(deployerPrivateKey); + + vm.startBroadcast(deployerPrivateKey); + + Network timelockImplementation = new Network(); + + address[] memory proposers = new address[](2); + proposers[0] = deployer; + proposers[1] = safe; + + address[] memory executors = new address[](2); + executors[0] = deployer; + executors[1] = safe; + + address admin = safe; + + bytes memory initializeCall = abi.encodeCall( + timelockImplementation.initialize, + (0, proposers, executors, admin) + ); + + TransparentUpgradeableProxy timelockProxy = new TransparentUpgradeableProxy( + address(timelockImplementation), + proxyAdmin, + initializeCall + ); + + Network timelock = Network(payable(timelockProxy)); + + timelock.hasRole(timelock.TIMELOCK_ADMIN_ROLE(), safe); + + bytes memory registerNetworkCall = abi.encodeWithSignature( + "registerNetwork()" + ); + + uint256 numCalls = 3; + + address[] memory targets = new address[](numCalls); + uint256[] memory values = new uint256[](numCalls); + bytes[] memory payloads = new bytes[](numCalls); + + targets[0] = networkRegistry; + values[0] = 0; + payloads[0] = registerNetworkCall; + + targets[1] = address(timelock); + values[1] = 0; + payloads[1] = abi.encodeCall(timelock.updateDelay, (MIN_DELAY)); + + targets[2] = address(timelock); + values[2] = 0; + payloads[2] = abi.encodeCall( + timelock.revokeRole, + (timelock.PROPOSER_ROLE(), deployer) + ); + + timelock.scheduleBatch( + targets, + values, + payloads, + bytes32(0), + bytes32(0), + 0 + ); + + timelock.executeBatch( + targets, + values, + payloads, + bytes32(0), + bytes32(0) + ); + + vm.stopBroadcast(); + + assert(timelock.hasRole(timelock.TIMELOCK_ADMIN_ROLE(), safe)); + assert(!timelock.hasRole(timelock.TIMELOCK_ADMIN_ROLE(), deployer)); + + assert(timelock.hasRole(timelock.PROPOSER_ROLE(), safe)); + assert(!timelock.hasRole(timelock.PROPOSER_ROLE(), deployer)); + + assert(timelock.hasRole(timelock.EXECUTOR_ROLE(), safe)); + assert(timelock.hasRole(timelock.EXECUTOR_ROLE(), deployer)); + } +} diff --git a/solidity/script/avs/Network.sol b/solidity/script/avs/Network.sol new file mode 100644 index 0000000000..9e55d93a92 --- /dev/null +++ b/solidity/script/avs/Network.sol @@ -0,0 +1,12 @@ +import {TimelockControllerUpgradeable} from "@openzeppelin/contracts-upgradeable/governance/TimelockControllerUpgradeable.sol"; + +contract Network is TimelockControllerUpgradeable { + function initialize( + uint256 minDelay, + address[] memory proposers, + address[] memory executors, + address admin + ) public initializer { + __TimelockController_init(minDelay, proposers, executors, admin); + } +} diff --git a/solidity/script/avs/mainnet.env b/solidity/script/avs/mainnet.env new file mode 100644 index 0000000000..ac789fe4fc --- /dev/null +++ b/solidity/script/avs/mainnet.env @@ -0,0 +1,3 @@ +export NETWORK_REGISTRY=0xC773b1011461e7314CF05f97d95aa8e92C1Fd8aA +export PROXY_ADMIN=0x75EE15Ee1B4A75Fa3e2fDF5DF3253c25599cc659 +export SAFE=0x3965AC3D295641E452E0ea896a086A9cD7C6C5b6 diff --git a/typescript/infra/config/environments/mainnet3/owners.ts b/typescript/infra/config/environments/mainnet3/owners.ts index 45b707218f..b6d00132b7 100644 --- a/typescript/infra/config/environments/mainnet3/owners.ts +++ b/typescript/infra/config/environments/mainnet3/owners.ts @@ -8,6 +8,7 @@ import { supportedChainNames } from './supportedChainNames.js'; export const timelocks: ChainMap
= { arbitrum: '0xAC98b0cD1B64EA4fe133C6D2EDaf842cE5cF4b01', + ethereum: '0x59cf937Ea9FA9D7398223E3aA33d92F7f5f986A2', // symbiotic network timelock }; export function localAccountRouters(): ChainMap
{ diff --git a/typescript/infra/scripts/safes/propose.ts b/typescript/infra/scripts/safes/propose.ts new file mode 100644 index 0000000000..851f76cf3b --- /dev/null +++ b/typescript/infra/scripts/safes/propose.ts @@ -0,0 +1,198 @@ +import { + createPublicClient, + encodeFunctionData, + http, + parseAbiItem, +} from 'viem'; +import { mainnet } from 'viem/chains'; +import yargs from 'yargs'; + +import { assert } from '@hyperlane-xyz/utils'; + +import { safes } from '../../config/environments/mainnet3/owners.js'; +import { SafeMultiSend } from '../../src/govern/multisend.js'; +import { withChain } from '../agent-utils.js'; +import { getEnvironmentConfig } from '../core-utils.js'; + +const VAULTS = [ + { + name: 'EtherFi LBTC', + vault: '0xd4E20ECA1f996Dab35883dC0AD5E3428AF888D45', + }, + { + name: 'EtherFi wstETH', + vault: '0x450a90fdEa8B87a6448Ca1C87c88Ff65676aC45b', + }, + { + name: 'Renzo pzETH', + vault: '0xa88e91cEF50b792f9449e2D4C699b6B3CcE1D19F', + }, + { + name: 'Swell swETH', + vault: '0x65b560d887c010c4993c8f8b36e595c171d69d63', + }, + { + name: 'Swell WBTC', + vault: '0x9e405601B645d3484baeEcf17bBF7aD87680f6e8', + }, + { + name: 'MEV Mellow wstETH', + vault: '0x446970400e1787814CA050A4b45AE9d21B3f7EA7', + }, + { + name: 'MEV Symbiotic wstETH', + vault: '0x4e0554959A631B3D3938ffC158e0a7b2124aF9c5', + }, + { + name: 'Gauntlet wstETH', + vault: '0xc10A7f0AC6E3944F4860eE97a937C51572e3a1Da', + }, + { + name: 'Gauntlet cbETH', + vault: '0xB8Fd82169a574eB97251bF43e443310D33FF056C', + }, + { + name: 'Gauntlet rETH', + vault: '0xaF07131C497E06361dc2F75de63dc1d3e113f7cb', + }, + { + name: 'Gauntlet wBETH', + vault: '0x81bb35c4152B605574BAbD320f8EABE2871CE8C6', + }, + { + name: 'P2P wstETH', + vault: '0x7b276aAD6D2ebfD7e270C5a2697ac79182D9550E', + }, + { + name: 'Re7 wstETH', + vault: '0x3D93b33f5E5fe74D54676720e70EA35210cdD46E', + }, +]; + +const NETWORK = '0x59cf937Ea9FA9D7398223E3aA33d92F7f5f986A2'; + +const SUBNETWORK_IDENTIFIER = BigInt(0); + +const SET_LIMIT_ABI = parseAbiItem( + 'function setMaxNetworkLimit(uint96 identifier, uint256 amount)', +); + +const VAULT_DELEGATOR_ABI = parseAbiItem( + 'function delegator() returns (address)', +); + +const SCHEDULE_BATCH_ABI = parseAbiItem( + 'function scheduleBatch(address[] calldata targets,uint256[] calldata values,bytes[] calldata payloads,bytes32 predecessor,bytes32 salt,uint256 delay)', +); + +const EXECUTE_BATCH_ABI = parseAbiItem( + 'function executeBatch(address[] calldata targets,uint256[] calldata values,bytes[] calldata payloads,bytes32 predecessor,bytes32 salt)', +); + +const ZERO_BYTES32 = '0x'.padEnd(64 + 2, '0') as `0x${string}`; + +async function main() { + const client = createPublicClient({ + chain: mainnet, + transport: http(), + }); + + const delegatorContracts = VAULTS.map(({ vault }) => ({ + address: vault as `0x${string}`, + abi: [VAULT_DELEGATOR_ABI], + functionName: 'delegator', + })); + + const delegatorResults = await client.multicall({ + contracts: delegatorContracts, + }); + + const delegators = delegatorResults.map(({ status, result }) => { + assert(status === 'success', 'Multicall failed'); + return result; + }); + + const { chain } = await withChain(yargs(process.argv.slice(2))).demandOption( + 'chain', + ).argv; + + const envConfig = getEnvironmentConfig('mainnet3'); + const multiProvider = await envConfig.getMultiProvider(); + + const multisend = new SafeMultiSend(multiProvider, chain, safes[chain]); + + const delegatorLimitCalls = VAULTS.map(({ name, vault }, index) => { + let asset: 'ETH' | 'BTC'; + if (name.endsWith('ETH')) { + asset = 'ETH'; + } else if (name.endsWith('BTC')) { + asset = 'BTC'; + } else { + throw new Error(`Invalid vault name ${name}`); + } + + const limit = asset === 'ETH' ? BigInt(3000e18) : BigInt(100e8); + + const delegator = delegators[index]; + + return { + to: delegator, + data: encodeFunctionData({ + abi: [SET_LIMIT_ABI], + args: [SUBNETWORK_IDENTIFIER, limit], + }), + description: `Set ${name} Hyperlane network delegation limit to ${limit} ${asset}`, + }; + }); + + const provider = multiProvider.getProvider(chain); + for (const call of delegatorLimitCalls) { + // simulate + await provider.estimateGas({ + from: NETWORK, + to: call.to, + data: call.data, + }); + } + + const targets = delegatorLimitCalls.map(({ to }) => to); + assert(new Set(targets).size === targets.length, 'Duplicate targets'); + + const payloads = delegatorLimitCalls.map(({ data }) => data); + const values = delegatorLimitCalls.map(() => BigInt(0)); + + const description = delegatorLimitCalls + .map(({ description }) => description) + .join('\n'); + + const scheduleTx = { + to: NETWORK, + data: encodeFunctionData({ + abi: [SCHEDULE_BATCH_ABI], + args: [targets, values, payloads, ZERO_BYTES32, ZERO_BYTES32, BigInt(0)], + }), + description: `Schedule batch:\n ${description}`, + }; + + console.log(scheduleTx); + + const executeTx = { + to: NETWORK, + data: encodeFunctionData({ + abi: [EXECUTE_BATCH_ABI], + args: [targets, values, payloads, ZERO_BYTES32, ZERO_BYTES32], + }), + // description: `Execute batch:\n ${description}`, + }; + + console.log(executeTx); + + await multiProvider.sendTransaction(chain, executeTx); + return; + await multisend.sendTransactions([scheduleTx]); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +});