diff --git a/.changeset/green-dryers-raise.md b/.changeset/green-dryers-raise.md new file mode 100644 index 000000000..c79c362c6 --- /dev/null +++ b/.changeset/green-dryers-raise.md @@ -0,0 +1,5 @@ +--- +'@chugsplash/contracts': minor +--- + +Adds the ProxyAdmin, which owns the proxies for a project. diff --git a/packages/contracts/contracts/ChugSplashManager.sol b/packages/contracts/contracts/ChugSplashManager.sol index c78be0f41..484ed761f 100644 --- a/packages/contracts/contracts/ChugSplashManager.sol +++ b/packages/contracts/contracts/ChugSplashManager.sol @@ -13,14 +13,6 @@ import { MerkleTree } from "./libraries/MerkleTree.sol"; * @title ChugSplashManager */ contract ChugSplashManager is Owned { - /** - * @notice The storage slot that holds the address of an EIP-1967 implementation. To be used - * as the implementation key for standard proxies. - * bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1) - */ - bytes32 internal constant EIP1967_IMPLEMENTATION_KEY = - 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; - /** * @notice Enum representing possible ChugSplash action types. */ @@ -154,7 +146,7 @@ contract ChugSplashManager is Owned { * @notice Mapping of target names to proxy addresses. If a target is using the default * proxy, then its value in this mapping is the zero-address. */ - mapping(string => address) public proxies; + mapping(string => address payable) public proxies; /** * @notice Mapping of target names to proxy types. If a target is using the default proxy, @@ -166,7 +158,7 @@ contract ChugSplashManager is Owned { * @param _registry Address of the ChugSplashRegistry. * @param _name Name of the project this contract is managing. * @param _owner Initial owner of this contract. - * @param _proxyUpdater Address of the ProxyUpdater for this project. + * @param _proxyUpdater Address of the ProxyUpdater. */ constructor( ChugSplashRegistry _registry, @@ -177,7 +169,7 @@ contract ChugSplashManager is Owned { registry = _registry; proxyUpdater = _proxyUpdater; name = _name; - proxyAdmin = new ProxyAdmin{ salt: bytes32(0) }(); + proxyAdmin = new ProxyAdmin{ salt: bytes32(0) }(_registry, _proxyUpdater); } /** @@ -297,7 +289,7 @@ contract ChugSplashManager is Owned { // Get the proxy to use for this target. The proxy can either be the default proxy used by // ChugSplash or a non-standard proxy that has previously been set by the project owner. - address proxy; + address payable proxy; if (proxyType == bytes32(0)) { // Use a default proxy if this target has no proxy type assigned to it. @@ -363,7 +355,7 @@ contract ChugSplashManager is Owned { */ function setProxyToTarget( string memory _name, - address _proxy, + address payable _proxy, bytes32 _proxyType ) external onlyOwner { proxies[_name] = _proxy; @@ -381,11 +373,11 @@ contract ChugSplashManager is Owned { * * @return Address of the proxy for the given name. */ - function getProxyByName(string memory _name) public view returns (address) { + function getProxyByName(string memory _name) public view returns (address payable) { return ( payable( Create2.compute(address(this), keccak256(bytes(_name)), type(Proxy).creationCode) ) ); } -} \ No newline at end of file +} diff --git a/packages/contracts/contracts/ChugSplashRegistry.sol b/packages/contracts/contracts/ChugSplashRegistry.sol index 789ad868e..b66ec1e2f 100644 --- a/packages/contracts/contracts/ChugSplashRegistry.sol +++ b/packages/contracts/contracts/ChugSplashRegistry.sol @@ -128,8 +128,10 @@ contract ChugSplashRegistry { adapters[_proxyType] == address(0), "ChugSplashRegistry: proxy type has an existing adapter" ); + // TODO: We might want to add a check here that the adapter supports the correct interface + // (e.g. using ERC165Checker) to avoid incorrectly inputted adapter addresses. adapters[_proxyType] = _adapter; emit ProxyTypeAdded(_proxyType, _adapter); } -} \ No newline at end of file +} diff --git a/packages/contracts/contracts/IProxyAdapter.sol b/packages/contracts/contracts/IProxyAdapter.sol new file mode 100644 index 000000000..b9db32d9a --- /dev/null +++ b/packages/contracts/contracts/IProxyAdapter.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; + +interface IProxyAdapter { + function getProxyImplementation() external returns (address implementation); + + function upgradeProxyTo(address _proxy, address _implementation) external; + + function setProxyCode(address _proxy, bytes memory _code) external; + + function setProxyStorage( + address _proxy, + bytes32 _key, + bytes32 _value + ) external; +} diff --git a/packages/contracts/contracts/ProxyAdmin.sol b/packages/contracts/contracts/ProxyAdmin.sol index 04d6c796a..385ffa723 100644 --- a/packages/contracts/contracts/ProxyAdmin.sol +++ b/packages/contracts/contracts/ProxyAdmin.sol @@ -1,28 +1,130 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.9; +import { ChugSplashRegistry } from "./ChugSplashRegistry.sol"; +import { Proxy } from "@eth-optimism/contracts-bedrock/contracts/universal/Proxy.sol"; +import { ProxyUpdater } from "./ProxyUpdater.sol"; +import { IProxyAdapter } from "./IProxyAdapter.sol"; + +/** + * @title ProxyAdmin + * @notice The ProxyAdmin is a contract associated with each ChugSplashManager that owns the various + * proxies for a given project. The ProxyAdmin delegatecalls into various ProxyAdapter + * contracts that correspond to different proxy types. Using this pattern, the ProxyAdmin + * can universally handle all different proxy types as long as the ProxyAdmin is considered + * the owning address of each proxy. + */ contract ProxyAdmin { - function getProxyImplementation(address _proxy, bytes32 _proxyType) - public - returns (address implementation) - {} + /** + * @notice Address of the ChugSplashRegistry. + */ + ChugSplashRegistry public immutable registry; - function upgrade( - address _proxy, - bytes32 _proxyType, - address _implementation - ) public {} + /** + * @notice Address of the ProxyUpdater. + */ + address public immutable proxyUpdater; + + /** + * @param _registry Address of the ChugSplashRegistry. + * @param _proxyUpdater Address of the ProxyUpdater. + */ + constructor(ChugSplashRegistry _registry, address _proxyUpdater) { + registry = _registry; + proxyUpdater = _proxyUpdater; + } + /** + * @notice Sets new code for the proxy contract's implementation. + * + * @param _proxy Address of the proxy to upgrade. + * @param _proxyType The proxy's type. This is the zero-address for default proxies. + * @param _code Creation bytecode to be deployed. + */ function setProxyCode( - address _proxy, + address payable _proxy, bytes32 _proxyType, - bytes memory _data - ) public {} + bytes memory _code + ) public { + // Get the adapter that corresponds to this proxy type. + address adapter = registry.adapters(_proxyType); + require(adapter != address(0), "ProxyAdmin: proxy type has no adapter"); + + // Delegatecall the adapter to upgrade the proxy's implementation to be the + // ProxyUpdater, which has the `setCode` function. + _upgradeUsingAdapter(_proxy, adapter, proxyUpdater); + // Delegatecall the adapter, which in turn will call the proxy to trigger a `setCode` + // action. + (bool success, ) = adapter.delegatecall( + abi.encodeCall(IProxyAdapter.setProxyCode, (_proxy, _code)) + ); + require(success, "ProxyAdmin: delegatecall to set proxy code failed"); + } + + /** + * @notice Modifies a storage slot within the proxy contract. + * + * @param _proxy Address of the proxy to upgrade. + * @param _proxyType The proxy's type. This is the zero-address for default proxies. + * @param _key Storage key to modify. + * @param _value New value for the storage key. + */ function setProxyStorage( - address _proxy, + address payable _proxy, bytes32 _proxyType, bytes32 _key, bytes32 _value - ) public {} + ) public { + // Get the adapter that corresponds to this proxy type. + address adapter = registry.adapters(_proxyType); + require(adapter != address(0), "ProxyAdmin: proxy type has no adapter"); + + // Get the address of the current implementation for the proxy. The ProxyAdmin will set + // the proxy's implementation back to this address after setting it to be the + // ProxyUpdater and calling `setStorage`. + (bool success, bytes memory implementationBytes) = adapter.delegatecall( + abi.encodeCall(IProxyAdapter.getProxyImplementation, ()) + ); + require(success, "ProxyAdmin: delegatecall to get proxy implementation failed"); + + // Delegatecall the adapter to upgrade the proxy's implementation to be the + // ProxyUpdater, which has the `setStorage` function. + _upgradeUsingAdapter(_proxy, adapter, proxyUpdater); + + // Delegatecall the adapter, which in turn will call the proxy to trigger a `setStorage` + // action. + (bool setStorageSuccess, ) = adapter.delegatecall( + abi.encodeCall(IProxyAdapter.setProxyStorage, (_proxy, _key, _value)) + ); + require(setStorageSuccess, "ProxyAdmin: delegatecall to set proxy storage failed"); + + // Convert the implementation's type from bytes to address. + address implementation; + assembly { + implementation := mload(add(implementationBytes, 32)) + } + + // Delegatecall the adapter to set the proxy's implementation back to its original + // address. + _upgradeUsingAdapter(_proxy, adapter, implementation); + } + + /** + * @notice Delegatecalls an adapter to upgrade a proxy's implementation contract. + * + * @param _proxy Address of the proxy to upgrade. + * @param _adapter Address of the adapter to use for the proxy. + * @param _implementation Address to set as the proxy's new implementation contract. + */ + function _upgradeUsingAdapter( + address _proxy, + address _adapter, + address _implementation + ) internal { + (bool success, ) = _adapter.delegatecall( + abi.encodeCall(IProxyAdapter.upgradeProxyTo, (_proxy, _implementation)) + ); + require(success, "ProxyAdmin: delegatecall to upgrade proxy failed"); + } }