|
| 1 | +// SPDX-License-Identifier: MIT |
| 2 | +pragma solidity ^0.8.20; |
| 3 | + |
| 4 | +import { console } from "forge-std/Script.sol"; |
| 5 | +import { ListaOFTAdapterV2 } from "../../../../contracts/oft/v2/ListaOFTAdapterV2.sol"; |
| 6 | +import { OFTScriptBase } from "./OFTScriptBase.sol"; |
| 7 | + |
| 8 | +/** |
| 9 | + * @title GrantRolesAndTransferOwnership |
| 10 | + * @notice Handoff step 1 of 2. Grants every role/authority to the target governance |
| 11 | + * holders WITHOUT stripping the deployer: |
| 12 | + * - Timelock -> DEFAULT_ADMIN_ROLE + Ownable owner |
| 13 | + * - Safe -> MANAGER + LayerZero endpoint delegate |
| 14 | + * - Pauser -> PAUSER |
| 15 | + * Run this first, verify on-chain that the new holders are correct and |
| 16 | + * actually controllable, THEN run RevokeDeployerRole to strip the deployer. |
| 17 | + * Splitting the grant from the irreversible renounce leaves a safe window: if |
| 18 | + * a target is wrong, the deployer still holds admin and can correct it. |
| 19 | + * |
| 20 | + * Env: |
| 21 | + * DEPLOYER_PRIVATE_KEY (required, must currently hold DEFAULT_ADMIN_ROLE + owner) |
| 22 | + * OAPP (required) local proxy address |
| 23 | + * TIMELOCK (required) new DEFAULT_ADMIN_ROLE holder + Ownable owner (must be a contract) |
| 24 | + * SAFE (required) new MANAGER holder + LayerZero delegate (must be a contract) |
| 25 | + * PAUSER (required) new PAUSER holder |
| 26 | + * |
| 27 | + * Usage: |
| 28 | + * OAPP=0x.. TIMELOCK=0x.. SAFE=0x.. PAUSER=0x.. \ |
| 29 | + * forge script scripts/foundry/oft/v2/GrantRolesAndTransferOwnership.s.sol \ |
| 30 | + * --rpc-url bsc --broadcast |
| 31 | + * |
| 32 | + * @dev ListaOFTAdapterV2 is a typed handle only; every selector used here |
| 33 | + * (grantRole, setDelegate, transferOwnership) is identical on ListaOFTv2. |
| 34 | + */ |
| 35 | +contract GrantRolesAndTransferOwnership is OFTScriptBase { |
| 36 | + function run() external { |
| 37 | + uint256 pk = vm.envUint("DEPLOYER_PRIVATE_KEY"); |
| 38 | + address deployer = vm.addr(pk); |
| 39 | + address oapp = vm.envAddress("OAPP"); |
| 40 | + address timelock = vm.envAddress("TIMELOCK"); |
| 41 | + address safe = vm.envAddress("SAFE"); |
| 42 | + address pauser = vm.envAddress("PAUSER"); |
| 43 | + |
| 44 | + require(timelock != address(0) && safe != address(0) && pauser != address(0), "zero target"); |
| 45 | + // All targets must differ from the deployer: a role granted to the deployer here |
| 46 | + // would be stripped by RevokeDeployerRole, leaving that role holderless (e.g. no |
| 47 | + // PAUSER = no emergency pause path). |
| 48 | + require(timelock != deployer && safe != deployer && pauser != deployer, "target is deployer"); |
| 49 | + // Guard against a mistyped or cross-chain address that has no code on THIS chain: |
| 50 | + // handing upgrade/config authority to an EOA-less, uncontrollable address bricks |
| 51 | + // governance. The admin/config authorities must be deployed contracts here. |
| 52 | + require(timelock.code.length > 0, "TIMELOCK has no code on this chain"); |
| 53 | + require(safe.code.length > 0, "SAFE has no code on this chain"); |
| 54 | + |
| 55 | + ListaOFTAdapterV2 oft = ListaOFTAdapterV2(oapp); |
| 56 | + bytes32 ADMIN = oft.DEFAULT_ADMIN_ROLE(); |
| 57 | + bytes32 MANAGER = oft.MANAGER(); |
| 58 | + bytes32 PAUSER = oft.PAUSER(); |
| 59 | + |
| 60 | + require(oft.hasRole(ADMIN, deployer), "deployer lacks ADMIN"); |
| 61 | + require(oft.owner() == deployer, "deployer is not owner"); |
| 62 | + |
| 63 | + console.log("OApp:", oapp); |
| 64 | + console.log("Timelock (ADMIN + owner):", timelock); |
| 65 | + console.log("Safe (MANAGER + delegate):", safe); |
| 66 | + console.log("Pauser (PAUSER):", pauser); |
| 67 | + |
| 68 | + vm.startBroadcast(pk); |
| 69 | + oft.grantRole(ADMIN, timelock); |
| 70 | + oft.grantRole(MANAGER, safe); |
| 71 | + oft.grantRole(PAUSER, pauser); |
| 72 | + // setDelegate is onlyOwner -> must run while the deployer is still owner, |
| 73 | + // i.e. BEFORE transferOwnership below. |
| 74 | + oft.setDelegate(safe); |
| 75 | + oft.transferOwnership(timelock); |
| 76 | + vm.stopBroadcast(); |
| 77 | + |
| 78 | + // verify the grants + ownership landed (reverts the broadcast otherwise). |
| 79 | + require(oft.hasRole(ADMIN, timelock), "timelock missing ADMIN"); |
| 80 | + require(oft.hasRole(MANAGER, safe), "safe missing MANAGER"); |
| 81 | + require(oft.hasRole(PAUSER, pauser), "pauser missing PAUSER"); |
| 82 | + require(oft.owner() == timelock, "owner not timelock"); |
| 83 | + // The deployer intentionally KEEPS ADMIN/MANAGER here; RevokeDeployerRole strips |
| 84 | + // it only after the operator has confirmed the new holders are controllable. |
| 85 | + console.log("Granted + ownership transferred. Deployer still ADMIN until RevokeDeployerRole."); |
| 86 | + } |
| 87 | +} |
0 commit comments