-
Notifications
You must be signed in to change notification settings - Fork 11.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add ERC1167 library (minimal proxy) (#2449)
Co-authored-by: Francisco Giordano <frangio.1@gmail.com>
- Loading branch information
Showing
8 changed files
with
335 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}); | ||
}); | ||
}); | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }, | ||
); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.