Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ERC1167 library (minimal proxy) #2449

Merged
merged 18 commits into from
Jan 19, 2021
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
}
74 changes: 74 additions & 0 deletions contracts/proxy/Clones.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// 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 specifie
* > a minimal bytecode implementation that delegates all calls to a known, fixed address.
*
* This contract provide tooling to deploy proxies following the EIP 1167
* proposed bytecode. This is possible using both create and create2.
*/
library Clones {
/**
* @dev Deploys and return the address of a clone that mimics the behaviour of `master`.
*
* This function uses the create opcode, which should never revert.
frangio marked this conversation as resolved.
Show resolved Hide resolved
*/
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)
}
}

/**
* @dev Deploys and return 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));
}
}
10 changes: 10 additions & 0 deletions contracts/proxy/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ CAUTION: Using upgradeable proxies correctly and securely is a difficult task th

{{UpgradeableBeacon}}

== Clones (ERC1167)

The clone 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. If upgradeability is necessary, it is possible to use this library to clone an updradeable proxy logic.

The clone library includes functions to deploy proxy using either `create` (traditional deployment) or `create2` (salted deterministic deployment). It also includes tools to predict the addresses of clones deployed using the deterministic method.

{{Clones}}

== Utilities

{{Initializable}}
Expand Down
158 changes: 158 additions & 0 deletions test/proxy/Clones.behaviour.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
const { expectRevert } = require('@openzeppelin/test-helpers');

const { expect } = require('chai');

const DummyImplementation = artifacts.require('DummyImplementation');

module.exports = function shouldBehaveLikeClone (createClone) {
// it('cannot be initialized with a non-contract address', async function () {
// const nonContractAddress = proxyCreator;
// const initializeData = Buffer.from('');
// await expectRevert.unspecified(
// createClone(nonContractAddress),
// );
// });

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,
});
});
});
});
};
57 changes: 57 additions & 0 deletions test/proxy/Clones.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers');

const shouldBehaveLikeClone = require('./Clones.behaviour');

const ClonesMock = artifacts.require('ClonesMock');

contract('Clones', function (accounts) {
beforeEach('deploying', async function () {
});

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