diff --git a/contracts/drafts/meta-tx/GSNContext.sol b/contracts/drafts/meta-tx/GSNContext.sol new file mode 100644 index 000000000..7e088e7df --- /dev/null +++ b/contracts/drafts/meta-tx/GSNContext.sol @@ -0,0 +1,71 @@ +pragma solidity ^0.5.0; + +import "./Context.sol"; + +/* + * @dev Enables GSN support by recognizing calls from RelayHub and extracting + * the actual sender and call data from the received values. + */ +contract GSNContext is Context { + address private _relayHub; + + constructor(address relayHub) public { + _setRelayHub(relayHub); + } + + function _setRelayHub(address relayHub) internal { + _relayHub = relayHub; + } + + // Overrides for Context's functions: when called from RelayHub, sender and + // data require some pre-processing: the actual sender is stored at the end + // of the call data, which in turns means it needs to be removed from it + // when handling said data. + + function _msgSender() internal view returns (address) { + if (msg.sender != _relayHub) { + return msg.sender; + } else { + return _getRelayedCallSender(); + } + } + + function _msgData() internal view returns (bytes memory) { + if (msg.sender != _relayHub) { + return msg.data; + } else { + return _getRelayedCallData(); + } + } + + function _getRelayedCallSender() private pure returns (address result) { + // We need to read 20 bytes (an address) located at array index msg.data.length - 20. In memory, the array + // is prefixed with a 32-byte length value, so we first add 32 to get the memory read index. However, doing + // so would leave the address in the upper 20 bytes of the 32-byte word, which is inconvenient and would + // require bit shifting. We therefore subtract 12 from the read index so the address lands on the lower 20 + // bytes. This can always be done due to the 32-byte prefix. + + // The final memory read index is msg.data.length - 20 + 32 - 12 = msg.data.length + + // These fields are not accessible from assembly + bytes memory array = msg.data; + uint256 index = msg.data.length; + + assembly { + // Load the 32 bytes word from memory with the address on the lower 20 bytes, and mask those. + result := and(mload(add(array, index)), 0xffffffffffffffffffffffffffffffffffffffff) + } + return result; + } + + function _getRelayedCallData() private pure returns (bytes memory) { + uint256 actualDataLength = msg.data.length - 20; + bytes memory actualData = new bytes(actualDataLength); + + for (uint256 i = 0; i < actualDataLength; ++i) { + actualData[i] = msg.data[i]; + } + + return actualData; + } +} diff --git a/contracts/mocks/GSNContextMock.sol b/contracts/mocks/GSNContextMock.sol new file mode 100644 index 000000000..d58186875 --- /dev/null +++ b/contracts/mocks/GSNContextMock.sol @@ -0,0 +1,10 @@ +pragma solidity ^0.5.0; + +import "./ContextMock.sol"; +import "../drafts/meta-tx/GSNContext.sol"; + +// By inheriting from GSNContext, the internal functions are overridden automatically +contract GSNContextMock is ContextMock, GSNContext { + constructor(address relayHub) public GSNContext(relayHub) { + } +} diff --git a/test/drafts/meta-tx/Context.behavior.js b/test/drafts/meta-tx/Context.behavior.js new file mode 100644 index 000000000..127055432 --- /dev/null +++ b/test/drafts/meta-tx/Context.behavior.js @@ -0,0 +1,42 @@ +const { BN, expectEvent } = require('openzeppelin-test-helpers'); + +const ContextMock = artifacts.require('ContextMock'); + +function shouldBehaveLikeRegularContext(sender) { + describe('msgSender', function () { + it('returns the transaction sender when called from an EOA', async function () { + const { logs } = await this.context.msgSender({ from: sender }); + expectEvent.inLogs(logs, 'Sender', { sender }); + }); + + it('returns the transaction sender when from another contract', async function () { + const { tx } = await this.caller.callSender(this.context.address, { from: sender }); + await expectEvent.inTransaction(tx, ContextMock, 'Sender', { sender: this.caller.address }); + }); + }); + + describe('msgData', function () { + const integerValue = new BN('42'); + const stringValue = 'OpenZeppelin'; + + let callData; + + beforeEach(async function () { + callData = this.context.contract.methods.msgData(integerValue.toString(), stringValue).encodeABI(); + }); + + it('returns the transaction data when called from an EOA', async function () { + const { logs } = await this.context.msgData(integerValue, stringValue); + expectEvent.inLogs(logs, 'Data', { data: callData, integerValue, stringValue }); + }); + + it('returns the transaction sender when from another contract', async function () { + const { tx } = await this.caller.callData(this.context.address, integerValue, stringValue); + await expectEvent.inTransaction(tx, ContextMock, 'Data', { data: callData, integerValue, stringValue }); + }); + }); +} + +module.exports = { + shouldBehaveLikeRegularContext +}; diff --git a/test/drafts/meta-tx/Context.test.js b/test/drafts/meta-tx/Context.test.js index cb2d03003..8db3a94f1 100644 --- a/test/drafts/meta-tx/Context.test.js +++ b/test/drafts/meta-tx/Context.test.js @@ -1,44 +1,15 @@ -const { BN, expectEvent } = require('openzeppelin-test-helpers'); +require('openzeppelin-test-helpers'); const ContextMock = artifacts.require('ContextMock'); const ContextMockCaller = artifacts.require('ContextMockCaller'); +const { shouldBehaveLikeRegularContext } = require('./Context.behavior'); + contract('Context', function ([_, sender]) { beforeEach(async function () { this.context = await ContextMock.new(); this.caller = await ContextMockCaller.new(); }); - describe('msgSender', function () { - it('returns the transaction sender when called directly', async function () { - const { logs } = await this.context.msgSender({ from: sender }); - expectEvent.inLogs(logs, 'Sender', { sender }); - }); - - it('returns the transaction sender when from another contract', async function () { - const { tx } = await this.caller.callSender(this.context.address, { from: sender }); - await expectEvent.inTransaction(tx, ContextMock, 'Sender', { sender: this.caller.address }); - }); - }); - - describe('msgData', function () { - const integerValue = new BN('42'); - const stringValue = 'OpenZeppelin'; - - let callData; - - beforeEach(async function () { - callData = this.context.contract.methods.msgData(integerValue.toString(), stringValue).encodeABI(); - }); - - it('returns the transaction data when called directly', async function () { - const { logs } = await this.context.msgData(integerValue, stringValue); - expectEvent.inLogs(logs, 'Data', { data: callData, integerValue, stringValue }); - }); - - it('returns the transaction sender when from another contract', async function () { - const { tx } = await this.caller.callData(this.context.address, integerValue, stringValue); - await expectEvent.inTransaction(tx, ContextMock, 'Data', { data: callData, integerValue, stringValue }); - }); - }); + shouldBehaveLikeRegularContext(sender); }); diff --git a/test/drafts/meta-tx/GSNContext.test.js b/test/drafts/meta-tx/GSNContext.test.js new file mode 100644 index 000000000..2e7ddbb80 --- /dev/null +++ b/test/drafts/meta-tx/GSNContext.test.js @@ -0,0 +1,17 @@ +require('openzeppelin-test-helpers'); + +const GSNContextMock = artifacts.require('GSNContextMock'); +const ContextMockCaller = artifacts.require('ContextMockCaller'); + +const { shouldBehaveLikeRegularContext } = require('./Context.behavior'); + +contract('GSNContext', function ([_, sender, rhub]) { + beforeEach(async function () { + this.context = await GSNContextMock.new(rhub); + this.caller = await ContextMockCaller.new(); + }); + + context('when called directly', function () { + shouldBehaveLikeRegularContext(sender); + }); +});