diff --git a/.changeset/blue-numbers-hammer.md b/.changeset/blue-numbers-hammer.md new file mode 100644 index 000000000..ff83f938f --- /dev/null +++ b/.changeset/blue-numbers-hammer.md @@ -0,0 +1,6 @@ +--- +"@ethereum-waffle/hardhat": patch +"@ethereum-waffle/mock-contract": patch +--- + +Add mock contract deployment at a specified address diff --git a/docs/source/mock-contract.rst b/docs/source/mock-contract.rst index ab477a722..ee16adabb 100644 --- a/docs/source/mock-contract.rst +++ b/docs/source/mock-contract.rst @@ -16,6 +16,15 @@ Create an instance of a mock contract providing the :code:`ABI` of the smart con const mockContract = await deployMockContract(wallet, contractAbi); +You can also choose the deployment address of the mock contract with the options argument: + +.. code-block:: ts + + const mockContract = await deployMockContract(wallet, contractAbi, { + address: deploymentAddress, + overrride: false // optional, specifies if the contract should be overwritten + }) + The mock contract can now be integrated into other contracts by using the :code:`address` attribute. Return values for mocked functions can be set using: diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 45dc32f00..8c9955e95 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -358,6 +358,7 @@ importers: waffle-hardhat: specifiers: '@ethereum-waffle/chai': workspace:* + '@ethereum-waffle/mock-contract': workspace:^* '@ethereum-waffle/provider': workspace:* '@nomiclabs/hardhat-ethers': 2.1.0 '@types/node': ^17.0.41 @@ -369,6 +370,7 @@ importers: mocha: ^8.2.1 devDependencies: '@ethereum-waffle/chai': link:../waffle-chai + '@ethereum-waffle/mock-contract': link:../waffle-mock-contract '@ethereum-waffle/provider': link:../waffle-provider '@nomiclabs/hardhat-ethers': 2.1.0_mdh44cjfarfq76b44mvd54x3wq '@types/node': 17.0.41 @@ -385,6 +387,7 @@ importers: '@ethereum-waffle/compiler': workspace:* '@ethereum-waffle/provider': workspace:* '@ethersproject/abi': ^5.6.1 + '@ethersproject/providers': 5.6.2 eslint: ^7.14.0 ethers: 5.6.2 mocha: ^8.2.1 @@ -398,6 +401,7 @@ importers: '@ethereum-waffle/compiler': link:../waffle-compiler '@ethereum-waffle/provider': link:../waffle-provider '@ethersproject/abi': 5.6.1 + '@ethersproject/providers': 5.6.2 eslint: 7.18.0 ethers: 5.6.2 mocha: 8.2.1 diff --git a/waffle-hardhat/package.json b/waffle-hardhat/package.json index 8ea250150..409060066 100644 --- a/waffle-hardhat/package.json +++ b/waffle-hardhat/package.json @@ -20,14 +20,15 @@ }, "devDependencies": { "@ethereum-waffle/chai": "workspace:*", + "@ethereum-waffle/mock-contract": "workspace:^*", "@ethereum-waffle/provider": "workspace:*", "@nomiclabs/hardhat-ethers": "2.1.0", - "hardhat-waffle-dev": "2.0.3-dev.c5b5c29", "@types/node": "^17.0.41", "eslint": "^7.14.0", "ethereum-waffle": "workspace:*", "ethers": "5.6.2", "hardhat": "2.10.1", + "hardhat-waffle-dev": "2.0.3-dev.c5b5c29", "mocha": "^8.2.1" } } diff --git a/waffle-hardhat/test/mockContract.test.ts b/waffle-hardhat/test/mockContract.test.ts new file mode 100644 index 000000000..884b22c77 --- /dev/null +++ b/waffle-hardhat/test/mockContract.test.ts @@ -0,0 +1,15 @@ +import {waffle} from 'hardhat'; +import {MockProvider} from 'ethereum-waffle'; +import {mockContractDirectTest} from '@ethereum-waffle/mock-contract/test/directTest'; +import {mockContractProxiedTest} from '@ethereum-waffle/mock-contract/test/proxiedTest'; + +describe('INTEGRATION: Mock Contract', () => { + const provider = waffle.provider as MockProvider; + + before(async () => { + await provider.send('hardhat_reset', []); + }); + + mockContractDirectTest(provider); + mockContractProxiedTest(provider); +}); diff --git a/waffle-mock-contract/package.json b/waffle-mock-contract/package.json index 941992038..d6f1f8830 100644 --- a/waffle-mock-contract/package.json +++ b/waffle-mock-contract/package.json @@ -32,32 +32,32 @@ "module": "dist/esm/src/index.ts", "types": "dist/esm/src/index.d.ts", "scripts": { - "test": "export NODE_ENV=test && yarn test:build && mocha", + "test": "export NODE_ENV=test && mocha", "lint": "eslint '{src,test}/**/*.ts'", "lint:fix": "eslint --fix '{src,test}/**/*.ts'", - "build": "rimraf ./dist && yarn build:sol && yarn build:esm && yarn build:cjs", + "build": "rimraf ./dist && yarn build:sol && yarn build:esm && yarn build:cjs && ts-node ./test/helpers/buildTestContracts.ts", "build:sol": "ts-node compile.ts", "build:esm": "tsc -p tsconfig.build.json --outDir dist/esm --module ES6", "build:cjs": "tsc -p tsconfig.build.json --outDir dist/cjs", - "test:build": "ts-node ./test/helpers/buildTestContracts.ts", "clean": "rimraf ./dist ./test/example/build" }, "engines": { "node": ">=10.0" }, "devDependencies": { - "ethers": "5.6.2", - "@ethersproject/abi": "^5.6.1", "@ethereum-waffle/chai": "workspace:*", "@ethereum-waffle/compiler": "workspace:*", - "solc": "0.8.15", "@ethereum-waffle/provider": "workspace:*", - "typechain": "^8.0.0", + "@ethersproject/abi": "^5.6.1", + "@ethersproject/providers": "5.6.2", + "eslint": "^7.14.0", + "ethers": "5.6.2", "mocha": "^8.2.1", "rimraf": "^3.0.2", - "typescript": "^4.6.2", - "eslint": "^7.14.0", - "ts-node": "^9.0.0" + "solc": "0.8.15", + "ts-node": "^9.0.0", + "typechain": "^8.0.0", + "typescript": "^4.6.2" }, "peerDependencies": { "ethers": "*" diff --git a/waffle-mock-contract/src/index.ts b/waffle-mock-contract/src/index.ts index 6ddfe3fa6..d93b5d294 100644 --- a/waffle-mock-contract/src/index.ts +++ b/waffle-mock-contract/src/index.ts @@ -2,11 +2,17 @@ import {Contract, ContractFactory, Signer, utils} from 'ethers'; import type {JsonFragment} from '@ethersproject/abi'; import DoppelgangerContract from './Doppelganger.json'; +import type {JsonRpcProvider} from '@ethersproject/providers'; type ABI = string | Array export type Stub = ReturnType; +type DeployOptions = { + address: string; + override?: boolean; +} + export interface MockContract extends Contract { mock: { [key: string]: Stub; @@ -15,7 +21,31 @@ export interface MockContract extends Contract { staticcall (contract: Contract, functionName: string, ...params: any[]): Promise; } -async function deploy(signer: Signer) { +async function deploy(signer: Signer, options?: DeployOptions) { + if (options) { + const {address, override} = options; + const provider = signer.provider as JsonRpcProvider; + if (!override && await provider.getCode(address) !== '0x') { + throw new Error( + `${address} already contains a contract. ` + + 'If you want to override it, set the override parameter.'); + } + if ((provider as any)._hardhatNetwork) { + if (await provider.send('hardhat_setCode', [ + address, + '0x' + DoppelgangerContract.evm.deployedBytecode.object + ])) { + return new Contract(address, DoppelgangerContract.abi, signer); + } else throw new Error(`Couldn't deploy at ${address}`); + } else { + if (await provider.send('evm_setAccountCode', [ + address, + '0x' + DoppelgangerContract.evm.deployedBytecode.object + ])) { + return new Contract(address, DoppelgangerContract.abi, signer); + } else throw new Error(`Couldn't deploy at ${address}`); + } + } const factory = new ContractFactory(DoppelgangerContract.abi, DoppelgangerContract.bytecode, signer); return factory.deploy(); } @@ -53,8 +83,8 @@ function createMock(abi: ABI, mockContractInstance: Contract) { return mockedAbi; } -export async function deployMockContract(signer: Signer, abi: ABI): Promise { - const mockContractInstance = await deploy(signer); +export async function deployMockContract(signer: Signer, abi: ABI, options?: DeployOptions): Promise { + const mockContractInstance = await deploy(signer, options); const mock = createMock(abi, mockContractInstance); const mockedContract = new Contract(mockContractInstance.address, abi, signer) as MockContract; diff --git a/waffle-mock-contract/test/direct.test.ts b/waffle-mock-contract/test/direct.test.ts index 14429a597..f5396db95 100644 --- a/waffle-mock-contract/test/direct.test.ts +++ b/waffle-mock-contract/test/direct.test.ts @@ -1,102 +1,4 @@ -import {use, expect} from 'chai'; -import chaiAsPromised from 'chai-as-promised'; import {MockProvider} from '@ethereum-waffle/provider'; -import {waffleChai} from '@ethereum-waffle/chai'; -import {ContractFactory} from 'ethers'; +import {mockContractDirectTest} from './directTest'; -import {deployMockContract} from '../src'; -import Counter from './helpers/interfaces/Counter.json'; - -use(chaiAsPromised); -use(waffleChai); - -describe('Mock Contract - Integration (called directly)', () => { - const [wallet] = new MockProvider().getWallets(); - - it('throws readable error if mock was not set up for a method', async () => { - const mockCounter = await deployMockContract(wallet, Counter.abi); - - await expect(mockCounter.read()).to.be.revertedWith('Mock on the method is not initialized'); - }); - - it('mocking returned values', async () => { - const mockCounter = await deployMockContract(wallet, Counter.abi); - await mockCounter.mock.read.returns(45291); - - expect(await mockCounter.read()).to.equal(45291); - }); - - it('mocking revert', async () => { - const mockCounter = await deployMockContract(wallet, Counter.abi); - await mockCounter.mock.read.reverts(); - - await expect(mockCounter.read()).to.be.revertedWith('Mock revert'); - }); - - it('mock with call arguments', async () => { - const mockCounter = await deployMockContract(wallet, Counter.abi); - await mockCounter.mock.add.returns(1); - await mockCounter.mock.add.withArgs(1).returns(2); - await mockCounter.mock.add.withArgs(2).reverts(); - - expect(await mockCounter.add(0)).to.equal(1); - expect(await mockCounter.add(1)).to.equal(2); - await expect(mockCounter.add(2)).to.be.revertedWith('Mock revert'); - expect(await mockCounter.add(3)).to.equal(1); - }); - - it('should be able to call to another contract', async () => { - const counterFactory = new ContractFactory(Counter.abi, Counter.bytecode, wallet); - const counter = await counterFactory.deploy(); - const mockCounter = await deployMockContract(wallet, Counter.abi); - - expect(await mockCounter.staticcall(counter, 'read()')).to.equal('0'); - expect(await mockCounter.staticcall(counter, 'read')).to.equal('0'); - }); - - it('should be able to call another contract with a parameter', async () => { - const counterFactory = new ContractFactory(Counter.abi, Counter.bytecode, wallet); - const counter = await counterFactory.deploy(); - const mockCounter = await deployMockContract(wallet, Counter.abi); - - expect(await mockCounter.staticcall(counter, 'add', 1)).to.equal('1'); - }); - - it('should be able to call another contract with many parameters', async () => { - const counterFactory = new ContractFactory(Counter.abi, Counter.bytecode, wallet); - const counter = await counterFactory.deploy(); - const mockCounter = await deployMockContract(wallet, Counter.abi); - - expect(await mockCounter.staticcall(counter, 'addThree', 1, 2, 3)).to.equal('6'); - }); - - it('should be able to execute another contract', async () => { - const counterFactory = new ContractFactory(Counter.abi, Counter.bytecode, wallet); - const counter = await counterFactory.deploy(); - const mockCounter = await deployMockContract(wallet, Counter.abi); - - await mockCounter.call(counter, 'increment()'); - expect(await counter.read()).to.equal('1'); - - await mockCounter.call(counter, 'increment'); - expect(await counter.read()).to.equal('2'); - }); - - it('should be able to execute another contract with a parameter', async () => { - const counterFactory = new ContractFactory(Counter.abi, Counter.bytecode, wallet); - const counter = await counterFactory.deploy(); - const mockCounter = await deployMockContract(wallet, Counter.abi); - - await mockCounter.call(counter, 'increaseBy', 2); - expect(await counter.read()).to.equal('2'); - }); - - it('should be able to execute another contract with many parameters', async () => { - const counterFactory = new ContractFactory(Counter.abi, Counter.bytecode, wallet); - const counter = await counterFactory.deploy(); - const mockCounter = await deployMockContract(wallet, Counter.abi); - - await mockCounter.call(counter, 'increaseByThreeValues', 1, 2, 3); - expect(await counter.read()).to.equal('6'); - }); -}); +mockContractDirectTest(new MockProvider()); diff --git a/waffle-mock-contract/test/directTest.ts b/waffle-mock-contract/test/directTest.ts new file mode 100644 index 000000000..296f20f6e --- /dev/null +++ b/waffle-mock-contract/test/directTest.ts @@ -0,0 +1,130 @@ +import {use, expect} from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import {MockProvider} from '@ethereum-waffle/provider'; +import {waffleChai} from '@ethereum-waffle/chai'; +import {ContractFactory, Wallet} from 'ethers'; + +import {deployMockContract} from '../src'; +import Counter from './helpers/interfaces/Counter.json'; + +import DoppelGangerContract from '../src/Doppelganger.json'; + +export function mockContractDirectTest(provider: MockProvider) { + use(chaiAsPromised); + use(waffleChai); + + describe('Mock Contract - Integration (called directly)', () => { + const [wallet] = provider.getWallets(); + + it('throws readable error if mock was not set up for a method', async () => { + const mockCounter = await deployMockContract(wallet, Counter.abi); + + await expect(mockCounter.read()).to.be.revertedWith('Mock on the method is not initialized'); + }); + + it('mocking returned values', async () => { + const mockCounter = await deployMockContract(wallet, Counter.abi); + await mockCounter.mock.read.returns(45291); + + expect(await mockCounter.read()).to.equal(45291); + }); + + it('mocking revert', async () => { + const mockCounter = await deployMockContract(wallet, Counter.abi); + await mockCounter.mock.read.reverts(); + + await expect(mockCounter.read()).to.be.revertedWith('Mock revert'); + }); + + it('mock with call arguments', async () => { + const mockCounter = await deployMockContract(wallet, Counter.abi); + await mockCounter.mock.add.returns(1); + await mockCounter.mock.add.withArgs(1).returns(2); + await mockCounter.mock.add.withArgs(2).reverts(); + + expect(await mockCounter.add(0)).to.equal(1); + expect(await mockCounter.add(1)).to.equal(2); + await expect(mockCounter.add(2)).to.be.revertedWith('Mock revert'); + expect(await mockCounter.add(3)).to.equal(1); + }); + + it('should be able to call to another contract', async () => { + const counterFactory = new ContractFactory(Counter.abi, Counter.bytecode, wallet); + const counter = await counterFactory.deploy(); + const mockCounter = await deployMockContract(wallet, Counter.abi); + + expect(await mockCounter.staticcall(counter, 'read()')).to.equal('0'); + expect(await mockCounter.staticcall(counter, 'read')).to.equal('0'); + }); + + it('should be able to call another contract with a parameter', async () => { + const counterFactory = new ContractFactory(Counter.abi, Counter.bytecode, wallet); + const counter = await counterFactory.deploy(); + const mockCounter = await deployMockContract(wallet, Counter.abi); + + expect(await mockCounter.staticcall(counter, 'add', 1)).to.equal('1'); + }); + + it('should be able to call another contract with many parameters', async () => { + const counterFactory = new ContractFactory(Counter.abi, Counter.bytecode, wallet); + const counter = await counterFactory.deploy(); + const mockCounter = await deployMockContract(wallet, Counter.abi); + + expect(await mockCounter.staticcall(counter, 'addThree', 1, 2, 3)).to.equal('6'); + }); + + it('should be able to execute another contract', async () => { + const counterFactory = new ContractFactory(Counter.abi, Counter.bytecode, wallet); + const counter = await counterFactory.deploy(); + const mockCounter = await deployMockContract(wallet, Counter.abi); + + await mockCounter.call(counter, 'increment()'); + expect(await counter.read()).to.equal('1'); + + await mockCounter.call(counter, 'increment'); + expect(await counter.read()).to.equal('2'); + }); + + it('should be able to execute another contract with a parameter', async () => { + const counterFactory = new ContractFactory(Counter.abi, Counter.bytecode, wallet); + const counter = await counterFactory.deploy(); + const mockCounter = await deployMockContract(wallet, Counter.abi); + + await mockCounter.call(counter, 'increaseBy', 2); + expect(await counter.read()).to.equal('2'); + }); + + it('should be able to execute another contract with many parameters', async () => { + const counterFactory = new ContractFactory(Counter.abi, Counter.bytecode, wallet); + const counter = await counterFactory.deploy(); + const mockCounter = await deployMockContract(wallet, Counter.abi); + + await mockCounter.call(counter, 'increaseByThreeValues', 1, 2, 3); + expect(await counter.read()).to.equal('6'); + }); + + it('can be deployed under specified address', async () => { + const address = Wallet.createRandom().address; + const mockCounter = await deployMockContract(wallet, Counter.abi, {address}); + expect(mockCounter.address).to.eq(address); + expect(await provider.getCode(address)).to.eq('0x' + DoppelGangerContract.evm.deployedBytecode.object); + }); + + it('can\'t be deployed twice under the same address', async () => { + const address = Wallet.createRandom().address; + await deployMockContract(wallet, Counter.abi, {address}); + await expect(deployMockContract(wallet, Counter.abi, {address})).to.be.eventually.rejectedWith( + Error, + `${address} already contains a contract` + ); + }); + + it('can be overidden', async () => { + const counterFactory = new ContractFactory(Counter.abi, Counter.bytecode, wallet); + const counter = await counterFactory.deploy(); + expect(await provider.getCode(counter.address)).to.eq('0x' + Counter.evm.deployedBytecode.object); + await deployMockContract(wallet, Counter.abi, {address: counter.address, override: true}); + expect(await provider.getCode(counter.address)).to.eq('0x' + DoppelGangerContract.evm.deployedBytecode.object); + }); + }); +} diff --git a/waffle-mock-contract/test/proxied.test.ts b/waffle-mock-contract/test/proxied.test.ts index ebcb9f1af..bfffd3d48 100644 --- a/waffle-mock-contract/test/proxied.test.ts +++ b/waffle-mock-contract/test/proxied.test.ts @@ -1,50 +1,4 @@ -import {use, expect} from 'chai'; -import chaiAsPromised from 'chai-as-promised'; -import {ContractFactory} from 'ethers'; +import {mockContractProxiedTest} from './proxiedTest'; import {MockProvider} from '@ethereum-waffle/provider'; -import {waffleChai} from '@ethereum-waffle/chai'; -import {deployMockContract} from '../src'; -import Counter from './helpers/interfaces/Counter.json'; -import Proxy from './helpers/interfaces/Proxy.json'; - -use(chaiAsPromised); -use(waffleChai); - -describe('Mock Contract - Integration (called by other contract)', () => { - const [wallet] = new MockProvider().getWallets(); - - const deploy = async () => { - const mockCounter = await deployMockContract(wallet, Counter.abi); - const capFactory = new ContractFactory(Proxy.abi, Proxy.bytecode, wallet); - const capContract = await capFactory.deploy(mockCounter.address); - - return {mockCounter, capContract}; - }; - - it('mocking returned values', async () => { - const {capContract, mockCounter} = await deploy(); - - await mockCounter.mock.read.returns(5); - expect(await capContract.readCapped()).to.equal(5); - - await mockCounter.mock.read.returns(12); - expect(await capContract.readCapped()).to.equal(10); - }); - - it('mocking revert', async () => { - const {capContract, mockCounter} = await deploy(); - - await mockCounter.mock.read.reverts(); - await expect(capContract.readCapped()).to.be.revertedWith('Mock revert'); - }); - - it('mocking with call arguments', async () => { - const {capContract, mockCounter} = await deploy(); - await mockCounter.mock.add.withArgs(1).returns(1); - await mockCounter.mock.add.withArgs(2).returns(2); - - expect(await capContract.addCapped(1)).to.equal(1); - expect(await capContract.addCapped(2)).to.equal(2); - }); -}); +mockContractProxiedTest(new MockProvider()); diff --git a/waffle-mock-contract/test/proxiedTest.ts b/waffle-mock-contract/test/proxiedTest.ts new file mode 100644 index 000000000..7d324f950 --- /dev/null +++ b/waffle-mock-contract/test/proxiedTest.ts @@ -0,0 +1,61 @@ +import {use, expect} from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import {ContractFactory, Wallet} from 'ethers'; +import {MockProvider} from '@ethereum-waffle/provider'; +import {waffleChai} from '@ethereum-waffle/chai'; + +import {deployMockContract} from '../src'; +import Counter from './helpers/interfaces/Counter.json'; +import Proxy from './helpers/interfaces/Proxy.json'; + +export function mockContractProxiedTest(provider: MockProvider) { + use(chaiAsPromised); + use(waffleChai); + + describe('Mock Contract - Integration (called by other contract)', () => { + const [wallet] = new MockProvider().getWallets(); + + const deploy = async () => { + const mockCounter = await deployMockContract(wallet, Counter.abi); + const capFactory = new ContractFactory(Proxy.abi, Proxy.bytecode, wallet); + const capContract = await capFactory.deploy(mockCounter.address); + + return {mockCounter, capContract}; + }; + + it('mocking returned values', async () => { + const {capContract, mockCounter} = await deploy(); + + await mockCounter.mock.read.returns(5); + expect(await capContract.readCapped()).to.equal(5); + + await mockCounter.mock.read.returns(12); + expect(await capContract.readCapped()).to.equal(10); + }); + + it('mocking revert', async () => { + const {capContract, mockCounter} = await deploy(); + + await mockCounter.mock.read.reverts(); + await expect(capContract.readCapped()).to.be.revertedWith('Mock revert'); + }); + + it('mocking with call arguments', async () => { + const {capContract, mockCounter} = await deploy(); + await mockCounter.mock.add.withArgs(1).returns(1); + await mockCounter.mock.add.withArgs(2).returns(2); + + expect(await capContract.addCapped(1)).to.equal(1); + expect(await capContract.addCapped(2)).to.equal(2); + }); + + it('Mocking a contract for an already initialized proxy', async () => { + const address = Wallet.createRandom().address; + const proxyFactory = new ContractFactory(Proxy.abi, Proxy.bytecode, wallet); + const proxy = await proxyFactory.deploy(address); + const mockContract = await deployMockContract(wallet, Counter.abi, {address}); + await mockContract.mock.read.returns(1); + expect(await proxy.readCapped()).to.eq(1); + }); + }); +}