1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
const ethSigUtil = require('eth-sig-util');
const Wallet = require('ethereumjs-wallet').default;
const { getDomain, domainType } = require('../helpers/eip712');
const { MAX_UINT48 } = require('../helpers/constants');
const { expectEvent } = require('@openzeppelin/test-helpers');
const { expect } = require('chai');
const ERC2771ContextMock = artifacts.require('ERC2771ContextMock');
const ERC2771Forwarder = artifacts.require('ERC2771Forwarder');
const ContextMockCaller = artifacts.require('ContextMockCaller');
const { shouldBehaveLikeRegularContext } = require('../utils/Context.behavior');
contract('ERC2771Context', function (accounts) {
const [, trustedForwarder, other] = accounts;
beforeEach(async function () {
this.forwarder = await ERC2771Forwarder.new('ERC2771Forwarder');
this.recipient = await ERC2771ContextMock.new(this.forwarder.address);
this.domain = await getDomain(this.forwarder);
this.types = {
EIP712Domain: domainType(this.domain),
ForwardRequest: [
{ name: 'from', type: 'address' },
{ name: 'to', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'gas', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint48' },
{ name: 'data', type: 'bytes' },
],
};
});
it('recognize trusted forwarder', async function () {
expect(await this.recipient.isTrustedForwarder(this.forwarder.address)).to.equal(true);
});
it('returns the trusted forwarder', async function () {
expect(await this.recipient.trustedForwarder()).to.equal(this.forwarder.address);
});
context('when called directly', function () {
beforeEach(async function () {
this.context = this.recipient; // The Context behavior expects the contract in this.context
this.caller = await ContextMockCaller.new();
});
shouldBehaveLikeRegularContext(...accounts);
});
context('when receiving a relayed call', function () {
beforeEach(async function () {
this.wallet = Wallet.generate();
this.sender = web3.utils.toChecksumAddress(this.wallet.getAddressString());
this.data = {
types: this.types,
domain: this.domain,
primaryType: 'ForwardRequest',
};
});
describe('msgSender', function () {
it('returns the relayed transaction original sender', async function () {
const data = this.recipient.contract.methods.msgSender().encodeABI();
const req = {
from: this.sender,
to: this.recipient.address,
value: '0',
gas: '100000',
nonce: (await this.forwarder.nonces(this.sender)).toString(),
deadline: MAX_UINT48,
data,
};
req.signature = ethSigUtil.signTypedMessage(this.wallet.getPrivateKey(), {
data: { ...this.data, message: req },
});
expect(await this.forwarder.verify(req)).to.equal(true);
const { tx } = await this.forwarder.execute(req);
await expectEvent.inTransaction(tx, ERC2771ContextMock, 'Sender', { sender: this.sender });
});
it('returns the original sender when calldata length is less than 20 bytes (address length)', async function () {
// The forwarder doesn't produce calls with calldata length less than 20 bytes
const recipient = await ERC2771ContextMock.new(trustedForwarder);
const { receipt } = await recipient.msgSender({ from: trustedForwarder });
await expectEvent(receipt, 'Sender', { sender: trustedForwarder });
});
});
describe('msgData', function () {
it('returns the relayed transaction original data', async function () {
const integerValue = '42';
const stringValue = 'OpenZeppelin';
const data = this.recipient.contract.methods.msgData(integerValue, stringValue).encodeABI();
const req = {
from: this.sender,
to: this.recipient.address,
value: '0',
gas: '100000',
nonce: (await this.forwarder.nonces(this.sender)).toString(),
deadline: MAX_UINT48,
data,
};
req.signature = ethSigUtil.signTypedMessage(this.wallet.getPrivateKey(), {
data: { ...this.data, message: req },
});
expect(await this.forwarder.verify(req)).to.equal(true);
const { tx } = await this.forwarder.execute(req);
await expectEvent.inTransaction(tx, ERC2771ContextMock, 'Data', { data, integerValue, stringValue });
});
});
it('returns the full original data when calldata length is less than 20 bytes (address length)', async function () {
// The forwarder doesn't produce calls with calldata length less than 20 bytes
const recipient = await ERC2771ContextMock.new(trustedForwarder);
const { receipt } = await recipient.msgDataShort({ from: trustedForwarder });
const data = recipient.contract.methods.msgDataShort().encodeABI();
await expectEvent(receipt, 'DataShort', { data });
});
});
it('multicall poison attack', async function () {
const attacker = Wallet.generate();
const attackerAddress = attacker.getChecksumAddressString();
const nonce = await this.forwarder.nonces(attackerAddress);
const msgSenderCall = web3.eth.abi.encodeFunctionCall(
{
name: 'msgSender',
type: 'function',
inputs: [],
},
[],
);
const data = web3.eth.abi.encodeFunctionCall(
{
name: 'multicall',
type: 'function',
inputs: [
{
internalType: 'bytes[]',
name: 'data',
type: 'bytes[]',
},
],
},
[[web3.utils.encodePacked({ value: msgSenderCall, type: 'bytes' }, { value: other, type: 'address' })]],
);
const req = {
from: attackerAddress,
to: this.recipient.address,
value: '0',
gas: '100000',
data,
nonce: Number(nonce),
deadline: MAX_UINT48,
};
req.signature = await ethSigUtil.signTypedMessage(attacker.getPrivateKey(), {
data: {
types: this.types,
domain: this.domain,
primaryType: 'ForwardRequest',
message: req,
},
});
expect(await this.forwarder.verify(req)).to.equal(true);
const receipt = await this.forwarder.execute(req);
await expectEvent.inTransaction(receipt.tx, ERC2771ContextMock, 'Sender', { sender: attackerAddress });
});
});