Skip to content

Commit

Permalink
feat: wip refactor of deploy scripts
Browse files Browse the repository at this point in the history
* chore: remove unused release folder
  • Loading branch information
wadealexc committed Sep 30, 2024
1 parent 2044982 commit fd8c2e1
Show file tree
Hide file tree
Showing 76 changed files with 2,041 additions and 5,307 deletions.
113 changes: 113 additions & 0 deletions script/HoleskyTimelock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.12;

/// @notice Modified version of our mainnet timelock for use on Holesky
/// Specifically, this removes SafeMath and changes `MINIMUM_DELAY` to `0 days`
///
/// See original version here: https://github.com/compound-finance/compound-protocol/blob/a3214f67b73310d547e00fc578e8355911c9d376/contracts/Timelock.sol
contract HoleskyTimelock {

event NewAdmin(address indexed newAdmin);
event NewPendingAdmin(address indexed newPendingAdmin);
event NewDelay(uint indexed newDelay);
event CancelTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint eta);
event ExecuteTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint eta);
event QueueTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint eta);

uint public constant GRACE_PERIOD = 14 days;
uint public constant MINIMUM_DELAY = 0;
uint public constant MAXIMUM_DELAY = 30 days;

address public admin;
address public pendingAdmin;
uint public delay;

mapping (bytes32 => bool) public queuedTransactions;


constructor(address admin_, uint delay_) public {
require(delay_ >= MINIMUM_DELAY, "Timelock::constructor: Delay must exceed minimum delay.");
require(delay_ <= MAXIMUM_DELAY, "Timelock::setDelay: Delay must not exceed maximum delay.");

admin = admin_;
delay = delay_;
}

fallback() external payable { }

function setDelay(uint delay_) public {
require(msg.sender == address(this), "Timelock::setDelay: Call must come from Timelock.");
require(delay_ >= MINIMUM_DELAY, "Timelock::setDelay: Delay must exceed minimum delay.");
require(delay_ <= MAXIMUM_DELAY, "Timelock::setDelay: Delay must not exceed maximum delay.");
delay = delay_;

emit NewDelay(delay);
}

function acceptAdmin() public {
require(msg.sender == pendingAdmin, "Timelock::acceptAdmin: Call must come from pendingAdmin.");
admin = msg.sender;
pendingAdmin = address(0);

emit NewAdmin(admin);
}

function setPendingAdmin(address pendingAdmin_) public {
require(msg.sender == address(this), "Timelock::setPendingAdmin: Call must come from Timelock.");
pendingAdmin = pendingAdmin_;

emit NewPendingAdmin(pendingAdmin);
}

function queueTransaction(address target, uint value, string memory signature, bytes memory data, uint eta) public returns (bytes32) {
require(msg.sender == admin, "Timelock::queueTransaction: Call must come from admin.");
require(eta >= getBlockTimestamp() + delay, "Timelock::queueTransaction: Estimated execution block must satisfy delay.");

bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta));
queuedTransactions[txHash] = true;

emit QueueTransaction(txHash, target, value, signature, data, eta);
return txHash;
}

function cancelTransaction(address target, uint value, string memory signature, bytes memory data, uint eta) public {
require(msg.sender == admin, "Timelock::cancelTransaction: Call must come from admin.");

bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta));
queuedTransactions[txHash] = false;

emit CancelTransaction(txHash, target, value, signature, data, eta);
}

function executeTransaction(address target, uint value, string memory signature, bytes memory data, uint eta) public payable returns (bytes memory) {
require(msg.sender == admin, "Timelock::executeTransaction: Call must come from admin.");

bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta));
require(queuedTransactions[txHash], "Timelock::executeTransaction: Transaction hasn't been queued.");
require(getBlockTimestamp() >= eta, "Timelock::executeTransaction: Transaction hasn't surpassed time lock.");
require(getBlockTimestamp() <= eta + GRACE_PERIOD, "Timelock::executeTransaction: Transaction is stale.");

queuedTransactions[txHash] = false;

bytes memory callData;

if (bytes(signature).length == 0) {
callData = data;
} else {
callData = abi.encodePacked(bytes4(keccak256(bytes(signature))), data);
}

// solium-disable-next-line security/no-call-value
(bool success, bytes memory returnData) = target.call{value: value}(callData);
require(success, "Timelock::executeTransaction: Transaction execution reverted.");

emit ExecuteTransaction(txHash, target, value, signature, data, eta);

return returnData;
}

function getBlockTimestamp() internal view returns (uint) {
// solium-disable-next-line security/no-block-members
return block.timestamp;
}
}
157 changes: 157 additions & 0 deletions script/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
## Release Scripting

Desired usecases:
* Creating a new release script from a template (e.g. `zeus new v0.4.2-pepe`)
* Being able to see whether a release script has been run for a given environment (`zeus status v0.4.2-pepe`)
* Since release scripts are split into 3 parts (`deploy`, `queue`, `execute`), being able to see

