diff --git a/CHANGELOG.md b/CHANGELOG.md index d3d794b4026..85eeb71c580 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * `ERC20Permit`: added an implementation of the ERC20 permit extension for gasless token approvals. ([#2237](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2237)) * Presets: added token presets with preminted fixed supply `ERC20PresetFixedSupply` and `ERC777PresetFixedSupply`. ([#2399](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2399)) * `Address`: added `functionDelegateCall`, similar to the existing `functionCall`. ([#2333](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2333)) + * `Clones`: added a library for deploying EIP 1167 minimal proxies. ([#2449](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2449)) * `Context`: moved from `contracts/GSN` to `contracts/utils`. ([#2453](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2453)) * `PaymentSplitter`: replace usage of `.transfer()` with `Address.sendValue` for improved compatibility with smart wallets. ([#2455](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2455)) * `UpgradeableProxy`: bubble revert reasons from initialization calls. ([#2454](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2454)) diff --git a/contracts/mocks/ClonesMock.sol b/contracts/mocks/ClonesMock.sol new file mode 100644 index 00000000000..b6926c663bd --- /dev/null +++ b/contracts/mocks/ClonesMock.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.6.0 <0.8.0; + +import "../proxy/Clones.sol"; +import "../utils/Address.sol"; + +contract ClonesMock { + using Address for address; + using Clones for address; + + event NewInstance(address instance); + + function clone(address master, bytes calldata initdata) public payable { + _initAndEmit(master.clone(), initdata); + } + + function cloneDeterministic(address master, bytes32 salt, bytes calldata initdata) public payable { + _initAndEmit(master.cloneDeterministic(salt), initdata); + } + + function predictDeterministicAddress(address master, bytes32 salt) public view returns (address predicted) { + return master.predictDeterministicAddress(salt); + } + + function _initAndEmit(address instance, bytes memory initdata) private { + if (initdata.length > 0) { + instance.functionCallWithValue(initdata, msg.value); + } + emit NewInstance(instance); + } +} diff --git a/contracts/proxy/Clones.sol b/contracts/proxy/Clones.sol new file mode 100644 index 00000000000..9ad5c223f56 --- /dev/null +++ b/contracts/proxy/Clones.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.6.0 <0.8.0; + +/** + * @dev https://eips.ethereum.org/EIPS/eip-1167[EIP 1167] is a standard for + * deploying minimal proxy contracts, also known as "clones". + * + * > To simply and cheaply clone contract functionality in an immutable way, this standard specifies + * > a minimal bytecode implementation that delegates all calls to a known, fixed address. + * + * The library includes functions to deploy a proxy using either `create` (traditional deployment) or `create2` + * (salted deterministic deployment). It also includes functions to predict the addresses of clones deployed using the + * deterministic method. + */ +library Clones { + /** + * @dev Deploys and returns the address of a clone that mimics the behaviour of `master`. + * + * This function uses the create opcode, which should never revert. + */ + function clone(address master) internal returns (address instance) { + // solhint-disable-next-line no-inline-assembly + assembly { + let ptr := mload(0x40) + mstore(ptr, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000) + mstore(add(ptr, 0x14), shl(0x60, master)) + mstore(add(ptr, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000) + instance := create(0, ptr, 0x37) + } + require(instance != address(0), "ERC1167: create failed"); + } + + /** + * @dev Deploys and returns the address of a clone that mimics the behaviour of `master`. + * + * This function uses the create2 opcode and a `salt` to deterministically deploy + * the clone. Using the same `master` and `salt` multiple time will revert, since + * the clones cannot be deployed twice at the same address. + */ + function cloneDeterministic(address master, bytes32 salt) internal returns (address instance) { + // solhint-disable-next-line no-inline-assembly + assembly { + let ptr := mload(0x40) + mstore(ptr, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000) + mstore(add(ptr, 0x14), shl(0x60, master)) + mstore(add(ptr, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000) + instance := create2(0, ptr, 0x37, salt) + } + require(instance != address(0), "ERC1167: create2 failed"); + } + + /** + * @dev Computes the address of a clone deployed using {Clones-cloneDeterministic}. + */ + function predictDeterministicAddress(address master, bytes32 salt, address deployer) internal pure returns (address predicted) { + // solhint-disable-next-line no-inline-assembly + assembly { + let ptr := mload(0x40) + mstore(ptr, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000) + mstore(add(ptr, 0x14), shl(0x60, master)) + mstore(add(ptr, 0x28), 0x5af43d82803e903d91602b57fd5bf3ff00000000000000000000000000000000) + mstore(add(ptr, 0x38), shl(0x60, deployer)) + mstore(add(ptr, 0x4c), salt) + mstore(add(ptr, 0x6c), keccak256(ptr, 0x37)) + predicted := keccak256(add(ptr, 0x37), 0x55) + } + } + + /** + * @dev Computes the address of a clone deployed using {Clones-cloneDeterministic}. + */ + function predictDeterministicAddress(address master, bytes32 salt) internal view returns (address predicted) { + return predictDeterministicAddress(master, salt, address(this)); + } +} diff --git a/contracts/proxy/README.adoc b/contracts/proxy/README.adoc index 255faeb43cd..452ad91333f 100644 --- a/contracts/proxy/README.adoc +++ b/contracts/proxy/README.adoc @@ -3,13 +3,15 @@ [.readme-notice] NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/proxy -This is a low-level set of contracts implementing the proxy pattern for upgradeability. For an in-depth overview of this pattern check out the xref:upgrades-plugins::proxies.adoc[Proxy Upgrade Pattern] page. +This is a low-level set of contracts implementing different proxy patterns with and without upgradeability. For an in-depth overview of this pattern check out the xref:upgrades-plugins::proxies.adoc[Proxy Upgrade Pattern] page. The abstract {Proxy} contract implements the core delegation functionality. If the concrete proxies that we provide below are not suitable, we encourage building on top of this base contract since it contains an assembly block that may be hard to get right. Upgradeability is implemented in the {UpgradeableProxy} contract, although it provides only an internal upgrade interface. For an upgrade interface exposed externally to an admin, we provide {TransparentUpgradeableProxy}. Both of these contracts use the storage slots specified in https://eips.ethereum.org/EIPS/eip-1967[EIP1967] to avoid clashes with the storage of the implementation contract behind the proxy. -An alternative upgradeability mechanism is provided in <>. This pattern, popularized by Dharma, allows multiple proxies to be upgraded to a different implementation in a single transaction. In this pattern, the proxy contract doesn't hold the implementation address in storage like {UpgradeableProxy}, but the address of a {UpgradeableBeacon} contract, which is where the implementation address is actually stored and retrieved from. The `upgrade` operations that change the implementation contract address are then sent to the beacon instead of to the proxy contract, and all proxies that follow that beacon are automatically upgraded. +An alternative upgradeability mechanism is provided in <>. This pattern, popularized by Dharma, allows multiple proxies to be upgraded to a different implementation in a single transaction. In this pattern, the proxy contract doesn't hold the implementation address in storage like {UpgradeableProxy}, but the address of a {UpgradeableBeacon} contract, which is where the implementation address is actually stored and retrieved from. The `upgrade` operations that change the implementation contract address are then sent to the beacon instead of to the proxy contract, and all proxies that follow that beacon are automatically upgraded. + +The {Clones} library provides a way to deploy minimal non-upgradeable proxies for cheap. This can be useful for applications that require deploying many instances of the same contract (for example one per user, or one per task). These instances are designed to be both cheap to deploy, and cheap to call. The drawback being that they are not upgradeable. CAUTION: Using upgradeable proxies correctly and securely is a difficult task that requires deep knowledge of the proxy pattern, Solidity, and the EVM. Unless you want a lot of low level control, we recommend using the xref:upgrades-plugins::index.adoc[OpenZeppelin Upgrades Plugins] for Truffle and Buidler. @@ -21,7 +23,7 @@ CAUTION: Using upgradeable proxies correctly and securely is a difficult task th {{TransparentUpgradeableProxy}} -== UpgradeableBeacon +== Beacon {{BeaconProxy}} @@ -29,6 +31,10 @@ CAUTION: Using upgradeable proxies correctly and securely is a difficult task th {{UpgradeableBeacon}} +== Minimal Clones + +{{Clones}} + == Utilities {{Initializable}} diff --git a/test/proxy/Clones.behaviour.js b/test/proxy/Clones.behaviour.js new file mode 100644 index 00000000000..81c5ee35f94 --- /dev/null +++ b/test/proxy/Clones.behaviour.js @@ -0,0 +1,150 @@ +const { expectRevert } = require('@openzeppelin/test-helpers'); + +const { expect } = require('chai'); + +const DummyImplementation = artifacts.require('DummyImplementation'); + +module.exports = function shouldBehaveLikeClone (createClone) { + before('deploy implementation', async function () { + this.implementation = web3.utils.toChecksumAddress((await DummyImplementation.new()).address); + }); + + const assertProxyInitialization = function ({ value, balance }) { + it('initializes the proxy', async function () { + const dummy = new DummyImplementation(this.proxy); + expect(await dummy.value()).to.be.bignumber.equal(value.toString()); + }); + + it('has expected balance', async function () { + expect(await web3.eth.getBalance(this.proxy)).to.be.bignumber.equal(balance.toString()); + }); + }; + + describe('initialization without parameters', function () { + describe('non payable', function () { + const expectedInitializedValue = 10; + const initializeData = new DummyImplementation('').contract.methods['initializeNonPayable()']().encodeABI(); + + describe('when not sending balance', function () { + beforeEach('creating proxy', async function () { + this.proxy = ( + await createClone(this.implementation, initializeData) + ).address; + }); + + assertProxyInitialization({ + value: expectedInitializedValue, + balance: 0, + }); + }); + + describe('when sending some balance', function () { + const value = 10e5; + + it('reverts', async function () { + await expectRevert.unspecified( + createClone(this.implementation, initializeData, { value }), + ); + }); + }); + }); + + describe('payable', function () { + const expectedInitializedValue = 100; + const initializeData = new DummyImplementation('').contract.methods['initializePayable()']().encodeABI(); + + describe('when not sending balance', function () { + beforeEach('creating proxy', async function () { + this.proxy = ( + await createClone(this.implementation, initializeData) + ).address; + }); + + assertProxyInitialization({ + value: expectedInitializedValue, + balance: 0, + }); + }); + + describe('when sending some balance', function () { + const value = 10e5; + + beforeEach('creating proxy', async function () { + this.proxy = ( + await createClone(this.implementation, initializeData, { value }) + ).address; + }); + + assertProxyInitialization({ + value: expectedInitializedValue, + balance: value, + }); + }); + }); + }); + + describe('initialization with parameters', function () { + describe('non payable', function () { + const expectedInitializedValue = 10; + const initializeData = new DummyImplementation('').contract + .methods.initializeNonPayableWithValue(expectedInitializedValue).encodeABI(); + + describe('when not sending balance', function () { + beforeEach('creating proxy', async function () { + this.proxy = ( + await createClone(this.implementation, initializeData) + ).address; + }); + + assertProxyInitialization({ + value: expectedInitializedValue, + balance: 0, + }); + }); + + describe('when sending some balance', function () { + const value = 10e5; + + it('reverts', async function () { + await expectRevert.unspecified( + createClone(this.implementation, initializeData, { value }), + ); + }); + }); + }); + + describe('payable', function () { + const expectedInitializedValue = 42; + const initializeData = new DummyImplementation('').contract + .methods.initializePayableWithValue(expectedInitializedValue).encodeABI(); + + describe('when not sending balance', function () { + beforeEach('creating proxy', async function () { + this.proxy = ( + await createClone(this.implementation, initializeData) + ).address; + }); + + assertProxyInitialization({ + value: expectedInitializedValue, + balance: 0, + }); + }); + + describe('when sending some balance', function () { + const value = 10e5; + + beforeEach('creating proxy', async function () { + this.proxy = ( + await createClone(this.implementation, initializeData, { value }) + ).address; + }); + + assertProxyInitialization({ + value: expectedInitializedValue, + balance: value, + }); + }); + }); + }); +}; diff --git a/test/proxy/Clones.test.js b/test/proxy/Clones.test.js new file mode 100644 index 00000000000..0f6a5de9762 --- /dev/null +++ b/test/proxy/Clones.test.js @@ -0,0 +1,54 @@ +const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); + +const shouldBehaveLikeClone = require('./Clones.behaviour'); + +const ClonesMock = artifacts.require('ClonesMock'); + +contract('Clones', function (accounts) { + describe('clone', function () { + shouldBehaveLikeClone(async (implementation, initData, opts = {}) => { + const factory = await ClonesMock.new(); + const receipt = await factory.clone(implementation, initData, { value: opts.value }); + const address = receipt.logs.find(({ event }) => event === 'NewInstance').args.instance; + return { address }; + }); + }); + + describe('cloneDeterministic', function () { + shouldBehaveLikeClone(async (implementation, initData, opts = {}) => { + const salt = web3.utils.randomHex(32); + const factory = await ClonesMock.new(); + const receipt = await factory.cloneDeterministic(implementation, salt, initData, { value: opts.value }); + const address = receipt.logs.find(({ event }) => event === 'NewInstance').args.instance; + return { address }; + }); + + it('address already used', async function () { + const implementation = web3.utils.randomHex(20); + const salt = web3.utils.randomHex(32); + const factory = await ClonesMock.new(); + // deploy once + expectEvent( + await factory.cloneDeterministic(implementation, salt, '0x'), + 'NewInstance', + ); + // deploy twice + await expectRevert( + factory.cloneDeterministic(implementation, salt, '0x'), + 'ERC1167: create2 failed', + ); + }); + + it('address prediction', async function () { + const implementation = web3.utils.randomHex(20); + const salt = web3.utils.randomHex(32); + const factory = await ClonesMock.new(); + const predicted = await factory.predictDeterministicAddress(implementation, salt); + expectEvent( + await factory.cloneDeterministic(implementation, salt, '0x'), + 'NewInstance', + { instance: predicted }, + ); + }); + }); +}); diff --git a/test/proxy/TransparentUpgradeableProxy.behaviour.js b/test/proxy/TransparentUpgradeableProxy.behaviour.js index fb8dd74efb9..800c0918565 100644 --- a/test/proxy/TransparentUpgradeableProxy.behaviour.js +++ b/test/proxy/TransparentUpgradeableProxy.behaviour.js @@ -1,6 +1,6 @@ const { BN, expectRevert, expectEvent, constants } = require('@openzeppelin/test-helpers'); const { ZERO_ADDRESS } = constants; -const { toChecksumAddress, keccak256 } = require('ethereumjs-util'); +const ethereumjsUtil = require('ethereumjs-util'); const { expect } = require('chai'); @@ -19,6 +19,10 @@ const ClashingImplementation = artifacts.require('ClashingImplementation'); const IMPLEMENTATION_LABEL = 'eip1967.proxy.implementation'; const ADMIN_LABEL = 'eip1967.proxy.admin'; +function toChecksumAddress (address) { + return ethereumjsUtil.toChecksumAddress('0x' + address.replace(/^0x/, '').padStart(40, '0')); +} + module.exports = function shouldBehaveLikeTransparentUpgradeableProxy (createProxy, accounts) { const [proxyAdminAddress, proxyAdminOwner, anotherAccount] = accounts; @@ -308,13 +312,13 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy (createPro describe('storage', function () { it('should store the implementation address in specified location', async function () { - const slot = '0x' + new BN(keccak256(Buffer.from(IMPLEMENTATION_LABEL))).subn(1).toString(16); + const slot = '0x' + new BN(ethereumjsUtil.keccak256(Buffer.from(IMPLEMENTATION_LABEL))).subn(1).toString(16); const implementation = toChecksumAddress(await web3.eth.getStorageAt(this.proxyAddress, slot)); expect(implementation).to.be.equal(this.implementationV0); }); it('should store the admin proxy in specified location', async function () { - const slot = '0x' + new BN(keccak256(Buffer.from(ADMIN_LABEL))).subn(1).toString(16); + const slot = '0x' + new BN(ethereumjsUtil.keccak256(Buffer.from(ADMIN_LABEL))).subn(1).toString(16); const proxyAdmin = toChecksumAddress(await web3.eth.getStorageAt(this.proxyAddress, slot)); expect(proxyAdmin).to.be.equal(proxyAdminAddress); }); diff --git a/test/proxy/UpgradeableProxy.behaviour.js b/test/proxy/UpgradeableProxy.behaviour.js index 52e679c8fc8..a0c3edb7a75 100644 --- a/test/proxy/UpgradeableProxy.behaviour.js +++ b/test/proxy/UpgradeableProxy.behaviour.js @@ -1,5 +1,5 @@ const { BN, expectRevert } = require('@openzeppelin/test-helpers'); -const { toChecksumAddress, keccak256 } = require('ethereumjs-util'); +const ethereumjsUtil = require('ethereumjs-util'); const { expect } = require('chai'); @@ -7,6 +7,10 @@ const DummyImplementation = artifacts.require('DummyImplementation'); const IMPLEMENTATION_LABEL = 'eip1967.proxy.implementation'; +function toChecksumAddress (address) { + return ethereumjsUtil.toChecksumAddress('0x' + address.replace(/^0x/, '').padStart(40, '0')); +} + module.exports = function shouldBehaveLikeUpgradeableProxy (createProxy, proxyAdminAddress, proxyCreator) { it('cannot be initialized with a non-contract address', async function () { const nonContractAddress = proxyCreator; @@ -24,7 +28,7 @@ module.exports = function shouldBehaveLikeUpgradeableProxy (createProxy, proxyAd const assertProxyInitialization = function ({ value, balance }) { it('sets the implementation address', async function () { - const slot = '0x' + new BN(keccak256(Buffer.from(IMPLEMENTATION_LABEL))).subn(1).toString(16); + const slot = '0x' + new BN(ethereumjsUtil.keccak256(Buffer.from(IMPLEMENTATION_LABEL))).subn(1).toString(16); const implementation = toChecksumAddress(await web3.eth.getStorageAt(this.proxy, slot)); expect(implementation).to.be.equal(this.implementation); });