Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use invalid opcode to consume all gas in MinimalForwarder #3035

Merged
merged 3 commits into from
Dec 22, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion contracts/metatx/MinimalForwarder.sol
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,17 @@ contract MinimalForwarder is EIP712 {
(bool success, bytes memory returndata) = req.to.call{gas: req.gas, value: req.value}(
abi.encodePacked(req.data, req.from)
);

// Validate that the relayer has sent enough gas for the call.
// See https://ronan.eth.link/blog/ethereum-gas-dangers/
assert(gasleft() > req.gas / 63);
if (gasleft() <= req.gas / 63) {
// We explicitly trigger invalid opcode to consume all gas and bubble-up the effects, since
// Panic error do not consume all gas since Solidity 0.8.0
Amxx marked this conversation as resolved.
Show resolved Hide resolved
// https://docs.soliditylang.org/en/v0.8.0/control-structures.html#panic-via-assert-and-error-via-require
assembly {
invalid()
}
}

return (success, returndata);
}
Expand Down
48 changes: 33 additions & 15 deletions test/metatx/MinimalForwarder.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const { expectRevert, constants } = require('@openzeppelin/test-helpers');
const { expect } = require('chai');

const MinimalForwarder = artifacts.require('MinimalForwarder');
const CallReceiverMock = artifacts.require('CallReceiverMock');

const name = 'MinimalForwarder';
const version = '0.0.1';
Expand Down Expand Up @@ -44,7 +45,7 @@ contract('MinimalForwarder', function (accounts) {
nonce: Number(await this.forwarder.getNonce(this.sender)),
data: '0x',
};
this.sign = ethSigUtil.signTypedMessage(
this.sign = () => ethSigUtil.signTypedMessage(
this.wallet.getPrivateKey(),
{
data: {
Expand All @@ -65,7 +66,7 @@ contract('MinimalForwarder', function (accounts) {
});

it('success', async function () {
expect(await this.forwarder.verify(this.req, this.sign)).to.be.equal(true);
expect(await this.forwarder.verify(this.req, this.sign())).to.be.equal(true);
});

afterEach(async function () {
Expand All @@ -76,27 +77,27 @@ contract('MinimalForwarder', function (accounts) {

context('invalid signature', function () {
it('tampered from', async function () {
expect(await this.forwarder.verify({ ...this.req, from: accounts[0] }, this.sign))
expect(await this.forwarder.verify({ ...this.req, from: accounts[0] }, this.sign()))
.to.be.equal(false);
});
it('tampered to', async function () {
expect(await this.forwarder.verify({ ...this.req, to: accounts[0] }, this.sign))
expect(await this.forwarder.verify({ ...this.req, to: accounts[0] }, this.sign()))
.to.be.equal(false);
});
it('tampered value', async function () {
expect(await this.forwarder.verify({ ...this.req, value: web3.utils.toWei('1') }, this.sign))
expect(await this.forwarder.verify({ ...this.req, value: web3.utils.toWei('1') }, this.sign()))
.to.be.equal(false);
});
it('tampered nonce', async function () {
expect(await this.forwarder.verify({ ...this.req, nonce: this.req.nonce + 1 }, this.sign))
expect(await this.forwarder.verify({ ...this.req, nonce: this.req.nonce + 1 }, this.sign()))
.to.be.equal(false);
});
it('tampered data', async function () {
expect(await this.forwarder.verify({ ...this.req, data: '0x1742' }, this.sign))
expect(await this.forwarder.verify({ ...this.req, data: '0x1742' }, this.sign()))
.to.be.equal(false);
});
it('tampered signature', async function () {
const tamperedsign = web3.utils.hexToBytes(this.sign);
const tamperedsign = web3.utils.hexToBytes(this.sign());
tamperedsign[42] ^= 0xff;
expect(await this.forwarder.verify(this.req, web3.utils.bytesToHex(tamperedsign)))
.to.be.equal(false);
Expand All @@ -112,7 +113,7 @@ contract('MinimalForwarder', function (accounts) {
});

it('success', async function () {
await this.forwarder.execute(this.req, this.sign); // expect to not revert
await this.forwarder.execute(this.req, this.sign()); // expect to not revert
});

afterEach(async function () {
Expand All @@ -124,43 +125,60 @@ contract('MinimalForwarder', function (accounts) {
context('invalid signature', function () {
it('tampered from', async function () {
await expectRevert(
this.forwarder.execute({ ...this.req, from: accounts[0] }, this.sign),
this.forwarder.execute({ ...this.req, from: accounts[0] }, this.sign()),
'MinimalForwarder: signature does not match request',
);
});
it('tampered to', async function () {
await expectRevert(
this.forwarder.execute({ ...this.req, to: accounts[0] }, this.sign),
this.forwarder.execute({ ...this.req, to: accounts[0] }, this.sign()),
'MinimalForwarder: signature does not match request',
);
});
it('tampered value', async function () {
await expectRevert(
this.forwarder.execute({ ...this.req, value: web3.utils.toWei('1') }, this.sign),
this.forwarder.execute({ ...this.req, value: web3.utils.toWei('1') }, this.sign()),
'MinimalForwarder: signature does not match request',
);
});
it('tampered nonce', async function () {
await expectRevert(
this.forwarder.execute({ ...this.req, nonce: this.req.nonce + 1 }, this.sign),
this.forwarder.execute({ ...this.req, nonce: this.req.nonce + 1 }, this.sign()),
'MinimalForwarder: signature does not match request',
);
});
it('tampered data', async function () {
await expectRevert(
this.forwarder.execute({ ...this.req, data: '0x1742' }, this.sign),
this.forwarder.execute({ ...this.req, data: '0x1742' }, this.sign()),
'MinimalForwarder: signature does not match request',
);
});
it('tampered signature', async function () {
const tamperedsign = web3.utils.hexToBytes(this.sign);
const tamperedsign = web3.utils.hexToBytes(this.sign());
tamperedsign[42] ^= 0xff;
await expectRevert(
this.forwarder.execute(this.req, web3.utils.bytesToHex(tamperedsign)),
'MinimalForwarder: signature does not match request',
);
});
});

it('bubble out of gas', async function () {
const receiver = await CallReceiverMock.new();
const gasAvailable = 100000;
this.req.to = receiver.address;
this.req.data = receiver.contract.methods.mockFunctionOutOfGas().encodeABI();
this.req.gas = 1000000;

await expectRevert.assertion(
this.forwarder.execute(this.req, this.sign(), { gas: gasAvailable }),
);

const { gasUsed } = await web3.eth.getBlock('latest')
.then(({ transactions }) => web3.eth.getTransactionReceipt(transactions.find(Boolean)));
Amxx marked this conversation as resolved.
Show resolved Hide resolved

expect(gasUsed).to.be.equal(gasAvailable);
});
});
});
});