From 3c206341c7ae2a80977662cf9dc0e97e7e2fd044 Mon Sep 17 00:00:00 2001 From: Michael de Hoog Date: Mon, 9 Dec 2024 06:01:22 -1000 Subject: [PATCH] Chain exit: add support for upgrades and ownership transfer to the minimal proxy implementation (#25) * Add upgrade and ownership transfer support to minimal proxy * Fix upgradeToAndCall return value * Add some comments * forge fmt --- src/DeployChain.sol | 64 +++---------- src/ResolvingProxy.sol | 155 +++++++++++++++++++++++++++++++ src/ResolvingProxyFactory.sol | 74 +++++++++++++++ test/ResolvingProxyFactory.t.sol | 92 ++++++++++++++++++ 4 files changed, 336 insertions(+), 49 deletions(-) create mode 100644 src/ResolvingProxy.sol create mode 100644 src/ResolvingProxyFactory.sol create mode 100644 test/ResolvingProxyFactory.t.sol diff --git a/src/DeployChain.sol b/src/DeployChain.sol index 16fd7bd..6460dfd 100644 --- a/src/DeployChain.sol +++ b/src/DeployChain.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; +import {ResolvingProxyFactory} from "./ResolvingProxyFactory.sol"; import {Portal} from "./Portal.sol"; import {OutputOracle} from "./OutputOracle.sol"; import {SystemConfigOwnable} from "./SystemConfigOwnable.sol"; @@ -101,13 +102,13 @@ contract DeployChain { function deployAddresses(uint256 chainID) external view returns (DeployAddresses memory) { bytes32 salt = keccak256(abi.encodePacked(chainID)); return DeployAddresses({ - l2OutputOracle: proxyAddress(l2OutputOracle, salt), - systemConfig: proxyAddress(systemConfig, salt), - optimismPortal: proxyAddress(optimismPortal, salt), - l1CrossDomainMessenger: proxyAddress(l1CrossDomainMessenger, salt), - l1StandardBridge: proxyAddress(l1StandardBridge, salt), - l1ERC721Bridge: proxyAddress(l1ERC721Bridge, salt), - optimismMintableERC20Factory: proxyAddress(optimismMintableERC20Factory, salt) + l2OutputOracle: ResolvingProxyFactory.proxyAddress(l2OutputOracle, proxyAdmin, salt), + systemConfig: ResolvingProxyFactory.proxyAddress(systemConfig, proxyAdmin, salt), + optimismPortal: ResolvingProxyFactory.proxyAddress(optimismPortal, proxyAdmin, salt), + l1CrossDomainMessenger: ResolvingProxyFactory.proxyAddress(l1CrossDomainMessenger, proxyAdmin, salt), + l1StandardBridge: ResolvingProxyFactory.proxyAddress(l1StandardBridge, proxyAdmin, salt), + l1ERC721Bridge: ResolvingProxyFactory.proxyAddress(l1ERC721Bridge, proxyAdmin, salt), + optimismMintableERC20Factory: ResolvingProxyFactory.proxyAddress(optimismMintableERC20Factory, proxyAdmin, salt) }); } @@ -149,13 +150,13 @@ contract DeployChain { function setupProxies(uint256 chainID) internal returns (DeployAddresses memory) { bytes32 salt = keccak256(abi.encodePacked(chainID)); return DeployAddresses({ - l2OutputOracle: setupProxy(l2OutputOracle, salt), - systemConfig: setupProxy(systemConfig, salt), - optimismPortal: setupProxy(optimismPortal, salt), - l1CrossDomainMessenger: setupProxy(l1CrossDomainMessenger, salt), - l1StandardBridge: setupProxy(l1StandardBridge, salt), - l1ERC721Bridge: setupProxy(l1ERC721Bridge, salt), - optimismMintableERC20Factory: setupProxy(optimismMintableERC20Factory, salt) + l2OutputOracle: ResolvingProxyFactory.setupProxy(l2OutputOracle, proxyAdmin, salt), + systemConfig: ResolvingProxyFactory.setupProxy(systemConfig, proxyAdmin, salt), + optimismPortal: ResolvingProxyFactory.setupProxy(optimismPortal, proxyAdmin, salt), + l1CrossDomainMessenger: ResolvingProxyFactory.setupProxy(l1CrossDomainMessenger, proxyAdmin, salt), + l1StandardBridge: ResolvingProxyFactory.setupProxy(l1StandardBridge, proxyAdmin, salt), + l1ERC721Bridge: ResolvingProxyFactory.setupProxy(l1ERC721Bridge, proxyAdmin, salt), + optimismMintableERC20Factory: ResolvingProxyFactory.setupProxy(optimismMintableERC20Factory, proxyAdmin, salt) }); } @@ -264,39 +265,4 @@ contract DeployChain { gasPayingToken: gasToken }); } - - function setupProxy(address proxy, bytes32 salt) internal returns (address instance) { - address _proxyAdmin = proxyAdmin; - /// @solidity memory-safe-assembly - assembly { - let ptr := mload(0x40) - mstore(ptr, 0x60678060095f395ff363204e1c7a60e01b5f5273000000000000000000000000) - mstore(add(ptr, 0x14), shl(0x60, proxy)) - mstore(add(ptr, 0x28), 0x6004525f5f60245f730000000000000000000000000000000000000000000000) - mstore(add(ptr, 0x31), shl(0x60, _proxyAdmin)) - mstore(add(ptr, 0x45), 0x5afa3d5f5f3e3d60201416604d573d5ffd5b5f5f365f5f51365f5f375af43d5f) - mstore(add(ptr, 0x65), 0x5f3e5f3d91606557fd5bf3000000000000000000000000000000000000000000) - instance := create2(0, ptr, 0x70, salt) - } - require(instance != address(0), "Proxy: create2 failed"); - } - - function proxyAddress(address proxy, bytes32 salt) internal view returns (address predicted) { - address _proxyAdmin = proxyAdmin; - address deployer = address(this); - /// @solidity memory-safe-assembly - assembly { - let ptr := mload(0x40) - mstore(ptr, 0x60678060095f395ff363204e1c7a60e01b5f5273000000000000000000000000) - mstore(add(ptr, 0x14), shl(0x60, proxy)) - mstore(add(ptr, 0x28), 0x6004525f5f60245f730000000000000000000000000000000000000000000000) - mstore(add(ptr, 0x31), shl(0x60, _proxyAdmin)) - mstore(add(ptr, 0x45), 0x5afa3d5f5f3e3d60201416604d573d5ffd5b5f5f365f5f51365f5f375af43d5f) - mstore(add(ptr, 0x65), 0x5f3e5f3d91606557fd5bf3ff0000000000000000000000000000000000000000) - mstore(add(ptr, 0x71), shl(0x60, deployer)) - mstore(add(ptr, 0x85), salt) - mstore(add(ptr, 0xa5), keccak256(ptr, 0x70)) - predicted := keccak256(add(ptr, 0x70), 0x55) - } - } } diff --git a/src/ResolvingProxy.sol b/src/ResolvingProxy.sol new file mode 100644 index 0000000..6a15aba --- /dev/null +++ b/src/ResolvingProxy.sol @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +// The ProxyAdmin contract conforms to this interface. +interface IResolver { + function getProxyImplementation(address _proxy) external view returns (address); +} + +/// @notice ResolvingProxy is a modification of the op-stack Proxy contract that allows for +/// proxying a proxy. This is useful to have a central upgradable proxy that this +/// contract can point to, but also support detaching this proxy so it can have its +/// own implementation. +/// @dev Only proxies that are owned by a ProxyAdmin can be proxied by this contract, +/// because it calls getProxyImplementation() on the ProxyAdmin to retrieve the +/// implementation address. +/// @dev This contract is based on the EIP-1967 transparent proxy standard. It is slightly +/// simplified in that it doesn't emit logs for implementation and admin changes. This +/// is to simplify the assembly implementation provided in ResolvingProxyFactory. +contract ResolvingProxy { + /// @notice The storage slot that holds the address of a proxy implementation. + /// @dev `bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)` + bytes32 internal constant IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + + /// @notice The storage slot that holds the address of the owner. + /// @dev `bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1)` + bytes32 internal constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; + + /// @notice A modifier that reverts if not called by the owner or by address(0) to allow + /// eth_call to interact with this proxy without needing to use low-level storage + /// inspection. We assume that nobody is able to trigger calls from address(0) during + /// normal EVM execution. + modifier proxyCallIfNotAdmin() { + if (msg.sender == _getAdmin() || msg.sender == address(0)) { + _; + } else { + // This WILL halt the call frame on completion. + _doProxyCall(); + } + } + + /// @notice Sets the initial admin during contract deployment. Admin address is stored at the + /// EIP-1967 admin storage slot so that accidental storage collision with the + /// implementation is not possible. + /// @param _admin Address of the initial contract admin. Admin has the ability to access the + /// transparent proxy interface. + constructor(address _implementation, address _admin) { + _setImplementation(_implementation); + _setAdmin(_admin); + } + + receive() external payable { + // Proxy call by default. + _doProxyCall(); + } + + fallback() external payable { + // Proxy call by default. + _doProxyCall(); + } + + /// @notice Gets the owner of the proxy contract. + /// @return Owner address. + function admin() external virtual proxyCallIfNotAdmin returns (address) { + return _getAdmin(); + } + + /// @notice Changes the owner of the proxy contract. Only callable by the owner. + /// @param _admin New owner of the proxy contract. + function changeAdmin(address _admin) external virtual proxyCallIfNotAdmin { + _setAdmin(_admin); + } + + //// @notice Queries the implementation address. + /// @return Implementation address. + function implementation() external virtual proxyCallIfNotAdmin returns (address) { + return _getImplementation(); + } + + /// @notice Set the implementation contract address. The code at the given address will execute + /// when this contract is called. + /// @param _implementation Address of the implementation contract. + function upgradeTo(address _implementation) external virtual proxyCallIfNotAdmin { + _setImplementation(_implementation); + } + + /// @notice Set the implementation and call a function in a single transaction. Useful to ensure + /// atomic execution of initialization-based upgrades. + /// @param _implementation Address of the implementation contract. + /// @param _data Calldata to delegatecall the new implementation with. + function upgradeToAndCall(address _implementation, bytes calldata _data) + external + payable + proxyCallIfNotAdmin + returns (bytes memory) + { + _setImplementation(_implementation); + _implementation = _resolveImplementation(); + (bool success, bytes memory returndata) = _implementation.delegatecall(_data); + require(success, "Proxy: delegatecall to new implementation contract failed"); + return returndata; + } + + function _getImplementation() internal view returns (address) { + address impl; + bytes32 proxyImplementation = IMPLEMENTATION_SLOT; + assembly { + impl := sload(proxyImplementation) + } + return impl; + } + + function _setImplementation(address _implementation) internal { + bytes32 proxyImplementation = IMPLEMENTATION_SLOT; + assembly { + sstore(proxyImplementation, _implementation) + } + } + + function _getAdmin() internal view returns (address) { + address owner; + bytes32 proxyOwner = ADMIN_SLOT; + assembly { + owner := sload(proxyOwner) + } + return owner; + } + + function _setAdmin(address _admin) internal { + bytes32 proxyOwner = ADMIN_SLOT; + assembly { + sstore(proxyOwner, _admin) + } + } + + function _doProxyCall() internal { + address impl = _resolveImplementation(); + assembly { + calldatacopy(0x0, 0x0, calldatasize()) + let success := delegatecall(gas(), impl, 0x0, calldatasize(), 0x0, 0x0) + returndatacopy(0x0, 0x0, returndatasize()) + if iszero(success) { revert(0x0, returndatasize()) } + return(0x0, returndatasize()) + } + } + + function _resolveImplementation() internal view returns (address) { + address proxy = _getImplementation(); + bytes memory data = abi.encodeCall(IResolver.getProxyImplementation, (proxy)); + (bool success, bytes memory returndata) = _getAdmin().staticcall(data); + if (success && returndata.length == 0x20) { + return abi.decode(returndata, (address)); + } + return proxy; + } +} diff --git a/src/ResolvingProxyFactory.sol b/src/ResolvingProxyFactory.sol new file mode 100644 index 0000000..885b4ec --- /dev/null +++ b/src/ResolvingProxyFactory.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import {ResolvingProxy} from "./ResolvingProxy.sol"; + +/// @title ResolvingProxyFactory +/// @notice ResolvingProxyFactory is a factory contract that creates ResolvingProxy instances. +/// @dev The setupProxy / proxyAddress functions provide a smaller assembly-based ResolvingProxy +/// implementation that is more gas efficient to deploy and operate than the solidity +/// ResolvingProxy implementation. +library ResolvingProxyFactory { + function setupProxy(address proxy, address admin, bytes32 salt) internal returns (address instance) { + /// @solidity memory-safe-assembly + assembly { + let ptr := mload(0x40) + mstore(ptr, shl(0xC0, 0x600661011c565b73)) + mstore(add(ptr, 0x8), shl(0x60, proxy)) + mstore(add(ptr, 0x1c), shl(0xE8, 0x905573)) + mstore(add(ptr, 0x1f), shl(0x60, admin)) + mstore(add(ptr, 0x33), 0x905561012580603f5f395ff35f365f600860dd565b8054909180548033143315) + mstore(add(ptr, 0x53), 0x171560545760045f5f375f5160e01c8063f851a4401460a25780635c60da1b14) + mstore(add(ptr, 0x73), 0x609f5780638f2839701460af5780633659cfe61460ac57634f1ef2861460aa57) + mstore(add(ptr, 0x93), 0x5b63204e1c7a60e01b5f52826004525f5f60245f845afa3d5f5f3e3d60201416) + mstore(add(ptr, 0xb3), 0x805f510290158402015f875f89895f375f935af43d5f893d60205260205f523e) + mstore(add(ptr, 0xd3), 0x5f3d890191609d57fd5bf35b50505b505f5260205ff35b5f5b93915b50506020) + mstore(add(ptr, 0xf3), 0x60045f375f518091559160d957903333602060445f375f519560649550506040) + mstore(add(ptr, 0x113), 0x96506054565b5f5ff35b7f360894a13ba1a3210667c828492db98dca3e2076cc) + mstore(add(ptr, 0x133), 0x3735a920a3ca505d382bbc7fb53127684a568b3173ae13b9f8a6016e243e63b6) + mstore(add(ptr, 0x153), shl(0x90, 0xe8ee1178d6a717850b5d61039156)) + instance := create2(0, ptr, 0x161, salt) + } + require(instance != address(0), "Proxy: create2 failed"); + } + + function proxyAddress(address proxy, address admin, bytes32 salt) internal view returns (address predicted) { + address deployer = address(this); + /// @solidity memory-safe-assembly + assembly { + let ptr := mload(0x40) + mstore(ptr, shl(0xC0, 0x600661011c565b73)) + mstore(add(ptr, 0x8), shl(0x60, proxy)) + mstore(add(ptr, 0x1c), shl(0xE8, 0x905573)) + mstore(add(ptr, 0x1f), shl(0x60, admin)) + mstore(add(ptr, 0x33), 0x905561012580603f5f395ff35f365f600860dd565b8054909180548033143315) + mstore(add(ptr, 0x53), 0x171560545760045f5f375f5160e01c8063f851a4401460a25780635c60da1b14) + mstore(add(ptr, 0x73), 0x609f5780638f2839701460af5780633659cfe61460ac57634f1ef2861460aa57) + mstore(add(ptr, 0x93), 0x5b63204e1c7a60e01b5f52826004525f5f60245f845afa3d5f5f3e3d60201416) + mstore(add(ptr, 0xb3), 0x805f510290158402015f875f89895f375f935af43d5f893d60205260205f523e) + mstore(add(ptr, 0xd3), 0x5f3d890191609d57fd5bf35b50505b505f5260205ff35b5f5b93915b50506020) + mstore(add(ptr, 0xf3), 0x60045f375f518091559160d957903333602060445f375f519560649550506040) + mstore(add(ptr, 0x113), 0x96506054565b5f5ff35b7f360894a13ba1a3210667c828492db98dca3e2076cc) + mstore(add(ptr, 0x133), 0x3735a920a3ca505d382bbc7fb53127684a568b3173ae13b9f8a6016e243e63b6) + mstore(add(ptr, 0x153), shl(0x88, 0xe8ee1178d6a717850b5d61039156ff)) + mstore(add(ptr, 0x162), shl(0x60, deployer)) + mstore(add(ptr, 0x176), salt) + mstore(add(ptr, 0x196), keccak256(ptr, 0x161)) + predicted := keccak256(add(ptr, 0x161), 0x55) + } + } + + function setupExpensiveProxy(address proxy, address admin, bytes32 salt) internal returns (address instance) { + return address(new ResolvingProxy{salt: salt}(proxy, admin)); + } + + function expensiveProxyAddress(address proxy, address admin, bytes32 salt) + internal + view + returns (address predicted) + { + bytes memory bytecode = abi.encodePacked(type(ResolvingProxy).creationCode, abi.encode(proxy, admin)); + bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, keccak256(bytecode))); + return address(uint160(uint256(hash))); + } +} diff --git a/test/ResolvingProxyFactory.t.sol b/test/ResolvingProxyFactory.t.sol new file mode 100644 index 0000000..069ecf7 --- /dev/null +++ b/test/ResolvingProxyFactory.t.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import {Vm} from "forge-std/Vm.sol"; +import {Test, console} from "forge-std/Test.sol"; +import {ResolvingProxyFactory} from "../src/ResolvingProxyFactory.sol"; +import {ProxyAdmin} from "@eth-optimism-bedrock/src/universal/ProxyAdmin.sol"; +import {Proxy} from "@eth-optimism-bedrock/src/universal/Proxy.sol"; + +abstract contract Implementation { + string internal value; + + function set(string memory _value) external { + value = _value; + } +} + +contract Implementation1 is Implementation { + function get() external view returns (string memory) { + return string.concat("Hello ", value); + } +} + +contract Implementation2 is Implementation { + function get() external view returns (string memory) { + return string.concat("Hi ", value); + } +} + +contract ResolvingProxyFactoryTest is Test { + Implementation1 public implementation1; + Implementation2 public implementation2; + ProxyAdmin public admin; + Proxy public proxy; + address public resolvingProxy; + + function setUp() public { + implementation1 = new Implementation1(); + implementation2 = new Implementation2(); + admin = new ProxyAdmin(address(this)); + proxy = new Proxy(address(admin)); + admin.upgrade(payable(address(proxy)), address(implementation1)); + resolvingProxy = ResolvingProxyFactory.setupProxy(address(proxy), address(admin), 0x00); + Implementation1(resolvingProxy).set("world"); + } + + function test_setupProxy() public view { + string memory value = Implementation1(resolvingProxy).get(); + assertEq(value, "Hello world"); + } + + function test_implementation() public view { + address implementation = admin.getProxyImplementation(payable(address(resolvingProxy))); + assertEq(implementation, address(proxy)); + } + + function test_admin() public view { + address _admin = admin.getProxyAdmin(payable(address(resolvingProxy))); + assertEq(_admin, address(admin)); + } + + function test_upgradeTo() public { + admin.upgrade(payable(address(resolvingProxy)), address(implementation2)); + Implementation1(resolvingProxy).set("alice"); + string memory value = Implementation1(resolvingProxy).get(); + assertEq(value, "Hi alice"); + + admin.upgrade(payable(address(resolvingProxy)), address(proxy)); + value = Implementation1(resolvingProxy).get(); + assertEq(value, "Hello alice"); + } + + function test_upgradeToAndCall() public { + bytes memory data = abi.encodeCall(Implementation.set, ("alice")); + admin.upgradeAndCall(payable(address(resolvingProxy)), address(implementation2), data); + string memory value = Implementation1(resolvingProxy).get(); + assertEq(value, "Hi alice"); + } + + function test_changeAdmin() public { + ProxyAdmin newAdmin = new ProxyAdmin(address(this)); + admin.changeProxyAdmin(payable(address(resolvingProxy)), address(newAdmin)); + newAdmin.upgrade(payable(address(resolvingProxy)), address(implementation2)); + string memory value = Implementation1(resolvingProxy).get(); + assertEq(value, "Hi world"); + } + + function test_proxyAddress() public view { + address predicted = ResolvingProxyFactory.proxyAddress(address(proxy), address(admin), 0x00); + assertEq(predicted, resolvingProxy); + } +}