Skip to content

chore(contracts): safe migration scripts #548

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,6 @@ go.work
go.work.sum

story

# Output files
script/admin-actions/output/31337/*
1 change: 1 addition & 0 deletions contracts/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ out
cache

.env
/script/admin-actions/output/31337/*
4 changes: 2 additions & 2 deletions contracts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,11 @@ To generate this first state:
```
ADMIN_ADDRESS=0x...
TIMELOCK_EXECUTOR_ADDRESS=0x...
TIMELOCK_GUARDIAN_ADDRESS=0x...
TIMELOCK_CANCELLER_ADDRESS=0x...
```
- `ADMIN_ADDRESS` will be the owner of the `TimelockController` contract. Will be able to propose transactions to the timelock, and cancel them.
- `TIMELOCK_EXECUTOR_ADDRESS` address allowed to execute the scheduled actions once the timelock matures.
- `TIMELOCK_GUARDIAN_ADDRESS` address allowed to cancel proposals
- `TIMELOCK_CANCELLER_ADDRESS` address allowed to cancel proposals

1. Run
```
Expand Down
1 change: 1 addition & 0 deletions contracts/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ fs_permissions = [
{ access = "read-write", path = "./test" },
{ access = "read-write", path = "./script" },
{ access = "read", path = "./out" },
{ access = "read", path = "./output" },
]

libraries = ["src/protocol/libraries/Secp256k1.sol:Secp256k1:0x00000000000000000000000000000000000256f1"]
Expand Down
2 changes: 1 addition & 1 deletion contracts/script/GenerateAlloc.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ contract GenerateAlloc is Script {
}

if (timelockGuardian == address(0)) {
timelockGuardian = vm.envAddress("TIMELOCK_GUARDIAN_ADDRESS");
timelockGuardian = vm.envAddress("TIMELOCK_CANCELLER_ADDRESS");
}
require(timelockGuardian != address(0), "canceller not set");

Expand Down
40 changes: 40 additions & 0 deletions contracts/script/admin-actions/SetUBIPercentage.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/* solhint-disable no-console */
// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;

import { TimelockOperations } from "script/utils/TimelockOperations.s.sol";
import { Predeploys } from "src/libraries/Predeploys.sol";
import { IUBIPool } from "src/interfaces/IUBIPool.sol";
import { console2 } from "forge-std/console2.sol";

