MinimalForwarder.test.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. const ethSigUtil = require('eth-sig-util');
  2. const Wallet = require('ethereumjs-wallet').default;
  3. const { getDomain, domainType } = require('../helpers/eip712');
  4. const { expectRevertCustomError } = require('../helpers/customError');
  5. const { constants, expectRevert } = require('@openzeppelin/test-helpers');
  6. const { expect } = require('chai');
  7. const MinimalForwarder = artifacts.require('MinimalForwarder');
  8. const CallReceiverMock = artifacts.require('CallReceiverMock');
  9. contract('MinimalForwarder', function (accounts) {
  10. beforeEach(async function () {
  11. this.forwarder = await MinimalForwarder.new();
  12. this.domain = await getDomain(this.forwarder);
  13. this.types = {
  14. EIP712Domain: domainType(this.domain),
  15. ForwardRequest: [
  16. { name: 'from', type: 'address' },
  17. { name: 'to', type: 'address' },
  18. { name: 'value', type: 'uint256' },
  19. { name: 'gas', type: 'uint256' },
  20. { name: 'nonce', type: 'uint256' },
  21. { name: 'data', type: 'bytes' },
  22. ],
  23. };
  24. });
  25. context('with message', function () {
  26. const tamperedValues = {
  27. from: accounts[0],
  28. to: accounts[0],
  29. value: web3.utils.toWei('1'),
  30. nonce: 1234,
  31. data: '0x1742',
  32. };
  33. beforeEach(async function () {
  34. this.wallet = Wallet.generate();
  35. this.sender = web3.utils.toChecksumAddress(this.wallet.getAddressString());
  36. this.req = {
  37. from: this.sender,
  38. to: constants.ZERO_ADDRESS,
  39. value: '0',
  40. gas: '100000',
  41. nonce: Number(await this.forwarder.getNonce(this.sender)),
  42. data: '0x',
  43. };
  44. this.forgeData = req => ({
  45. types: this.types,
  46. domain: this.domain,
  47. primaryType: 'ForwardRequest',
  48. message: { ...this.req, ...req },
  49. });
  50. this.sign = req =>
  51. ethSigUtil.signTypedMessage(this.wallet.getPrivateKey(), {
  52. data: this.forgeData(req),
  53. });
  54. });
  55. context('verify', function () {
  56. context('valid signature', function () {
  57. beforeEach(async function () {
  58. expect(await this.forwarder.getNonce(this.req.from)).to.be.bignumber.equal(web3.utils.toBN(this.req.nonce));
  59. });
  60. it('success', async function () {
  61. expect(await this.forwarder.verify(this.req, this.sign())).to.be.equal(true);
  62. });
  63. afterEach(async function () {
  64. expect(await this.forwarder.getNonce(this.req.from)).to.be.bignumber.equal(web3.utils.toBN(this.req.nonce));
  65. });
  66. });
  67. context('with tampered values', function () {
  68. for (const [key, value] of Object.entries(tamperedValues)) {
  69. it(`returns false with tampered ${key}`, async function () {
  70. expect(await this.forwarder.verify(this.forgeData({ [key]: value }).message, this.sign())).to.be.equal(
  71. false,
  72. );
  73. });
  74. }
  75. it('returns false with tampered signature', async function () {
  76. const tamperedsign = web3.utils.hexToBytes(this.sign());
  77. tamperedsign[42] ^= 0xff;
  78. expect(await this.forwarder.verify(this.req, web3.utils.bytesToHex(tamperedsign))).to.be.equal(false);
  79. });
  80. it('returns false with valid signature for non-current nonce', async function () {
  81. const req = {
  82. ...this.req,
  83. nonce: this.req.nonce + 1,
  84. };
  85. const sig = this.sign(req);
  86. expect(await this.forwarder.verify(req, sig)).to.be.equal(false);
  87. });
  88. });
  89. });
  90. context('execute', function () {
  91. context('valid signature', function () {
  92. beforeEach(async function () {
  93. expect(await this.forwarder.getNonce(this.req.from)).to.be.bignumber.equal(web3.utils.toBN(this.req.nonce));
  94. });
  95. it('success', async function () {
  96. await this.forwarder.execute(this.req, this.sign()); // expect to not revert
  97. });
  98. afterEach(async function () {
  99. expect(await this.forwarder.getNonce(this.req.from)).to.be.bignumber.equal(
  100. web3.utils.toBN(this.req.nonce + 1),
  101. );
  102. });
  103. });
  104. context('with tampered values', function () {
  105. for (const [key, value] of Object.entries(tamperedValues)) {
  106. it(`reverts with tampered ${key}`, async function () {
  107. const sig = this.sign();
  108. const data = this.forgeData({ [key]: value });
  109. await expectRevertCustomError(this.forwarder.execute(data.message, sig), 'MinimalForwarderInvalidSigner', [
  110. ethSigUtil.recoverTypedSignature({ data, sig }),
  111. data.message.from,
  112. ]);
  113. });
  114. }
  115. it('reverts with tampered signature', async function () {
  116. const tamperedSig = web3.utils.hexToBytes(this.sign());
  117. tamperedSig[42] ^= 0xff;
  118. await expectRevertCustomError(
  119. this.forwarder.execute(this.req, web3.utils.bytesToHex(tamperedSig)),
  120. 'MinimalForwarderInvalidSigner',
  121. [ethSigUtil.recoverTypedSignature({ data: this.forgeData(), sig: tamperedSig }), this.req.from],
  122. );
  123. });
  124. it('reverts with valid signature for non-current nonce', async function () {
  125. const req = {
  126. ...this.req,
  127. nonce: this.req.nonce + 1,
  128. };
  129. const sig = this.sign(req);
  130. await expectRevertCustomError(this.forwarder.execute(req, sig), 'MinimalForwarderInvalidNonce', [
  131. this.req.from,
  132. this.req.nonce,
  133. ]);
  134. });
  135. });
  136. it('bubble out of gas', async function () {
  137. const receiver = await CallReceiverMock.new();
  138. const gasAvailable = 100000;
  139. this.req.to = receiver.address;
  140. this.req.data = receiver.contract.methods.mockFunctionOutOfGas().encodeABI();
  141. this.req.gas = 1000000;
  142. await expectRevert.assertion(this.forwarder.execute(this.req, this.sign(), { gas: gasAvailable }));
  143. const { transactions } = await web3.eth.getBlock('latest');
  144. const { gasUsed } = await web3.eth.getTransactionReceipt(transactions[0]);
  145. expect(gasUsed).to.be.equal(gasAvailable);
  146. });
  147. });
  148. });
  149. });