diff --git a/.changeset/warm-guests-rule.md b/.changeset/warm-guests-rule.md new file mode 100644 index 00000000000..049da4dd5d5 --- /dev/null +++ b/.changeset/warm-guests-rule.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': patch +--- + +`ERC2771Context`: Prevent revert in `_msgData()` when a call originating from a trusted forwarder is not long enough to contain the request signer address (i.e. `msg.data.length` is less than 20 bytes). Return the full calldata in that case. diff --git a/contracts/metatx/ERC2771Context.sol b/contracts/metatx/ERC2771Context.sol index 0996fcbd915..d54aca9eedd 100644 --- a/contracts/metatx/ERC2771Context.sol +++ b/contracts/metatx/ERC2771Context.sol @@ -34,7 +34,7 @@ abstract contract ERC2771Context is Context { } function _msgData() internal view virtual override returns (bytes calldata) { - if (isTrustedForwarder(msg.sender)) { + if (isTrustedForwarder(msg.sender) && msg.data.length >= 20) { return msg.data[:msg.data.length - 20]; } else { return super._msgData(); diff --git a/contracts/mocks/ContextMock.sol b/contracts/mocks/ContextMock.sol index 7759f350639..6cc0abc497b 100644 --- a/contracts/mocks/ContextMock.sol +++ b/contracts/mocks/ContextMock.sol @@ -16,6 +16,12 @@ contract ContextMock is Context { function msgData(uint256 integerValue, string memory stringValue) public { emit Data(_msgData(), integerValue, stringValue); } + + event DataShort(bytes data); + + function msgDataShort() public { + emit DataShort(_msgData()); + } } contract ContextMockCaller { diff --git a/test/metatx/ERC2771Context.test.js b/test/metatx/ERC2771Context.test.js index cfa5e7298e0..5108d11cdfa 100644 --- a/test/metatx/ERC2771Context.test.js +++ b/test/metatx/ERC2771Context.test.js @@ -12,7 +12,7 @@ const ContextMockCaller = artifacts.require('ContextMockCaller'); const { shouldBehaveLikeRegularContext } = require('../utils/Context.behavior'); contract('ERC2771Context', function (accounts) { - const [, anotherAccount] = accounts; + const [, trustedForwarder] = accounts; beforeEach(async function () { this.forwarder = await MinimalForwarder.new(); @@ -78,11 +78,11 @@ contract('ERC2771Context', function (accounts) { 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(anotherAccount); + const recipient = await ERC2771ContextMock.new(trustedForwarder); - const { receipt } = await recipient.msgSender({ from: anotherAccount }); + const { receipt } = await recipient.msgSender({ from: trustedForwarder }); - await expectEvent(receipt, 'Sender', { sender: anotherAccount }); + await expectEvent(receipt, 'Sender', { sender: trustedForwarder }); }); }); @@ -108,5 +108,15 @@ contract('ERC2771Context', function (accounts) { 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 }); + }); }); });