ERC2771Context.test.js 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. const { ethers } = require('hardhat');
  2. const { expect } = require('chai');
  3. const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
  4. const { impersonate } = require('../helpers/account');
  5. const { getDomain, ForwardRequest } = require('../helpers/eip712');
  6. const { MAX_UINT48 } = require('../helpers/constants');
  7. const { shouldBehaveLikeRegularContext } = require('../utils/Context.behavior');
  8. async function fixture() {
  9. const [sender, other] = await ethers.getSigners();
  10. const forwarder = await ethers.deployContract('ERC2771Forwarder', []);
  11. const forwarderAsSigner = await impersonate(forwarder.target);
  12. const context = await ethers.deployContract('ERC2771ContextMock', [forwarder]);
  13. const domain = await getDomain(forwarder);
  14. const types = { ForwardRequest };
  15. return { sender, other, forwarder, forwarderAsSigner, context, domain, types };
  16. }
  17. describe('ERC2771Context', function () {
  18. beforeEach(async function () {
  19. Object.assign(this, await loadFixture(fixture));
  20. });
  21. it('recognize trusted forwarder', async function () {
  22. expect(await this.context.isTrustedForwarder(this.forwarder)).to.be.true;
  23. });
  24. it('returns the trusted forwarder', async function () {
  25. expect(await this.context.trustedForwarder()).to.equal(this.forwarder);
  26. });
  27. describe('when called directly', function () {
  28. shouldBehaveLikeRegularContext();
  29. });
  30. describe('when receiving a relayed call', function () {
  31. describe('msgSender', function () {
  32. it('returns the relayed transaction original sender', async function () {
  33. const nonce = await this.forwarder.nonces(this.sender);
  34. const data = this.context.interface.encodeFunctionData('msgSender');
  35. const req = {
  36. from: await this.sender.getAddress(),
  37. to: await this.context.getAddress(),
  38. value: 0n,
  39. data,
  40. gas: 100000n,
  41. nonce,
  42. deadline: MAX_UINT48,
  43. };
  44. req.signature = await this.sender.signTypedData(this.domain, this.types, req);
  45. expect(await this.forwarder.verify(req)).to.be.true;
  46. await expect(this.forwarder.execute(req)).to.emit(this.context, 'Sender').withArgs(this.sender);
  47. });
  48. it('returns the original sender when calldata length is less than 20 bytes (address length)', async function () {
  49. // The forwarder doesn't produce calls with calldata length less than 20 bytes so `this.forwarderAsSigner` is used instead.
  50. await expect(this.context.connect(this.forwarderAsSigner).msgSender())
  51. .to.emit(this.context, 'Sender')
  52. .withArgs(this.forwarder);
  53. });
  54. });
  55. describe('msgData', function () {
  56. it('returns the relayed transaction original data', async function () {
  57. const args = [42n, 'OpenZeppelin'];
  58. const nonce = await this.forwarder.nonces(this.sender);
  59. const data = this.context.interface.encodeFunctionData('msgData', args);
  60. const req = {
  61. from: await this.sender.getAddress(),
  62. to: await this.context.getAddress(),
  63. value: 0n,
  64. data,
  65. gas: 100000n,
  66. nonce,
  67. deadline: MAX_UINT48,
  68. };
  69. req.signature = this.sender.signTypedData(this.domain, this.types, req);
  70. expect(await this.forwarder.verify(req)).to.be.true;
  71. await expect(this.forwarder.execute(req))
  72. .to.emit(this.context, 'Data')
  73. .withArgs(data, ...args);
  74. });
  75. });
  76. it('returns the full original data when calldata length is less than 20 bytes (address length)', async function () {
  77. const data = this.context.interface.encodeFunctionData('msgDataShort');
  78. // The forwarder doesn't produce calls with calldata length less than 20 bytes so `this.forwarderAsSigner` is used instead.
  79. await expect(await this.context.connect(this.forwarderAsSigner).msgDataShort())
  80. .to.emit(this.context, 'DataShort')
  81. .withArgs(data);
  82. });
  83. });
  84. it('multicall poison attack', async function () {
  85. const nonce = await this.forwarder.nonces(this.sender);
  86. const data = this.context.interface.encodeFunctionData('multicall', [
  87. [
  88. // poisonned call to 'msgSender()'
  89. ethers.concat([this.context.interface.encodeFunctionData('msgSender'), this.other.address]),
  90. ],
  91. ]);
  92. const req = {
  93. from: await this.sender.getAddress(),
  94. to: await this.context.getAddress(),
  95. value: 0n,
  96. data,
  97. gas: 100000n,
  98. nonce,
  99. deadline: MAX_UINT48,
  100. };
  101. req.signature = await this.sender.signTypedData(this.domain, this.types, req);
  102. expect(await this.forwarder.verify(req)).to.be.true;
  103. await expect(this.forwarder.execute(req)).to.emit(this.context, 'Sender').withArgs(this.sender);
  104. });
  105. });