Skip to content
Merged
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
42 changes: 42 additions & 0 deletions script/deploy/Utils.sol
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,20 @@ contract Utils is Script {
address SystemDictatorProxy;
}

struct AddressesL2ImplementationsConfig {
address BaseFeeVault;
address GasPriceOracle;
address L1Block;
address L1FeeVault;
address L2CrossDomainMessenger;
address L2ERC721Bridge;
address L2StandardBridge;
address L2ToL1MessagePasser;
address SequencerFeeVault;
address OptimismMintableERC20Factory;
address OptimismMintableERC721Factory;
}

function getDeployBedrockConfig() external view returns(DeployBedrockConfig memory) {
string memory root = vm.projectRoot();
string memory path = string.concat(root, "/inputs/foundry-config.json");
Expand All @@ -60,6 +74,14 @@ contract Utils is Script {
return abi.decode(addressRaw, (AddressesConfig));
}

function readImplAddressesL2File() external view returns (AddressesL2ImplementationsConfig memory) {
string memory root = vm.projectRoot();
string memory addressPath = string.concat(root, "/inputs/addresses-l2.json");
string memory addressJson = vm.readFile(addressPath);
bytes memory addressRaw = vm.parseJson(addressJson);
return abi.decode(addressRaw, (AddressesL2ImplementationsConfig));
}

function writeAddressesFile(AddressesConfig memory cfg) external {
string memory json= "";

Expand All @@ -80,4 +102,24 @@ contract Utils is Script {

finalJson.write(string.concat("unsorted.json"));
}

function writeImplAddressesL2File(AddressesL2ImplementationsConfig memory cfg) external {
string memory json = "";

vm.serializeAddress(json, "BaseFeeVault", cfg.BaseFeeVault);
vm.serializeAddress(json, "GasPriceOracle", cfg.GasPriceOracle);
vm.serializeAddress(json, "L1Block", cfg.L1Block);
vm.serializeAddress(json, "L1FeeVault", cfg.L1FeeVault);
vm.serializeAddress(json, "L2CrossDomainMessenger", cfg.L2CrossDomainMessenger);
vm.serializeAddress(json, "L2ERC721Bridge", cfg.L2ERC721Bridge);
vm.serializeAddress(json, "L2StandardBridge", cfg.L2StandardBridge);
vm.serializeAddress(json, "L2ToL1MessagePasser", cfg.L2ToL1MessagePasser);
vm.serializeAddress(json, "SequencerFeeVault", cfg.SequencerFeeVault);
vm.serializeAddress(json, "OptimismMintableERC20Factory", cfg.OptimismMintableERC20Factory);
string memory finalJson = vm.serializeAddress(
json, "OptimismMintableERC721Factory", cfg.OptimismMintableERC721Factory
);

finalJson.write(string.concat("unsortedl2Impls.json"));
}
}
16 changes: 16 additions & 0 deletions script/deploy/l2/DeployBedrockL2ImplContracts.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ contract DeployBedrockL2ImplContracts is Script {
Utils utils;
address deployer;
Utils.DeployBedrockConfig deployConfig;
Utils.AddressesL2ImplementationsConfig addressL2Cfg;

// Implementations
BaseFeeVault baseFeeVaultImpl;
Expand Down Expand Up @@ -99,5 +100,20 @@ contract DeployBedrockL2ImplContracts is Script {
optimismMintableERC721FactoryImpl = new OptimismMintableERC721Factory(Predeploys.L2_ERC721_BRIDGE, deployConfig.l2ChainId);
require(address(optimismMintableERC721FactoryImpl.BRIDGE()) == address(Predeploys.L2_ERC721_BRIDGE), "Deploy: optimismMintableERC721Factory l2ERC721BridgeProxy is incorrect");
require(optimismMintableERC721FactoryImpl.REMOTE_CHAIN_ID() == deployConfig.l2ChainId, "Deploy: optimismMintableERC721Factory chain ID is incorrect");

// Publish L2 implementation contract addresses
addressL2Cfg.BaseFeeVault = address(baseFeeVaultImpl);
addressL2Cfg.GasPriceOracle = address(gasPriceOracleImpl);
addressL2Cfg.L1Block = address(l1BlockImpl);
addressL2Cfg.L1FeeVault = address(l1FeeVaultImpl);
addressL2Cfg.L2CrossDomainMessenger = address(l2CrossDomainMessengerImpl);
addressL2Cfg.L2ERC721Bridge = address(l2ERC721BridgeImpl);
addressL2Cfg.L2StandardBridge = address(l2StandardBridgeImpl);
addressL2Cfg.L2ToL1MessagePasser = address(l2ToL1MessagePasserImpl);
addressL2Cfg.SequencerFeeVault = address(sequencerFeeVaultImpl);
addressL2Cfg.OptimismMintableERC20Factory = address(optimismMintableERC20FactoryImpl);
addressL2Cfg.OptimismMintableERC721Factory = address(optimismMintableERC721FactoryImpl);

utils.writeImplAddressesL2File(addressL2Cfg);
}
}
203 changes: 203 additions & 0 deletions script/upgrade/SafeBuilder.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
// SPDX-License-Identifier: MIT
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

diff with Optimism's code:

diff --git a/script/upgrade/SafeBuilder.sol b/script/upgrade/SafeBuilder.sol
index a0d9108..57574db 100644
--- a/script/upgrade/SafeBuilder.sol
+++ b/script/upgrade/SafeBuilder.sol
@@ -4,10 +4,11 @@ pragma solidity 0.8.15;
 import { console } from "forge-std/console.sol";
 import { IMulticall3 } from "forge-std/interfaces/IMulticall3.sol";
 
-import { LibSort } from "@eth-optimism-bedrock/scripts/libraries/LibSort.sol";
-import { IGnosisSafe, Enum } from "@eth-optimism-bedrock/scripts/interfaces/IGnosisSafe.sol";
-import { EnhancedScript } from "@eth-optimism-bedrock/scripts/universal/EnhancedScript.sol";
-import { GlobalConstants } from "@eth-optimism-bedrock/scripts/universal/GlobalConstants.sol";
+import { LibSort } from "../libraries/LibSort.sol";
+import { IGnosisSafe, Enum } from "../interfaces/IGnosisSafe.sol";
+import { EnhancedScript } from "../universal/EnhancedScript.sol";
+import { GlobalConstants } from "../universal/GlobalConstants.sol";
+import { ProxyAdmin } from "../../contracts/universal/ProxyAdmin.sol";
 
 /**
  * @title SafeBuilder
@@ -22,7 +23,6 @@ import { GlobalConstants } from "@eth-optimism-bedrock/scripts/universal/GlobalC
  *         Run the command without the `--broadcast` flag and it will print a tenderly URL.
  */
 abstract contract SafeBuilder is EnhancedScript, GlobalConstants {
-
     /**
      * @notice Interface for multicall3.
      */
@@ -47,8 +47,14 @@ abstract contract SafeBuilder is EnhancedScript, GlobalConstants {
     /**
      * @notice Creates the calldata
      */
-    function buildCalldata() internal virtual view returns (bytes memory);
+    function buildCalldata(address _proxyAdmin) internal virtual view returns (bytes memory);
 
+    /**
+     * @notice Internal helper function to compute the safe transaction hash.
+     */
+    function computeSafeTransactionHash(address _safe, address _proxyAdmin) public virtual returns (bytes32) {
+        return _getTransactionHash(_safe, _proxyAdmin);
+    }
 
     /**
      * -----------------------------------------------------------
@@ -59,9 +65,9 @@ abstract contract SafeBuilder is EnhancedScript, GlobalConstants {
     /**
      * @notice The entrypoint to this script.
      */
-    function run(address _safe) public returns (bool) {
+    function run(address _safe, address _proxyAdmin) public returns (bool) {
         vm.startBroadcast();
-        bool success = _run(_safe);
+        bool success = _run(_safe, _proxyAdmin);
         if (success) _postCheck();
         return success;
     }
@@ -69,15 +75,16 @@ abstract contract SafeBuilder is EnhancedScript, GlobalConstants {
     /**
      * @notice Computes the safe transaction hash for the provided safe and proxy admin.
      */
-    function _getTransactionHash(address _safe) internal view returns (bytes32) {
+    function _getTransactionHash(address _safe, address _proxyAdmin) internal view returns (bytes32) {
         // Ensure that the required contracts exist
         require(address(multicall).code.length > 0, "multicall3 not deployed");
         require(_safe.code.length > 0, "no code at safe address");
+        require(_proxyAdmin.code.length > 0, "no code at proxy admin address");
 
         IGnosisSafe safe = IGnosisSafe(payable(_safe));
         uint256 nonce = safe.nonce();
 
-        bytes memory data = buildCalldata();
+        bytes memory data = buildCalldata(_proxyAdmin);
 
         // Compute the safe transaction hash
         bytes32 hash = safe.getTransactionHash({
@@ -102,12 +109,12 @@ abstract contract SafeBuilder is EnhancedScript, GlobalConstants {
      *         the nonce changes by a different transaction finalizing while not
      *         all of the signers have used this script.
      */
-    function _run(address _safe) public returns (bool) {
+    function _run(address _safe, address _proxyAdmin) public returns (bool) {
         IGnosisSafe safe = IGnosisSafe(payable(_safe));
-        bytes memory data = buildCalldata();
+        bytes memory data = buildCalldata(_proxyAdmin);
 
         // Compute the safe transaction hash
-        bytes32 hash = _getTransactionHash(_safe);
+        bytes32 hash = _getTransactionHash(_safe, _proxyAdmin);
 
         // Send a transaction to approve the hash
         safe.approveHash(hash);

pragma solidity 0.8.15;

import { console } from "forge-std/console.sol";
import { IMulticall3 } from "forge-std/interfaces/IMulticall3.sol";

import { LibSort } from "@eth-optimism-bedrock/scripts/libraries/LibSort.sol";
import { IGnosisSafe, Enum } from "@eth-optimism-bedrock/scripts/interfaces/IGnosisSafe.sol";
import { EnhancedScript } from "@eth-optimism-bedrock/scripts/universal/EnhancedScript.sol";
import { GlobalConstants } from "@eth-optimism-bedrock/scripts/universal/GlobalConstants.sol";

/**
* @title SafeBuilder
* @notice Builds SafeTransactions
* Assumes that a gnosis safe is used as the privileged account and the same
* gnosis safe is the owner the proxy admin.
* This could be optimized by checking for the number of approvals up front
* and not submitting the final approval as `execTransaction` can be called when
* there are `threshold - 1` approvals.
* Uses the "approved hashes" method of interacting with the gnosis safe. Allows
* for the most simple user experience when using automation and no indexer.
* Run the command without the `--broadcast` flag and it will print a tenderly URL.
*/
abstract contract SafeBuilder is EnhancedScript, GlobalConstants {

/**
* @notice Interface for multicall3.
*/
IMulticall3 internal constant multicall = IMulticall3(MULTICALL3_ADDRESS);

/**
* @notice An array of approvals, used to generate the execution transaction.
*/
address[] internal approvals;

/**
* -----------------------------------------------------------
* Virtual Functions
* -----------------------------------------------------------
*/

/**
* @notice Follow up assertions to ensure that the script ran to completion.
*/
function _postCheck() internal virtual view;

/**
* @notice Creates the calldata
*/
function buildCalldata() internal virtual view returns (bytes memory);


/**
* -----------------------------------------------------------
* Implemented Functions
* -----------------------------------------------------------
*/

/**
* @notice The entrypoint to this script.
*/
function run(address _safe) public returns (bool) {
vm.startBroadcast();
bool success = _run(_safe);
if (success) _postCheck();
return success;
}

/**
* @notice Computes the safe transaction hash for the provided safe and proxy admin.
*/
function _getTransactionHash(address _safe) internal view returns (bytes32) {
// Ensure that the required contracts exist
require(address(multicall).code.length > 0, "multicall3 not deployed");
require(_safe.code.length > 0, "no code at safe address");

IGnosisSafe safe = IGnosisSafe(payable(_safe));
uint256 nonce = safe.nonce();

bytes memory data = buildCalldata();

// Compute the safe transaction hash
bytes32 hash = safe.getTransactionHash({
to: address(multicall),
value: 0,
data: data,
operation: Enum.Operation.DelegateCall,
safeTxGas: 0,
baseGas: 0,
gasPrice: 0,
gasToken: address(0),
refundReceiver: address(0),
_nonce: nonce
});

return hash;
}

/**
* @notice The implementation of the upgrade. Split into its own function
* to allow for testability. This is subject to a race condition if
* the nonce changes by a different transaction finalizing while not
* all of the signers have used this script.
*/
function _run(address _safe) public returns (bool) {
IGnosisSafe safe = IGnosisSafe(payable(_safe));
bytes memory data = buildCalldata();

// Compute the safe transaction hash
bytes32 hash = _getTransactionHash(_safe);

// Send a transaction to approve the hash
safe.approveHash(hash);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we make it so we only approve if the given owner hasn't already?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed this would be good to save gas 👍 I'll address this as a follow up


logSimulationLink({
_to: address(safe),
_from: msg.sender,
_data: abi.encodeCall(safe.approveHash, (hash))
});

uint256 threshold = safe.getThreshold();
address[] memory owners = safe.getOwners();

for (uint256 i; i < owners.length; i++) {
address owner = owners[i];
uint256 approved = safe.approvedHashes(owner, hash);
if (approved == 1) {
approvals.push(owner);
}
}

if (approvals.length >= threshold) {
bytes memory signatures = buildSignatures();

bool success = safe.execTransaction({
to: address(multicall),
value: 0,
data: data,
operation: Enum.Operation.DelegateCall,
safeTxGas: 0,
baseGas: 0,
gasPrice: 0,
gasToken: address(0),
refundReceiver: payable(address(0)),
signatures: signatures
});

logSimulationLink({
_to: address(safe),
_from: msg.sender,
_data: abi.encodeCall(
safe.execTransaction,
(
address(multicall),
0,
data,
Enum.Operation.DelegateCall,
0,
0,
0,
address(0),
payable(address(0)),
signatures
)
)
});

require(success, "call not successful");
return true;
} else {
console.log("not enough approvals");
}

// Reset the approvals because they are only used transiently.
assembly {
sstore(approvals.slot, 0)
}

return false;
}

/**
* @notice Builds the signatures by tightly packing them together.
* Ensures that they are sorted.
*/
function buildSignatures() internal view returns (bytes memory) {
address[] memory addrs = new address[](approvals.length);
for (uint256 i; i < approvals.length; i++) {
addrs[i] = approvals[i];
}

LibSort.sort(addrs);

bytes memory signatures;
uint8 v = 1;
bytes32 s = bytes32(0);
for (uint256 i; i < addrs.length; i++) {
bytes32 r = bytes32(uint256(uint160(addrs[i])));
signatures = bytes.concat(signatures, abi.encodePacked(r, s, v));
}
return signatures;
}
}
Loading