`zeus run deploy pepe --sender "0x1234"`

`zeus run deploy pepe --live --ledger`

`zeus run queue pepe --live --ledger`
* https://docs.safe.global/sdk/api-kit
* For proposing txns to the Safe UI

`zeus status pepe`

### Creating a New Release Script

```
zeus new $VERSION $RELEASE_NAME
```

This command will generate a new release script based on `Release_Template.s.sol`. The new script is placed in the `/releases` folder and named accordingly (e.g. `zeus new v0.4.2-pepe` will create `releases/v0.4.2-pepe/script.s.sol`).

Release scripts look like this:

```solidity
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.12;
import "./utils/Releasoor.s.sol";
contract Release_TEMPLATE is Releasoor {
using TxBuilder for *;
using AddressUtils for *;
function deploy(Addresses memory addrs) internal override {
// If you're deploying contracts, do that here
}
function queueUpgrade(Addresses memory addrs) internal override {
// If you're queueing an upgrade via the timelock, you can
// define and encode those transactions here
}
function executeUpgrade(Addresses memory addrs) internal override {
// Whether you are using the timelock or just making transactions
// from the ops multisig, you can define/encode those transactions here
}
}
```

### Implementing Your Release Script

Release scripts have three functions which may or may not be used, depending on your needs. Generally, a release consists of three steps:
1. Deploying new implementation contracts
2. Queueing an upgrade to these contracts in the timelock
3. Executing the queued timelock upgrade after the timelock delay has passed

However, your release may not require deploying or queueing an upgrade in the timelock. For example, if all you want to do is have the ops multisig call `StrategyFactory.whitelistStrategies`, you don't need to deploy any contracts or queue a timelock transaction. In this case, you don't need to fill in the `deploy` or `queueUpgrade` methods - all you need to implement is `executeUpgrade`.

#### Deploying a New Contract

Typically, we deploy new contracts via EOA. As such, deploying new contracts can be done directly in the `deploy` method of your release script by invoking `vm.startBroadcast()`, deploying your contract(s), then invoking `vm.stopBroadcast()`.

**Important**: In order to keep our configs up to date, ensure your deploy scripts call `addrs.X.setPending(newImpl)`! As you run this script for various environments, this will automatically update each environment's config to track these new deployments as "pending" upgrades to the contract in question.

```solidity
function deploy(Addresses memory addrs) public override {
vm.startBroadcast();
// Deploy new implementation
EigenPod newEigenPodImpl = new EigenPod(
IETHPOSDeposit(params.ethPOS),
IEigenPodManager(addrs.eigenPodManager.proxy),
params.EIGENPOD_GENESIS_TIME
);
vm.stopBroadcast();
// Update the current environment's config
// (sets eigenPod.pendingImpl = newEigenPodImpl)
addrs.eigenPod.setPending(address(newEigenPodImpl));
}
```

You can run your deploy script on a forked version of an existing environment with the following command:
* `make deploy <release name> <environment>`

