Skip to content

Commit

Permalink
Add ERC1167 library (minimal proxy) (#2449)
Browse files Browse the repository at this point in the history
Co-authored-by: Francisco Giordano <frangio.1@gmail.com>
  • Loading branch information
Amxx and frangio authored Jan 19, 2021
1 parent dd86c97 commit 9e49be4
Show file tree
Hide file tree
Showing 8 changed files with 335 additions and 8 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
32 changes: 32 additions & 0 deletions contracts/mocks/ClonesMock.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
76 changes: 76 additions & 0 deletions contracts/proxy/Clones.sol
Original file line number Diff line number Diff line change
@@ -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));
}
}
12 changes: 9 additions & 3 deletions contracts/proxy/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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 <<UpgradeableBeacon>>. 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 <<Beacon>>. 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.

Expand All @@ -21,14 +23,18 @@ CAUTION: Using upgradeable proxies correctly and securely is a difficult task th

{{TransparentUpgradeableProxy}}

== UpgradeableBeacon
== Beacon

{{BeaconProxy}}

{{IBeacon}}

{{UpgradeableBeacon}}

== Minimal Clones

{{Clones}}

== Utilities

{{Initializable}}
Expand Down
150 changes: 150 additions & 0 deletions test/proxy/Clones.behaviour.js
Original file line number Diff line number Diff line change
@@ -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,
});
});
});
});
};
54 changes: 54 additions & 0 deletions test/proxy/Clones.test.js
Original file line number Diff line number Diff line change
@@ -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 },
);
});
});
});
10 changes: 7 additions & 3 deletions test/proxy/TransparentUpgradeableProxy.behaviour.js
Original file line number Diff line number Diff line change
@@ -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');

Expand All @@ -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;

Expand Down Expand Up @@ -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);
});
Expand Down
Loading

0 comments on commit 9e49be4

Please sign in to comment.