Add ERC1167 library (minimal proxy) (#2449)

Co-authored-by: Francisco Giordano <frangio.1@gmail.com>
This commit is contained in:
Hadrien Croubois
2021-01-19 21:54:47 +01:00
committed by GitHub
parent dd86c97e18
commit 9e49be41b6
8 changed files with 335 additions and 8 deletions

View File

@ -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
test/proxy/Clones.test.js Normal file
View File

@ -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 },
);
});
});
});

View File

@ -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);
});

View File

@ -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);
});