/// @notice Helper script that generates a json file with the timelocked operation to set the UBI percentage
/// @dev Set in the constructor Modes.SCHEDULE to run _scheduleActions, Modes.EXECUTE to run _executeActions
/// or Modes.CANCEL to run _cancelActions
contract SetUBIPercentage is TimelockOperations {
address[] public from;
uint32 public percentage = 500; // 5.00%

constructor() TimelockOperations("set-ubi-percentage-5%") {
from = new address[](3);
from[0] = vm.envAddress("ADMIN_ADDRESS");
from[1] = vm.envAddress("ADMIN_ADDRESS");
from[2] = vm.envAddress("ADMIN_ADDRESS");
console2.log("from", from[0], from[1], from[2]);
}

function _getTargetTimelock() internal view virtual override returns (address) {
return USE_CURRENT_TIMELOCK;
}

function _generate() internal virtual override {
_generateAction(
from,
Predeploys.UBIPool,
uint256(0),
abi.encodeWithSelector(IUBIPool.setUBIPercentage.selector, percentage),
bytes32(0),
bytes32(0),
minDelay
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/* solhint-disable no-console */
// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;

import { console2 } from "forge-std/console2.sol";
import { Script } from "forge-std/Script.sol";
import { TimelockController } from "@openzeppelin/contracts/governance/TimelockController.sol";
import { Create3 } from "src/deploy/Create3.sol";
import { Predeploys } from "src/libraries/Predeploys.sol";

/// @title DeployNewTimelock
/// @notice Deploy a new TimelockController governed by the new multisigs
contract DeployNewTimelock is Script {
function run() public virtual {
require(!isTimelockDeployed(), "TimelockController already deployed");
deployTimelock();
}

/// @notice Check if the TimelockController is deployed
/// @return True if the TimelockController is deployed, false otherwise
function isTimelockDeployed() internal view returns (bool) {
bytes32 salt = keccak256("STORY_TIMELOCK_CONTROLLER_SAFE");
address timelockAddress = Create3(Predeploys.Create3).predictDeterministicAddress(salt);

if (timelockAddress.code.length == 0) {
// Not deployed
return false;
}
TimelockController timelock = TimelockController(payable(timelockAddress));

// Check if timelock has a minDelay and assigned proposer role as proof of deployment
if (
timelock.getMinDelay() == 0 ||
!timelock.hasRole(timelock.PROPOSER_ROLE(), vm.envAddress("SAFE_TIMELOCK_PROPOSER"))
) {
revert("wrong timelock controller deployment");
}
return true;
}

/// @notice Deploy a new TimelockController deterministically
function deployTimelock() internal {
uint256 deployerPrivateKey = vm.envUint("NEW_TIMELOCK_DEPLOYER_PRIVATE_KEY");
address deployer = vm.addr(deployerPrivateKey);
vm.startBroadcast(deployerPrivateKey);

address timelockProposer = vm.envAddress("SAFE_TIMELOCK_PROPOSER");
require(timelockProposer != address(0), "safe admin address not set");
console2.log("timelockProposer", timelockProposer);

// Old timelock proposer during migration
address oldTimelockProposer = vm.envAddress("OLD_TIMELOCK_PROPOSER");
require(oldTimelockProposer != address(0), "old protocol admin address not set");
console2.log("oldTimelockProposer", oldTimelockProposer);

address[] memory proposers = new address[](2);
proposers[0] = timelockProposer;
proposers[1] = oldTimelockProposer;
console2.log("proposers", proposers[0], proposers[1]);

// Executor can be address(0), public execution
address timelockExecutor = vm.envAddress("SAFE_TIMELOCK_EXECUTOR");
address[] memory timelockExecutors = new address[](2);
timelockExecutors[0] = timelockExecutor;
timelockExecutors[1] = oldTimelockProposer; // Old multisig during migration
console2.log("timelockExecutor", timelockExecutor);

address timelockGuardian = vm.envAddress("SAFE_TIMELOCK_CANCELLER");
require(timelockGuardian != address(0), "Zero address as timelock guardian");

address oldTimelockGuardian = vm.envAddress("OLD_TIMELOCK_CANCELLER");
require(oldTimelockGuardian != address(0), "old timelock guardian address not set");
console2.log("oldTimelockGuardian", oldTimelockGuardian);

address[] memory timelockCancellers = new address[](2);
timelockCancellers[0] = timelockGuardian;
timelockCancellers[1] = oldTimelockGuardian;
console2.log("timelockGuardian", timelockGuardian);

bytes memory creationCode = abi.encodePacked(
type(TimelockController).creationCode,
abi.encode(
vm.envUint("MIN_DELAY"),
proposers,
timelockExecutors,
deployer // Warning: root admin. Must renounce by end of script
)
);

bytes32 salt = keccak256("STORY_TIMELOCK_CONTROLLER_SAFE");

address newTimelockAddress = Create3(Predeploys.Create3).deployDeterministic(creationCode, salt);
console2.log("Deployed TimelockController at address:", newTimelockAddress);
console2.log("Temporary root admin:", deployer);
console2.log("Proposers", proposers[0], proposers[1]);
console2.log("timelockExecutors", timelockExecutors[0], timelockExecutors[1]);
TimelockController newTimelock = TimelockController(payable(newTimelockAddress));

for (uint256 i = 0; i < timelockCancellers.length; i++) {
newTimelock.grantRole(newTimelock.CANCELLER_ROLE(), timelockCancellers[i]);
}
console2.log("timelockCancellers", timelockCancellers[0], timelockCancellers[1]);

// WARNING: Revoke this role after migration
newTimelock.grantRole(newTimelock.DEFAULT_ADMIN_ROLE(), oldTimelockProposer);
console2.log("Granted DEFAULT_ADMIN_ROLE to oldTimelockProposer", oldTimelockProposer);
// WARNING: Consider revoking this role after migration
newTimelock.grantRole(newTimelock.DEFAULT_ADMIN_ROLE(), timelockProposer);
console2.log("Granted DEFAULT_ADMIN_ROLE to timelockProposer", timelockProposer);

newTimelock.renounceRole(
newTimelock.DEFAULT_ADMIN_ROLE(), // DEFAULT_ADMIN_ROLE
deployer
);
console2.log("Renounced DEFAULT_ADMIN_ROLE from deployer", deployer);

vm.stopBroadcast();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/* solhint-disable no-console */
// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;

/* solhint-disable max-line-length */

import { BaseTransferOwnershipProxyAdmin } from "script/admin-actions/migrate-to-safe/BaseTransferOwnershipProxyAdmin.s.sol";

/// @title TransferOwnershipsProxyAdmin1
/// @notice Generates json files with the timelocked operations to transfer the ownership of the
/// last 256 proxy admins to the new timelock
contract TransferOwnershipsProxyAdmin1 is BaseTransferOwnershipProxyAdmin {
constructor()
BaseTransferOwnershipProxyAdmin(
"safe-migr-transfer-ownerships-proxy-admin-1",
769, // fromIndex
1024 // toIndex
)
{}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/* solhint-disable no-console */
// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;

/* solhint-disable max-line-length */

import { BaseTransferOwnershipProxyAdmin } from "script/admin-actions/migrate-to-safe/BaseTransferOwnershipProxyAdmin.s.sol";

/// @title TransferOwnershipsProxyAdmin2
/// @notice Generates json files with the timelocked operations to transfer the ownership of proxy admins
/// from index 512 to 768
contract TransferOwnershipsProxyAdmin2 is BaseTransferOwnershipProxyAdmin {
constructor()
BaseTransferOwnershipProxyAdmin(
"safe-migr-transfer-ownerships-proxy-admin-2",
513, // fromIndex
768 // toIndex
)
{}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/* solhint-disable no-console */
// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;

/* solhint-disable max-line-length */

import { BaseTransferOwnershipProxyAdmin } from "script/admin-actions/migrate-to-safe/BaseTransferOwnershipProxyAdmin.s.sol";

/// @title TransferOwnershipsProxyAdmin3
/// @notice Generates json files with the timelocked operations to transfer the ownership of proxy admins
/// from index 256 to 512
contract TransferOwnershipsProxyAdmin3 is BaseTransferOwnershipProxyAdmin {
constructor()
BaseTransferOwnershipProxyAdmin(
"safe-migr-transfer-ownerships-proxy-admin-3",
257, // fromIndex
512 // toIndex
)
{}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/* solhint-disable no-console */
// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;

/* solhint-disable max-line-length */

import { BaseTransferOwnershipProxyAdmin } from "script/admin-actions/migrate-to-safe/BaseTransferOwnershipProxyAdmin.s.sol";

/// @title TransferOwnershipsProxyAdmin4
/// @notice Generates json files with the timelocked operations to transfer the ownership of proxy admins
/// from index 0 to 256
contract TransferOwnershipsProxyAdmin4 is BaseTransferOwnershipProxyAdmin {
constructor()
BaseTransferOwnershipProxyAdmin(
"safe-migr-transfer-ownerships-proxy-admin-4",
1, // fromIndex
256 // toIndex
)
{}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/* solhint-disable no-console */
// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;

/* solhint-disable max-line-length */

import { TimelockOperations } from "script/utils/TimelockOperations.s.sol";
import { TimelockController } from "@openzeppelin/contracts/governance/TimelockController.sol";
import { Ownable2StepUpgradeable } from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol";
import { Predeploys } from "src/libraries/Predeploys.sol";
import { Create3 } from "src/deploy/Create3.sol";

/// @title TransferOwnershipsUpgradesEntrypoint
/// @notice Generates json files with the timelocked operations to transfer the ownership of the other half of the proxy admins to the new timelock
/// We start with UpgradesEntrypoint and move backwards, to test the migration in case of failure.
/// This contract is Ownable2StepUpgradeable, so we need to accept ownership transfer from the new timelock in the next step.
contract TransferOwnershipsUpgradesEntrypoint is TimelockOperations {
TimelockController public newTimelock;

address[] public from;

constructor() TimelockOperations("safe-migr-transfer-ownerships-upgrades-entrypoint") {
from = new address[](3);
from[0] = vm.envAddress("OLD_TIMELOCK_PROPOSER");
from[1] = vm.envAddress("OLD_TIMELOCK_EXECUTOR");
from[2] = vm.envAddress("OLD_TIMELOCK_CANCELLER");
bytes32 salt = keccak256("STORY_TIMELOCK_CONTROLLER_SAFE");
address newTimelockAddress = Create3(Predeploys.Create3).predictDeterministicAddress(salt);
newTimelock = TimelockController(payable(newTimelockAddress));
}

/// @dev the old timelock will execute the operations
function _getTargetTimelock() internal virtual override returns (address) {
return vm.envAddress("OLD_TIMELOCK_ADDRESS");
}

function _generate() internal virtual override {
require(address(newTimelock) != address(0), "Timelock not deployed");
require(address(newTimelock) != address(currentTimelock()), "Timelock already set");

bytes4 selector = Ownable2StepUpgradeable.transferOwnership.selector;
_generateAction(
from,
address(Predeploys.Upgrades),
0,
abi.encodeWithSelector(selector, address(newTimelock)),
bytes32(0),
bytes32(0),
minDelay
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/* solhint-disable no-console */
/* solhint-disable max-line-length */
// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;

import { console2 } from "forge-std/console2.sol";
import { TimelockOperations } from "script/utils/TimelockOperations.s.sol";
import { TimelockController } from "@openzeppelin/contracts/governance/TimelockController.sol";
import { Ownable2StepUpgradeable } from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol";
import { Predeploys } from "src/libraries/Predeploys.sol";
import { Create3 } from "src/deploy/Create3.sol";

/// @title ReceiveOwnershipUpgradesEntryPoint
/// @notice Generates json files with the timelocked operations to receive the ownership of the contracts from the old timelock
contract ReceiveOwnershipUpgradesEntryPoint is TimelockOperations {
TimelockController public newTimelock;

address[] public from;

constructor() TimelockOperations("safe-migr-receive-ownerships-upgrades-entrypoint") {
from = new address[](3);
from[0] = vm.envAddress("SAFE_TIMELOCK_PROPOSER");
from[1] = vm.envAddress("SAFE_TIMELOCK_EXECUTOR");
from[2] = vm.envAddress("SAFE_TIMELOCK_CANCELLER");

console2.log("from---------->", from[0], from[1], from[2]);
}

/// @dev target timelock is the newer timelock
function _getTargetTimelock() internal virtual override returns (address) {
bytes32 salt = keccak256("STORY_TIMELOCK_CONTROLLER_SAFE");
address newTimelockAddress = Create3(Predeploys.Create3).predictDeterministicAddress(salt);
newTimelock = TimelockController(payable(newTimelockAddress));
require(address(newTimelock) != address(0), "Timelock not deployed");
require(address(newTimelock) != address(currentTimelock()), "Timelock already set");
return address(newTimelock);
}

function _generate() internal virtual override {
bytes4 selector = Ownable2StepUpgradeable.acceptOwnership.selector;
_generateAction(
from,
address(Predeploys.Upgrades),
0,
abi.encodeWithSelector(selector),
bytes32(0),
bytes32(0),
minDelay
);
}
}
Loading
Loading