diff --git a/contracts/test/utils/proxy/proxy-factory.ts b/contracts/test/utils/proxy/proxy-factory.ts new file mode 100644 index 00000000..0a463612 --- /dev/null +++ b/contracts/test/utils/proxy/proxy-factory.ts @@ -0,0 +1,182 @@ +import { + ProxyFactory, + ProxyFactory__factory, + TestGovernanceERC20, + TestGovernanceERC20__factory, +} from '../../../typechain'; +import {ProxyCreatedEvent} from '../../../typechain/src/utils/ProxyFactory'; +import {findEvent} from '../../../utils/helpers'; +import { + ERC1967_IMPLEMENTATION_SLOT, + OZ_INITIALIZED_SLOT_POSITION, + readStorage, +} from '../../../utils/storage'; +import {SignerWithAddress} from '@nomiclabs/hardhat-ethers/signers'; +import {expect} from 'chai'; +import {ethers} from 'hardhat'; + +const MOCK_ADDRESS = `0x${'0123456789'.repeat(4)}`; +const DEFAULT_INITIALIZATION: [ + string, + string, + string, + {receivers: string[]; amounts: number[]} +] = [ + MOCK_ADDRESS, + 'GOV', + 'GOV', + { + receivers: [], + amounts: [], + }, +]; + +describe.only('ProxyFactory', function () { + let deployer: SignerWithAddress; + let proxyFactory: ProxyFactory; + let logic: TestGovernanceERC20; + + before(async () => { + deployer = (await ethers.getSigners())[0]; + + logic = await new TestGovernanceERC20__factory(deployer).deploy( + ...DEFAULT_INITIALIZATION + ); + proxyFactory = await new ProxyFactory__factory(deployer).deploy( + logic.address + ); + }); + + describe('initialization', async () => { + it('points to the right logic contract', async () => { + expect(await proxyFactory.LOGIC()).to.equal(logic.address); + }); + }); + + describe('deployUUPSProxy', async () => { + it('deploys the proxy unitialized if no data is provided', async () => { + const initData: any = []; + + const tx = await proxyFactory.deployUUPSProxy(initData); + const event = await findEvent(tx, 'ProxyCreated'); + if (!event) { + throw new Error('Failed to get the event'); + } + + const tokenProxy = TestGovernanceERC20__factory.connect( + event.args.proxy, + deployer + ); + + // Check that the proxy points to the right logic contract. + expect( + await readStorage(tokenProxy.address, ERC1967_IMPLEMENTATION_SLOT, [ + 'address', + ]) + ).to.equal(logic.address); + + // Check that the proxy is not initialized. + expect( + await readStorage(tokenProxy.address, OZ_INITIALIZED_SLOT_POSITION, [ + 'uint8', + ]) + ).to.equal(0); + + // Check that the DAO address is not set. + expect(await tokenProxy.dao()).to.equal(ethers.constants.AddressZero); + }); + + it('deploys the proxy initialized if data is provided', async () => { + const initData = + TestGovernanceERC20__factory.createInterface().encodeFunctionData( + 'initialize', + DEFAULT_INITIALIZATION + ); + + const tx = await proxyFactory.deployUUPSProxy(initData); + const event = await findEvent(tx, 'ProxyCreated'); + if (!event) { + throw new Error('Failed to get the event'); + } + + const tokenProxy = TestGovernanceERC20__factory.connect( + event.args.proxy, + deployer + ); + + // Check that the proxy points to the right logic contract. + expect( + await readStorage(tokenProxy.address, ERC1967_IMPLEMENTATION_SLOT, [ + 'address', + ]) + ).to.equal(logic.address); + + // Check that the proxy is initialized. + expect( + await readStorage(tokenProxy.address, OZ_INITIALIZED_SLOT_POSITION, [ + 'uint8', + ]) + ).to.equal(1); + + // Check that the DAO address is set. + expect(await tokenProxy.dao()).to.equal(MOCK_ADDRESS); + }); + }); + + describe('deployMinimalProxy', async () => { + it('deploys the proxy unitialized if no data is provided', async () => { + const initData: any = []; + + const tx = await proxyFactory.deployMinimalProxy(initData); + + const event = await findEvent(tx, 'ProxyCreated'); + if (!event) { + throw new Error('Failed to get the event'); + } + + const tokenProxy = TestGovernanceERC20__factory.connect( + event.args.proxy, + deployer + ); + + // Check that the proxy is not initialized. + expect( + await readStorage(tokenProxy.address, OZ_INITIALIZED_SLOT_POSITION, [ + 'uint8', + ]) + ).to.equal(0); + + // Check that the DAO address is not set. + expect(await tokenProxy.dao()).to.equal(ethers.constants.AddressZero); + }); + + it('deploys the proxy initialized if data is provided', async () => { + const initData = + TestGovernanceERC20__factory.createInterface().encodeFunctionData( + 'initialize', + DEFAULT_INITIALIZATION + ); + + const tx = await proxyFactory.deployMinimalProxy(initData); + const event = await findEvent(tx, 'ProxyCreated'); + if (!event) { + throw new Error('Failed to get the event'); + } + + const tokenProxy = TestGovernanceERC20__factory.connect( + event.args.proxy, + deployer + ); + + // Check that the proxy is initialized. + expect( + await readStorage(tokenProxy.address, OZ_INITIALIZED_SLOT_POSITION, [ + 'uint8', + ]) + ).to.equal(1); + + // Check that the DAO address is set. + expect(await tokenProxy.dao()).to.equal(MOCK_ADDRESS); + }); + }); +}); diff --git a/contracts/utils/storage.ts b/contracts/utils/storage.ts new file mode 100644 index 00000000..c82313ea --- /dev/null +++ b/contracts/utils/storage.ts @@ -0,0 +1,18 @@ +import {defaultAbiCoder} from 'ethers/lib/utils'; +import {ethers} from 'hardhat'; + +// See https://eips.ethereum.org/EIPS/eip-1967 +export const ERC1967_IMPLEMENTATION_SLOT = + '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc'; // bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1) + +export const OZ_INITIALIZED_SLOT_POSITION = 0; + +export async function readStorage( + contractAddress: string, + location: number | string, + types: string[] +): Promise { + return await ethers.provider + .getStorageAt(contractAddress, location) + .then(encoded => defaultAbiCoder.decode(types, encoded)[0]); +}