e.g. `make deploy pepe preprod` will:
* Read `preprod.config` and populate [your script helpers](#script-helpers)
* Spin up an anvil fork of the preprod environment
* Deploy your contract(s) to the anvil fork
* Update `preprod.config` with the `pendingImpl` addresses (via `setPending`)
<!--
TODO - should config be updated if we know we're on a local fork?
Ideally, yes -- because it's helpful to see a git diff that shows the config got updated
However, we don't want people committing pendingImpls for local tests...
-->

After you've tested the script using a forked environment, you can run it on a live network with the following command:
* `make deploy <release name> <environment> --live`

e.g. `make deploy pepe preprod --live` will:
* Read `preprod.config` and populate [your script helpers](#script-helpers)
* Deploy your contracts to `preprod` using the configured RPC endpoint and a ledger connection
* Verify the deployed contracts on etherscan
* Update `preprod.config` with the `pendingImpl` addresses (via `setPending`)

#### Queueing a Timelock Transaction

The most common thing you'll need to do when deploying a release is using the ops multisig to queue transactions via the timelock. Defining transactions for this stage of the process can involve multiple contracts. Ultimately, the goal is to *define one or more contract calls that the executor multisig will carry out*.

However, the actual output of this part of the process will be the contract call made by the ops multisig to the timelock queueing a transaction that (when the delay has elapsed) can be executed to trigger the executor multisig to make the aforementioned calls. Whew.

Instead of trying to keep track of all of this, `queueUpgrade` can take care of this for you. All you need to define (and return) is:
1. The ETA (TODO different ETA between environments)
2. The contract calls you want the executor multisig to execute

```solidity
function queueUpgrade(Addresses memory addrs) public override returns (Tx[] memory executorTxns, uint eta) {
Txs storage txs = _newTxs();
eta = env.isMainnet() ? 12351235 : 0;
txs.append({
to: addrs.eigenPod.beacon,
data: EncBeacon.upgradeTo(addrs.eigenPod.getPending())
});
txs.append({
to: addrs.eigenPodManager.proxy,
data: EncProxyAdmin.upgrade(addrs.eigenPodManager.proxy, addrs.eigenPodManager.getPending())
});
return (txs.toArray(), eta);
}
```

The above function defines two transactions for the executor multisig:
* A call to `eigenPod.beacon.upgradeTo()`, upgrading the `eigenPod` beacon
* A call to `proxyAdmin.upgrade()`, upgrading the `eigenPodManager` proxy

**Important**: Note that the implementation addresses for the `eigenPod` and `eigenPodManager` are fetched using `getPending()`, which gets the `pendingImpl` for both addresses (recall that this `pendingImpl` was set during the `deploy` part of this release script). If `getPending()` finds that no `pendingImpl` address has been set, it will revert. This prevents `queueUpgrade` from running if it has a dependency on `deploy`.

You can run the `queueUpgrade` script on a forked version of an existing environment with the following command:
* `make queue <release name> <environment>`

e.g. `make queue pepe preprod` will:
* Read `preprod.config` and populate [your script helpers](#script-helpers)
* Spin up an anvil fork of the preprod environment
*
* Update `preprod.config` with the `pendingImpl` addresses (via `setPending`)

### Script Helpers
119 changes: 119 additions & 0 deletions script/README_OLD.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
Use Cases:
1. Fork a current deployment on any network, given a config file
* Use - simulating upgrades/changes to a current deployment
2. Deploy entire system from the current branch to any network
* Use - integration tests (mainly want to deploy locally)
3. Easily deploy/upgrade _specific_ contracts on any ENV
* Use - writing deploy/upgrade scripts
* Note: this should also update that env's config with new addresses

## Mock Command - Release/Upgrade Scripting

```
make release preprod
```

* Generates a script file (`preprod-<GIT_COMMIT_HEAD>.s.sol`) that automatically loads the config for the `preprod` environment
* `make release preprod --test preprod-<GIT_COMMIT_HEAD>.s.sol`:
* Run the script in "test mode", forking preprod locally via anvil and simulating the deploy and upgrade steps specified in the script.
* Aside from helpful console output, this should generate an output file (to a `.gitignored` directory) that shows what the new config values will be after running for real.
* `make release preprod --run preprod-<GIT_COMMIT_HEAD>.s.sol`:
* Run the script for real, submitting both deploy and upgrade transactions to preprod, then updating the preprod config with the new addresses
* If `holesky` or `mainnet` environments are used here, the `upgrade` step should generate multisig transactions that can be signed

## Mock Command - Deploying

```
make deploy preprod
```

* Deploys the entire system to `preprod` using the `DeployAll` script
* Generates an output file that gives the config for this new system. This is generated to a `.gitignored` directory, but if moved into the `config` folder, it can become a named, usable environment

---

## Preprod Release Workflow

#### Deploying New Contracts

```
$ make release pepe
Generated release script: `./scripts/v0_4_5_pepe.s.sol`
```

<Edit script (`deploy`)>

```
$ make deploy pepe --preprod --dry-run
Launching anvil using $RPC... done
Running `v0_4_5_pepe.s.sol:deploy`... done
Results ("preprod.json"):
{
"eigenPod": {
"pendingImpl": "0xDEADBEEF"
},
"eigenPodManager": {
"pendingImpl": "0xABADDEED"
}
}
```

<Confirm results look correct, then run again>

```
$ make deploy pepe --preprod
Launching anvil using $RPC... done
Running `v0_4_5_pepe.s.sol:deploy`... done
Results ("preprod.json"):
{
"eigenPod": {
"pendingImpl": "0xDEADBEEF"
},
"eigenPodManager": {
"pendingImpl": "0xABADDEED"
}
}
Is this correct? Press (y/n) to update config: y
Updating `config/preprod.json`... done
```

Contracts should be successfully deployed, and config updated.

#### Perform Upgrade

<Edit existing script (`execute`)>

```
$ make execute pepe --preprod --dry-run
Launching anvil using $RPC... done
Running `v0_4_5_pepe.s.sol:execute`... done
Actions:
[
"executorMultisig": [
"eigenPodBeacon.proxy.upgradeTo(pendingImpl)",
"proxyAdmin.upgrade(eigenPodManager.proxy, pendingImpl)"
]
]
Results ("preprod.json"):
{
"eigenPod": {
"impl": "0xDEADBEEF"
},
"eigenPodManager": {
"impl": "0xABADDEED"
}
}
```
Loading

0 comments on commit fd8c2e1

Please sign in to comment.