Skip to content

Commit 4d83cde

Browse files
authoredFeb 6, 2023
⭐️ Support events not defined in a contract (#822)
* ⭐️ Support events not defined in a contract
1 parent a1d89d0 commit 4d83cde

File tree

6 files changed

+188
-35
lines changed

6 files changed

+188
-35
lines changed
 

‎.changeset/hip-adults-guess.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@ethereum-waffle/mock-contract": patch
3+
"@ethereum-waffle/chai": patch
4+
---
5+
6+
Emit matcher improvement

‎waffle-chai/src/matchers/emit.ts

+51-33
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,88 @@
11
import {Contract, providers, utils} from 'ethers';
2+
import {keccak256, toUtf8Bytes} from 'ethers/lib/utils';
23
import {callPromise} from '../call-promise';
34
import {waitForPendingTransaction} from './misc/transaction';
45
import {supportWithArgs} from './withArgs';
56
import {supportWithNamedArgs} from './withNamedArgs';
67

78
export function supportEmit(Assertion: Chai.AssertionStatic) {
8-
const filterLogsWithTopics = (logs: providers.Log[], topic: any, contractAddress: string) =>
9+
const filterLogsWithTopics = (logs: providers.Log[], topic: any, contractAddress?: string) =>
910
logs.filter((log) => log.topics.includes(topic))
10-
.filter((log) => log.address && log.address.toLowerCase() === contractAddress.toLowerCase());
11+
.filter((log) =>
12+
log.address &&
13+
(contractAddress === undefined || log.address.toLowerCase() === contractAddress.toLowerCase()
14+
));
1115

12-
Assertion.addMethod('emit', function (this: any, contract: Contract, eventName: string) {
16+
const assertEmit = (assertion: any, frag: utils.EventFragment, isNegated: boolean, from?: string) => {
17+
const topic = keccak256(toUtf8Bytes(frag.format()));
18+
const receipt: providers.TransactionReceipt = assertion.txReceipt;
19+
assertion.args = filterLogsWithTopics(receipt.logs, topic, from);
20+
const isCurrentlyNegated = assertion.__flags.negate === true;
21+
assertion.__flags.negate = isNegated;
22+
assertion.assert(assertion.args.length > 0,
23+
`Expected event "${frag.name}" to be emitted, but it wasn't`,
24+
`Expected event "${frag.name}" NOT to be emitted, but it was`
25+
);
26+
assertion.__flags.negate = isCurrentlyNegated;
27+
};
28+
29+
Assertion.addMethod('emit', function (this: any, contractOrEventSig: Contract|string, eventName?: string) {
1330
if (typeof this._obj === 'string') {
31+
if (typeof contractOrEventSig === 'string') {
32+
throw new Error('The emit by event signature matcher must be called on a transaction');
33+
}
1434
// Handle specific case of using transaction hash to specify transaction. Done for backwards compatibility.
15-
this.callPromise = waitForPendingTransaction(this._obj, contract.provider)
35+
this.callPromise = waitForPendingTransaction(this._obj, contractOrEventSig.provider)
1636
.then(txReceipt => {
1737
this.txReceipt = txReceipt;
1838
});
1939
} else {
2040
callPromise(this);
2141
}
2242
const isNegated = this.__flags.negate === true;
23-
this.callPromise = this.callPromise
24-
.then(() => {
25-
if (!('txReceipt' in this)) {
26-
throw new Error('The emit matcher must be called on a transaction');
43+
this.callPromise = this.callPromise.then(() => {
44+
if (!('txReceipt' in this)) {
45+
throw new Error('The emit matcher must be called on a transaction');
46+
}
47+
let eventFragment: utils.EventFragment | undefined;
48+
if (typeof contractOrEventSig === 'string') {
49+
try {
50+
eventFragment = utils.EventFragment.from(contractOrEventSig);
51+
} catch (e) {
52+
throw new Error(`Invalid event signature: "${contractOrEventSig}"`);
2753
}
28-
const receipt: providers.TransactionReceipt = this.txReceipt;
29-
let eventFragment: utils.EventFragment | undefined;
54+
assertEmit(this, eventFragment, isNegated);
55+
} else if (eventName) {
3056
try {
31-
eventFragment = contract.interface.getEvent(eventName);
57+
eventFragment = contractOrEventSig.interface.getEvent(eventName);
3258
} catch (e) {
33-
// ignore error
59+
// ignore error
3460
}
3561
if (eventFragment === undefined) {
3662
this.assert(
37-
isNegated,
63+
this.__flags.negate,
3864
`Expected event "${eventName}" to be emitted, but it doesn't` +
39-
' exist in the contract. Please make sure you\'ve compiled' +
40-
' its latest version before running the test.',
65+
' exist in the contract. Please make sure you\'ve compiled' +
66+
' its latest version before running the test.',
4167
`WARNING: Expected event "${eventName}" NOT to be emitted.` +
42-
' The event wasn\'t emitted because it doesn\'t' +
43-
' exist in the contract. Please make sure you\'ve compiled' +
44-
' its latest version before running the test.',
68+
' The event wasn\'t emitted because it doesn\'t' +
69+
' exist in the contract. Please make sure you\'ve compiled' +
70+
' its latest version before running the test.',
4571
eventName,
4672
''
4773
);
4874
return;
4975
}
76+
assertEmit(this, eventFragment, isNegated, contractOrEventSig.address);
77+
78+
this.contract = contractOrEventSig;
79+
} else {
80+
throw new Error('The emit matcher must be called with a contract and an event name or an event signature');
81+
}
82+
});
5083

51-
const topic = contract.interface.getEventTopic(eventFragment);
52-
this.args = filterLogsWithTopics(receipt.logs, topic, contract.address);
53-
// As this callback will be resolved after the chain of matchers is finished, we need to
54-
// know if the matcher has been negated or not. To simulate chai behaviour, we keep track of whether
55-
// the matcher has been negated or not and set the internal chai flag __flags.negate to the same value.
56-
// After the assertion is finished, we set the flag back to original value to not affect other assertions.
57-
const isCurrentlyNegated = this.__flags.negate === true;
58-
this.__flags.negate = isNegated;
59-
this.assert(this.args.length > 0,
60-
`Expected event "${eventName}" to be emitted, but it wasn't`,
61-
`Expected event "${eventName}" NOT to be emitted, but it was`
62-
);
63-
this.__flags.negate = isCurrentlyNegated;
64-
});
6584
this.then = this.callPromise.then.bind(this.callPromise);
6685
this.catch = this.callPromise.catch.bind(this.callPromise);
67-
this.contract = contract;
6886
this.eventName = eventName;
6987
this.txMatcher = 'emit';
7088
return this;

‎waffle-chai/src/types.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ declare namespace Chai {
88
interface Assertion extends LanguageChains, NumericComparison, TypeComparison {
99
reverted: AsyncAssertion;
1010
revertedWith(reason: string | RegExp): RevertedWithAssertion;
11-
emit(contract: any, eventName: string): EmitAssertion;
11+
emit(contractOrEventSig: any, eventName?: string): EmitAssertion;
1212
properHex(length: number): void;
1313
hexEqual(other: string): void;
1414
properPrivateKey: void;
+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
export const EVENTSPROXY_SOURCE = `
2+
pragma solidity ^0.8.3;
3+
4+
import {Events} from "./Events.sol";
5+
6+
contract EventsProxy {
7+
Events public events;
8+
9+
constructor(Events _events) {
10+
events = _events;
11+
}
12+
13+
function emitTwoDelegate() public {
14+
// emit Two with a delegatecall to the events contract
15+
(bool success, ) = address(events).delegatecall(
16+
abi.encodeWithSignature("emitTwo()")
17+
);
18+
require(success, "delegatecall failed");
19+
}
20+
21+
function emitOne() public {
22+
events.emitOne();
23+
}
24+
}`;
25+
26+
export const EVENTSPROXY_ABI = [
27+
{
28+
inputs: [
29+
{
30+
internalType: 'contract Events',
31+
name: '_events',
32+
type: 'address'
33+
}
34+
],
35+
stateMutability: 'nonpayable',
36+
type: 'constructor'
37+
},
38+
{
39+
inputs: [],
40+
name: 'emitOne',
41+
outputs: [],
42+
stateMutability: 'nonpayable',
43+
type: 'function'
44+
},
45+
{
46+
inputs: [],
47+
name: 'emitTwoDelegate',
48+
outputs: [],
49+
stateMutability: 'nonpayable',
50+
type: 'function'
51+
},
52+
{
53+
inputs: [],
54+
name: 'events',
55+
outputs: [
56+
{
57+
internalType: 'contract Events',
58+
name: '',
59+
type: 'address'
60+
}
61+
],
62+
stateMutability: 'view',
63+
type: 'function'
64+
}
65+
];
66+
67+
// eslint-disable-next-line max-len
68+
export const EVENTSPROXY_BYTECODE = '608060405234801561001057600080fd5b506040516105413803806105418339818101604052810190610032919061008d565b806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555050610111565b600081519050610087816100fa565b92915050565b60006020828403121561009f57600080fd5b60006100ad84828501610078565b91505092915050565b60006100c1826100da565b9050919050565b60006100d3826100b6565b9050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b610103816100c8565b811461010e57600080fd5b50565b610421806101206000396000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c8063272b33af146100465780633f0e64ba14610050578063b5f8558c1461005a575b600080fd5b61004e610078565b005b6100586101c9565b005b61006261024b565b60405161006f91906102e9565b60405180910390f35b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff166040516024016040516020818303038152906040527f34c10115000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff838183161783525050505060405161014291906102d2565b600060405180830381855af49150503d806000811461017d576040519150601f19603f3d011682016040523d82523d6000602084013e610182565b606091505b50509050806101c6576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101bd90610304565b60405180910390fd5b50565b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16633f0e64ba6040518163ffffffff1660e01b8152600401600060405180830381600087803b15801561023157600080fd5b505af1158015610245573d6000803e3d6000fd5b50505050565b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b600061027a82610324565b610284818561032f565b935061029481856020860161038f565b80840191505092915050565b6102a98161036b565b82525050565b60006102bc60138361033a565b91506102c7826103c2565b602082019050919050565b60006102de828461026f565b915081905092915050565b60006020820190506102fe60008301846102a0565b92915050565b6000602082019050818103600083015261031d816102af565b9050919050565b600081519050919050565b600081905092915050565b600082825260208201905092915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006103768261037d565b9050919050565b60006103888261034b565b9050919050565b60005b838110156103ad578082015181840152602081019050610392565b838111156103bc576000848401525b50505050565b7f64656c656761746563616c6c206661696c65640000000000000000000000000060008201525056fea26469706673582212200ce5c4d7353a3aed66c47b692095e41e9a8c9f4b6412620eabf464a32caff56a64736f6c63430008030033';

‎waffle-chai/test/matchers/eventsTest.ts

+46-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import {expect, AssertionError} from 'chai';
22
import {Wallet, Contract, ContractFactory, BigNumber, ethers} from 'ethers';
33
import {EVENTS_ABI, EVENTS_BYTECODE} from '../contracts/Events';
4+
import {EVENTSPROXY_ABI, EVENTSPROXY_BYTECODE} from '../contracts/EventsProxy';
45

56
import type {TestProvider} from '@ethereum-waffle/provider';
6-
77
/**
88
* Struct emitted in the Events contract, emitStruct method
99
*/
@@ -698,5 +698,50 @@ export const eventsWithNamedArgs = (provider: TestProvider) => {
698698
'{ Object (hash, value, ...) } to deeply equal { Object (hash, value, ...) }'
699699
);
700700
});
701+
702+
it('Signature only - delegatecall', async () => {
703+
const proxyFactory = new ContractFactory(EVENTSPROXY_ABI, EVENTSPROXY_BYTECODE, wallet);
704+
const proxy = await proxyFactory.deploy(events.address);
705+
706+
await expect(proxy.emitTwoDelegate()).to.emit('Two(uint256,string)');
707+
});
708+
709+
it('Signature only - regular event', async () => {
710+
await expect(events.emitTwo()).to.emit('Two(uint256,string)');
711+
});
712+
713+
it('Signature only - negative', async () => {
714+
await expect(
715+
expect(events.emitTwo()).to.emit('One(uint256,string,bytes32)')
716+
).to.be.eventually.rejectedWith(
717+
AssertionError,
718+
'Expected event "One" to be emitted, but it wasn\'t'
719+
);
720+
});
721+
722+
it('Signature only - invalid event signature', async () => {
723+
await expect(
724+
expect(events.emitTwo()).to.emit('One')
725+
).to.be.eventually.rejectedWith(
726+
Error,
727+
'Invalid event signature: "One"'
728+
);
729+
});
730+
731+
it('Signature only - invalid args', async () => {
732+
await expect(
733+
expect(events.emitTwo()).to.emit(events)
734+
).to.be.eventually.rejectedWith(
735+
Error,
736+
'The emit matcher must be called with a contract and an event name or an event signature'
737+
);
738+
});
739+
740+
it('Signature only - Other contract event', async () => {
741+
const proxyFactory = new ContractFactory(EVENTSPROXY_ABI, EVENTSPROXY_BYTECODE, wallet);
742+
const proxy = await proxyFactory.deploy(events.address);
743+
744+
await expect(proxy.emitOne()).to.emit('One(uint256,string,bytes32)');
745+
});
701746
});
702747
};

‎waffle-mock-contract/test/proxiedTest.ts

+16
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,21 @@ export function mockContractProxiedTest(provider: MockProvider) {
5757
await mockContract.mock.read.returns(1);
5858
expect(await proxy.readCapped()).to.eq(1);
5959
});
60+
61+
it('calledOnContract with mock contract', async () => {
62+
const {capContract, mockCounter} = await deploy();
63+
64+
await mockCounter.mock.read.returns(1);
65+
await capContract.readCapped();
66+
expect('read').to.be.calledOnContract(mockCounter);
67+
});
68+
69+
it('calledOnContractWith with mock contract', async () => {
70+
const {capContract, mockCounter} = await deploy();
71+
72+
await mockCounter.mock.add.returns(1);
73+
await capContract.addCapped(1);
74+
expect('add').to.be.calledOnContractWith(mockCounter, [1]);
75+
});
6076
});
6177
}

0 commit comments

Comments
 (0)
Please sign in to comment.