Skip to content

Commit

Permalink
Add functions to get approval process information from Defender (#23)
Browse files Browse the repository at this point in the history
Co-authored-by: Ernesto García <ernestognw@gmail.com>
  • Loading branch information
ericglau and ernestognw authored Feb 20, 2024
1 parent 2dbfade commit f38a460
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 30 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# Changelog

## Unreleased
## 0.0.2 (2024-02-20)

- Support constructor arguments for Defender deployments. ([#16](https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades/pull/16))
- Support Defender deployments for upgradeable contracts. ([#18](https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades/pull/18))
- Add `Defender.proposeUpgrade` function. ([#21](https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades/pull/21))
- Add functions to get approval process information from Defender ([#23](https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades/pull/23))

### Breaking changes
- `Defender.deployContract` functions now return `address` instead of `string`.
Expand Down
18 changes: 14 additions & 4 deletions DEFENDER.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ pragma solidity ^0.8.20;
import {Script} from "forge-std/Script.sol";
import {console} from "forge-std/console.sol";
import {Defender, ApprovalProcessResponse} from "openzeppelin-foundry-upgrades/Defender.sol";
import {Upgrades, Options} from "openzeppelin-foundry-upgrades/Upgrades.sol";
import {MyContract} from "../src/MyContract.sol";
Expand All @@ -54,12 +55,18 @@ contract DefenderScript is Script {
function setUp() public {}
function run() public {
ApprovalProcessResponse memory upgradeApprovalProcess = Defender.getUpgradeApprovalProcess();
if (upgradeApprovalProcess.via == address(0)) {
revert(string.concat("Upgrade approval process with id ", upgradeApprovalProcess.approvalProcessId, " has no assigned address"));
}
Options memory opts;
opts.defender.useDefenderDeploy = true;
address proxy = Upgrades.deployUUPSProxy(
"MyContract.sol",
abi.encodeCall(MyContract.initialize, ("arguments for the initialize function")),
abi.encodeCall(MyContract.initialize, ("Hello World", upgradeApprovalProcess.via)),
opts
);
Expand All @@ -73,12 +80,15 @@ Then run the following command:
forge script <path to the script you created above> --ffi --rpc-url <RPC URL for the network you want to use>
```

The above example calls the `Upgrades.deployUUPSProxy` function with the `defender.useDefenderDeploy` option to deploy both the implementation contract and a UUPS proxy to the connected network using Defender. The function waits for the deployments to complete, which may take a few minutes per contract, then returns with the deployed proxy address. While the function is waiting, you can monitor your deployment status in OpenZeppelin Defender's [Deploy module](https://defender.openzeppelin.com/v2/#/deploy).
The above example assumes the implementation contract takes an initial owner address as an argument for its `initialize` function. The script retrieves the address associated with the upgrade approval process configured in Defender (such as a multisig address), and uses that address as the initial owner so that it can have upgrade rights for the proxy.

This example calls the `Upgrades.deployUUPSProxy` function with the `defender.useDefenderDeploy` option to deploy both the implementation contract and a UUPS proxy to the connected network using Defender. The function waits for the deployments to complete, which may take a few minutes per contract, then returns with the deployed proxy address. While the function is waiting, you can monitor your deployment status in OpenZeppelin Defender's [Deploy module](https://defender.openzeppelin.com/v2/#/deploy).

> **Note:**
> If using an EOA or Safe to deploy, you must submit the pending deployments in Defender while the script is running. The script waits for each deployment to complete before it continues.
**Example 2 - Upgrading a proxy**:
**Example 2 - Proposing an upgrade to a proxy**:
To propose an upgrade through Defender, create a script like the following:
```
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
Expand All @@ -105,7 +115,7 @@ contract DefenderScript is Script {
}
}
```
Then run the script as in Example 1.
Then run the script as in Example 1, and go the resulting URL to review and approve the upgrade proposal.

### Non-Upgradeable Contracts

Expand Down
25 changes: 25 additions & 0 deletions src/Defender.sol
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,34 @@ library Defender {
opts
);
}

/**
* @dev Gets the default deploy approval process configured for your deployment environment on OpenZeppelin Defender.
*
* @return Struct with the default deploy approval process ID and the associated address, such as a Relayer, EOA, or multisig wallet address.
*/
function getDeployApprovalProcess() internal returns (ApprovalProcessResponse memory) {
return DefenderDeploy.getApprovalProcess("getDeployApprovalProcess");
}

/**
* @dev Gets the default upgrade approval process configured for your deployment environment on OpenZeppelin Defender.
* For example, this is useful for determining the default multisig wallet that you can use in your scripts to assign as the owner of your proxy.
*
* @return Struct with the default upgrade approval process ID and the associated address, such as a multisig or governor contract address.
*/
function getUpgradeApprovalProcess() internal returns (ApprovalProcessResponse memory) {
return DefenderDeploy.getApprovalProcess("getUpgradeApprovalProcess");
}
}

struct ProposeUpgradeResponse {
string proposalId;
string url;
}

struct ApprovalProcessResponse {
string approvalProcessId;
address via;
string viaType;
}
99 changes: 75 additions & 24 deletions src/internal/DefenderDeploy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
import {Utils, ContractInfo} from "./Utils.sol";
import {Versions} from "./Versions.sol";
import {Options, DefenderOptions} from "../Options.sol";
import {ProposeUpgradeResponse} from "../Defender.sol";
import {ProposeUpgradeResponse, ApprovalProcessResponse} from "../Defender.sol";

/**
* @dev Internal helper methods for Defender deployments.
Expand Down Expand Up @@ -42,13 +42,8 @@ library DefenderDeploy {
revert(string.concat("Failed to deploy contract ", contractName, ": ", string(result.stderr)));
}

strings.slice memory delim = "Deployed to address: ".toSlice();
if (stdout.toSlice().contains(delim)) {
string memory deployedAddress = stdout.toSlice().copy().find(delim).beyond(delim).toString();
return Vm(Utils.CHEATCODE_ADDRESS).parseAddress(deployedAddress);
} else {
revert(string.concat("Failed to parse deployment address from output: ", stdout));
}
string memory deployedAddress = _parseLine("Deployed to address: ", stdout, true);
return Vm(Utils.CHEATCODE_ADDRESS).parseAddress(deployedAddress);
}

function buildDeployCommand(
Expand Down Expand Up @@ -144,26 +139,29 @@ library DefenderDeploy {

function parseProposeUpgradeResponse(string memory stdout) internal pure returns (ProposeUpgradeResponse memory) {
ProposeUpgradeResponse memory response;
response.proposalId = _parseLine("Proposal ID: ", stdout, true);
response.url = _parseLine("Proposal URL: ", stdout, false);
return response;
}

strings.slice memory idDelim = "Proposal ID: ".toSlice();
strings.slice memory urlDelim = "Proposal URL: ".toSlice();

if (stdout.toSlice().contains(idDelim)) {
strings.slice memory idSlice = stdout.toSlice().copy().find(idDelim).beyond(idDelim);
// Remove any following lines, such as the Proposal URL line
if (idSlice.contains("\n".toSlice())) {
idSlice = idSlice.split("\n".toSlice());
function _parseLine(
string memory expectedPrefix,
string memory stdout,
bool required
) private pure returns (string memory) {
strings.slice memory delim = expectedPrefix.toSlice();
if (stdout.toSlice().contains(delim)) {
strings.slice memory slice = stdout.toSlice().copy().find(delim).beyond(delim);
// Remove any following lines
if (slice.contains("\n".toSlice())) {
slice = slice.split("\n".toSlice());
}
response.proposalId = idSlice.toString();
return slice.toString();
} else if (required) {
revert(string.concat("Failed to find line with prefix '", expectedPrefix, "' in output: ", stdout));
} else {
revert(string.concat("Failed to parse proposal ID from output: ", stdout));
return "";
}

if (stdout.toSlice().contains(urlDelim)) {
response.url = stdout.toSlice().copy().find(urlDelim).beyond(urlDelim).toString();
}

return response;
}

function buildProposeUpgradeCommand(
Expand Down Expand Up @@ -210,4 +208,57 @@ library DefenderDeploy {

return inputs;
}

function getApprovalProcess(string memory command) internal returns (ApprovalProcessResponse memory) {
string[] memory inputs = buildGetApprovalProcessCommand(command);

Vm.FfiResult memory result = Utils.runAsBashCommand(inputs);
string memory stdout = string(result.stdout);

if (result.exitCode != 0) {
revert(string.concat("Failed to get approval process: ", string(result.stderr)));
}

return parseApprovalProcessResponse(stdout);
}

function parseApprovalProcessResponse(string memory stdout) internal pure returns (ApprovalProcessResponse memory) {
Vm vm = Vm(Utils.CHEATCODE_ADDRESS);

ApprovalProcessResponse memory response;

response.approvalProcessId = _parseLine("Approval process ID: ", stdout, true);

string memory viaString = _parseLine("Via: ", stdout, false);
if (viaString.toSlice().len() != 0) {
response.via = vm.parseAddress(viaString);
}

response.viaType = _parseLine("Via type: ", stdout, false);

return response;
}

function buildGetApprovalProcessCommand(string memory command) internal view returns (string[] memory) {
string[] memory inputBuilder = new string[](255);

uint8 i = 0;

inputBuilder[i++] = "npx";
inputBuilder[i++] = string.concat(
"@openzeppelin/defender-deploy-client-cli@",
Versions.DEFENDER_DEPLOY_CLIENT_CLI
);
inputBuilder[i++] = command;
inputBuilder[i++] = "--chainId";
inputBuilder[i++] = Strings.toString(block.chainid);

// Create a copy of inputs but with the correct length
string[] memory inputs = new string[](i);
for (uint8 j = 0; j < i; j++) {
inputs[j] = inputBuilder[j];
}

return inputs;
}
}
38 changes: 37 additions & 1 deletion test/internal/DefenderDeploy.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {Utils, ContractInfo} from "openzeppelin-foundry-upgrades/internal/Utils.
import {DefenderDeploy} from "openzeppelin-foundry-upgrades/internal/DefenderDeploy.sol";
import {Versions} from "openzeppelin-foundry-upgrades/internal/Versions.sol";
import {Options, DefenderOptions} from "openzeppelin-foundry-upgrades/Options.sol";
import {ProposeUpgradeResponse} from "openzeppelin-foundry-upgrades/Defender.sol";
import {ProposeUpgradeResponse, ApprovalProcessResponse} from "openzeppelin-foundry-upgrades/Defender.sol";
import {WithConstructor} from "../contracts/WithConstructor.sol";

/**
Expand Down Expand Up @@ -151,4 +151,40 @@ contract DefenderDeployTest is Test {
assertEq(response.proposalId, "123");
assertEq(response.url, "");
}

function testBuildGetApprovalProcessCommand() public {
string memory commandString = _toString(
DefenderDeploy.buildGetApprovalProcessCommand("getDeployApprovalProcess")
);

assertEq(
commandString,
string.concat(
"npx @openzeppelin/defender-deploy-client-cli@",
Versions.DEFENDER_DEPLOY_CLIENT_CLI,
" getDeployApprovalProcess --chainId 31337"
)
);
}

function testParseApprovalProcessResponse() public {
string
memory output = "Approval process ID: abc\nVia: 0x1230000000000000000000000000000000000456\nVia type: Relayer";

ApprovalProcessResponse memory response = DefenderDeploy.parseApprovalProcessResponse(output);

assertEq(response.approvalProcessId, "abc");
assertEq(response.via, 0x1230000000000000000000000000000000000456);
assertEq(response.viaType, "Relayer");
}

function testParseApprovalProcessResponseIdOnly() public {
string memory output = "Approval process ID: abc";

ApprovalProcessResponse memory response = DefenderDeploy.parseApprovalProcessResponse(output);

assertEq(response.approvalProcessId, "abc");
assertTrue(response.via == address(0));
assertEq(response.viaType, "");
}
}

0 comments on commit f38a460

Please sign in to comment.