diff --git a/src/contracts/immutable-beacon-proxy/ImmutableBeaconProxy.sol b/src/contracts/immutable-beacon-proxy/ImmutableBeaconProxy.sol new file mode 100644 index 0000000..0e701ec --- /dev/null +++ b/src/contracts/immutable-beacon-proxy/ImmutableBeaconProxy.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IBeacon} from './interfaces/IBeacon.sol'; +import {Proxy} from '../oz-common/Proxy.sol'; +import {Address} from '../oz-common/Address.sol'; + +/** + * @dev This contract implements a proxy that gets the implementation address for each call from an {UpgradeableBeacon}. + * The beacon address is immutable. The purpose of it, is to be able to access this proxy via delegatecall + + * !!! IMPORTANT CONSIDERATION !!! + * We expect that the implementation will not have any storage associated, + * because it when accessed via delegatecall, will not work as expected creating dangerous side-effects. Preferable, the implementation should be declared as a library + */ +contract ImmutableBeaconProxy is Proxy { + address internal immutable _beacon; + + event ImmutableBeaconSet(address indexed beacon); + + constructor(address beacon) { + require(Address.isContract(beacon), 'INVALID_BEACON'); + require(Address.isContract(IBeacon(beacon).implementation()), 'INVALID_IMPLEMENTATION'); + + // there is no initialization call, because we expect that implementation should have no storage + _beacon = beacon; + emit ImmutableBeaconSet(beacon); + } + + /** + * @dev Returns the current implementation address of the associated beacon. + */ + function _implementation() internal view virtual override returns (address) { + return IBeacon(_beacon).implementation(); + } +} diff --git a/src/contracts/immutable-beacon-proxy/UpgradeableBeacon.sol b/src/contracts/immutable-beacon-proxy/UpgradeableBeacon.sol new file mode 100644 index 0000000..7766f01 --- /dev/null +++ b/src/contracts/immutable-beacon-proxy/UpgradeableBeacon.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (proxy/beacon/UpgradeableBeacon.sol) + +pragma solidity ^0.8.0; + +import {IBeacon} from './interfaces/IBeacon.sol'; +import {Ownable} from '../oz-common/Ownable.sol'; +import {Address} from '../oz-common/Address.sol'; + +/** + * @dev This contract is used in conjunction with one or more instances of {BeaconProxy} to determine their + * implementation contract, which is where they will delegate all function calls. + * + * An owner is able to change the implementation the beacon points to, thus upgrading the proxies that use this beacon. + */ +contract UpgradeableBeacon is IBeacon, Ownable { + address private _implementation; + + /** + * @dev Emitted when the implementation returned by the beacon is changed. + */ + event Upgraded(address indexed implementation); + + /** + * @dev Sets the address of the initial implementation, and the deployer account as the owner who can upgrade the + * beacon. + */ + constructor(address implementation_) { + _setImplementation(implementation_); + } + + /** + * @dev Returns the current implementation address. + */ + function implementation() public view virtual override returns (address) { + return _implementation; + } + + /** + * @dev Upgrades the beacon to a new implementation. + * + * Emits an {Upgraded} event. + * + * Requirements: + * + * - msg.sender must be the owner of the contract. + * - `newImplementation` must be a contract. + */ + function upgradeTo(address newImplementation) public virtual onlyOwner { + _setImplementation(newImplementation); + emit Upgraded(newImplementation); + } + + /** + * @dev Sets the implementation contract address for this beacon + * + * Requirements: + * + * - `newImplementation` must be a contract. + */ + function _setImplementation(address newImplementation) private { + require( + Address.isContract(newImplementation), + 'UpgradeableBeacon: implementation is not a contract' + ); + _implementation = newImplementation; + } +} diff --git a/src/contracts/immutable-beacon-proxy/interfaces/IBeacon.sol b/src/contracts/immutable-beacon-proxy/interfaces/IBeacon.sol new file mode 100644 index 0000000..b29ed70 --- /dev/null +++ b/src/contracts/immutable-beacon-proxy/interfaces/IBeacon.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (proxy/beacon/IBeacon.sol) + +pragma solidity ^0.8.0; + +/** + * @dev This is the interface that {BeaconProxy} expects of its beacon. + */ +interface IBeacon { + /** + * @dev Must return an address that can be used as a delegate call target. + * + * {BeaconProxy} will check that this address is a contract. + */ + function implementation() external view returns (address); +} diff --git a/src/contracts/transparent-proxy/Proxy.sol b/src/contracts/oz-common/Proxy.sol similarity index 100% rename from src/contracts/transparent-proxy/Proxy.sol rename to src/contracts/oz-common/Proxy.sol diff --git a/src/contracts/transparent-proxy/ERC1967Proxy.sol b/src/contracts/transparent-proxy/ERC1967Proxy.sol index 409395c..c7f74c5 100644 --- a/src/contracts/transparent-proxy/ERC1967Proxy.sol +++ b/src/contracts/transparent-proxy/ERC1967Proxy.sol @@ -9,7 +9,7 @@ pragma solidity ^0.8.0; -import './Proxy.sol'; +import '../oz-common/Proxy.sol'; import './ERC1967Upgrade.sol'; /** diff --git a/test/ImmutableBeaconProxy.t.sol b/test/ImmutableBeaconProxy.t.sol new file mode 100644 index 0000000..9af6726 --- /dev/null +++ b/test/ImmutableBeaconProxy.t.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; +import 'forge-std/Test.sol'; +import {ImmutableBeaconProxy, IBeacon} from '../src/contracts/immutable-beacon-proxy/ImmutableBeaconProxy.sol'; + +contract ImplementationMock {} + +contract BeaconMock is IBeacon { + address public implementation; + + constructor(address newImplementation) { + implementation = newImplementation; + } +} + +contract ImmutableBeaconProxyMock is ImmutableBeaconProxy { + constructor(address beacon) ImmutableBeaconProxy(beacon) {} + + function implementation() public view returns (address) { + return _implementation(); + } +} + +contract ImmutableBeaconProxyTest is Test { + event ImmutableBeaconSet(address indexed beacon); + + function testResolvesImplementationCorrectly() public { + address implementation = address(new ImplementationMock()); + address beacon = address(new BeaconMock(implementation)); + + vm.expectEmit(true, false, false, true); + emit ImmutableBeaconSet(beacon); + assertEq(implementation, (new ImmutableBeaconProxyMock(beacon)).implementation()); + } + + function testBeaconNotAContract() public { + vm.expectRevert(bytes('INVALID_BEACON')); + new ImmutableBeaconProxy(address(1)); + } + + function testImplementationNotAContract() public { + address beacon = address(new BeaconMock(address(1))); + + vm.expectRevert(bytes('INVALID_IMPLEMENTATION')); + new ImmutableBeaconProxy(beacon); + } +}