Skip to content
45 changes: 15 additions & 30 deletions test/metatx/ERC2771Context.test.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');

const { impersonate } = require('../helpers/account');
const { getDomain, ForwardRequest } = require('../helpers/eip712');
const { MAX_UINT48 } = require('../helpers/constants');

const { shouldBehaveLikeRegularContext } = require('../utils/Context.behavior');

async function fixture() {
const [sender, other] = await ethers.getSigners();

const forwarder = await ethers.deployContract('ERC2771Forwarder', []);
const forwarderAsSigner = await impersonate(forwarder.target);
const context = await ethers.deployContract('ERC2771ContextMock', [forwarder]);
const domain = await getDomain(forwarder);
const types = { ForwardRequest };

return { sender, other, forwarder, forwarderAsSigner, context, domain, types };
}

Expand All @@ -25,7 +21,7 @@ describe('ERC2771Context', function () {
Object.assign(this, await loadFixture(fixture));
});

it('recognize trusted forwarder', async function () {
it('recognizes trusted forwarder', async function () {
expect(await this.context.isTrustedForwarder(this.forwarder)).to.be.true;
});

Expand All @@ -42,7 +38,6 @@ describe('ERC2771Context', function () {
it('returns the relayed transaction original sender', async function () {
const nonce = await this.forwarder.nonces(this.sender);
const data = this.context.interface.encodeFunctionData('msgSender');

const req = {
from: await this.sender.getAddress(),
to: await this.context.getAddress(),
Expand All @@ -52,16 +47,15 @@ describe('ERC2771Context', function () {
nonce,
deadline: MAX_UINT48,
};

req.signature = await this.sender.signTypedData(this.domain, this.types, req);

expect(await this.forwarder.verify(req)).to.be.true;

await expect(this.forwarder.execute(req)).to.emit(this.context, 'Sender').withArgs(this.sender);
await expect(this.forwarder.execute(req))
.to.emit(this.context, 'Sender')
.withArgs(await this.sender.getAddress());
});

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 so `this.forwarderAsSigner` is used instead.
// The forwarder doesn't produce calls with calldata length less than 20 bytes so we use `forwarderAsSigner`
await expect(this.context.connect(this.forwarderAsSigner).msgSender())
.to.emit(this.context, 'Sender')
.withArgs(this.forwarder);
Expand All @@ -71,10 +65,8 @@ describe('ERC2771Context', function () {
describe('msgData', function () {
it('returns the relayed transaction original data', async function () {
const args = [42n, 'OpenZeppelin'];

const nonce = await this.forwarder.nonces(this.sender);
const data = this.context.interface.encodeFunctionData('msgData', args);

const req = {
from: await this.sender.getAddress(),
to: await this.context.getAddress(),
Expand All @@ -84,11 +76,8 @@ describe('ERC2771Context', function () {
nonce,
deadline: MAX_UINT48,
};

req.signature = this.sender.signTypedData(this.domain, this.types, req);

req.signature = await this.sender.signTypedData(this.domain, this.types, req);
expect(await this.forwarder.verify(req)).to.be.true;

await expect(this.forwarder.execute(req))
.to.emit(this.context, 'Data')
.withArgs(data, ...args);
Expand All @@ -97,23 +86,20 @@ describe('ERC2771Context', function () {

it('returns the full original data when calldata length is less than 20 bytes (address length)', async function () {
const data = this.context.interface.encodeFunctionData('msgDataShort');

// The forwarder doesn't produce calls with calldata length less than 20 bytes so `this.forwarderAsSigner` is used instead.
await expect(await this.context.connect(this.forwarderAsSigner).msgDataShort())
// The forwarder doesn't produce calls with calldata length less than 20 bytes so we use `forwarderAsSigner`
await expect(this.context.connect(this.forwarderAsSigner).msgDataShort())
.to.emit(this.context, 'DataShort')
.withArgs(data);
});
});

it('multicall poison attack', async function () {
const nonce = await this.forwarder.nonces(this.sender);
const data = this.context.interface.encodeFunctionData('multicall', [
[
// poisonned call to 'msgSender()'
ethers.concat([this.context.interface.encodeFunctionData('msgSender'), this.other.address]),
],
const callData = ethers.concat([
this.context.interface.encodeFunctionData('msgSender'),
this.other.address,
]);

const data = this.context.interface.encodeFunctionData('multicall', [[callData]]);
const req = {
from: await this.sender.getAddress(),
to: await this.context.getAddress(),
Expand All @@ -123,11 +109,10 @@ describe('ERC2771Context', function () {
nonce,
deadline: MAX_UINT48,
};

req.signature = await this.sender.signTypedData(this.domain, this.types, req);

expect(await this.forwarder.verify(req)).to.be.true;

await expect(this.forwarder.execute(req)).to.emit(this.context, 'Sender').withArgs(this.sender);
await expect(this.forwarder.execute(req))
.to.emit(this.context, 'Sender')
.withArgs(await this.sender.getAddress());
});
});