Skip to content

Commit

Permalink
Introduce SequentialOperations and ParallelOperations between EIP712 …
Browse files Browse the repository at this point in the history
…and signature-based operations like ERC20Permit, Votes, Governor, MinimalForwarder
  • Loading branch information
k06a committed Dec 2, 2022
1 parent 484a7ac commit 4f3422c
Show file tree
Hide file tree
Showing 12 changed files with 188 additions and 69 deletions.
27 changes: 10 additions & 17 deletions contracts/governance/Governor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ pragma solidity ^0.8.0;
import "../token/ERC721/IERC721Receiver.sol";
import "../token/ERC1155/IERC1155Receiver.sol";
import "../utils/cryptography/ECDSA.sol";
import "../utils/cryptography/EIP712.sol";
import "../utils/cryptography/ParallelOperations.sol";
import "../utils/introspection/ERC165.sol";
import "../utils/math/SafeCast.sol";
import "../utils/structs/DoubleEndedQueue.sol";
Expand All @@ -26,7 +26,7 @@ import "./IGovernor.sol";
*
* _Available since v4.3._
*/
abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receiver, IERC1155Receiver {
abstract contract Governor is Context, ERC165, ParallelOperations, IGovernor, IERC721Receiver, IERC1155Receiver {
using DoubleEndedQueue for DoubleEndedQueue.Bytes32Deque;
using SafeCast for uint256;
using Timers for Timers.BlockNumber;
Expand Down Expand Up @@ -450,8 +450,10 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive
bytes32 r,
bytes32 s
) public virtual override returns (uint256) {
address voter = ECDSA.recover(
_hashTypedDataV4(keccak256(abi.encode(BALLOT_TYPEHASH, proposalId, support))),
address voter = _validateParallelOperation(
BALLOT_TYPEHASH,
keccak256(abi.encode(BALLOT_TYPEHASH, proposalId, support)),
proposalId, // use proposalId as nonce
v,
r,
s
Expand All @@ -471,23 +473,14 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive
bytes32 r,
bytes32 s
) public virtual override returns (uint256) {
address voter = ECDSA.recover(
_hashTypedDataV4(
keccak256(
abi.encode(
EXTENDED_BALLOT_TYPEHASH,
proposalId,
support,
keccak256(bytes(reason)),
keccak256(params)
)
)
),
address voter = _validateParallelOperation(
EXTENDED_BALLOT_TYPEHASH,
keccak256(abi.encode(EXTENDED_BALLOT_TYPEHASH, proposalId, support, keccak256(bytes(reason)), keccak256(params))),
proposalId, // use proposalId as nonce
v,
r,
s
);

return _castVote(proposalId, voter, support, reason, params);
}

Expand Down
15 changes: 7 additions & 8 deletions contracts/governance/utils/Votes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ pragma solidity ^0.8.0;
import "../../utils/Context.sol";
import "../../utils/Nonces.sol";
import "../../utils/Checkpoints.sol";
import "../../utils/cryptography/EIP712.sol";
import "../../utils/cryptography/SequentialOperations.sol";
import "./IVotes.sol";
import "../../utils/math/SafeCast.sol";

Expand All @@ -29,17 +29,15 @@ import "../../utils/math/SafeCast.sol";
*
* _Available since v4.5._
*/
abstract contract Votes is IVotes, Context, EIP712 {
abstract contract Votes is IVotes, Context, SequentialOperations {
using Checkpoints for Checkpoints.History;
using Nonces for Nonces.Data;

bytes32 private constant _DELEGATION_TYPEHASH =
keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)");

mapping(address => address) private _delegation;
mapping(address => Checkpoints.History) private _delegateCheckpoints;
Checkpoints.History private _totalCheckpoints;
Nonces.Data private _nonces;

/**
* @dev Returns the current amount of votes that `account` has.
Expand Down Expand Up @@ -86,7 +84,7 @@ abstract contract Votes is IVotes, Context, EIP712 {
* @dev Returns the delegation nonce for `owner`.
*/
function delegationNonces(address owner) public view virtual override returns (uint256) {
return _nonces.nonces(owner);
return operationNonces(_DELEGATION_TYPEHASH, owner);
}

/**
Expand Down Expand Up @@ -116,13 +114,14 @@ abstract contract Votes is IVotes, Context, EIP712 {
bytes32 s
) public virtual override {
require(block.timestamp <= expiry, "Votes: signature expired");
address signer = ECDSA.recover(
_hashTypedDataV4(keccak256(abi.encode(_DELEGATION_TYPEHASH, delegatee, nonce, expiry))),
address signer = _validateSequentialOperation(
_DELEGATION_TYPEHASH,
keccak256(abi.encode(_DELEGATION_TYPEHASH, delegatee, nonce, expiry)),
nonce,
v,
r,
s
);
require(nonce == _nonces.useNonce(signer), "Votes: invalid nonce");
_delegate(signer, delegatee);
}

Expand Down
24 changes: 12 additions & 12 deletions contracts/metatx/MinimalForwarder.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
pragma solidity ^0.8.0;

import "../utils/cryptography/ECDSA.sol";
import "../utils/cryptography/EIP712.sol";
import "../utils/cryptography/SequentialOperations.sol";

/**
* @dev Simple minimal forwarder to be used together with an ERC2771 compatible contract. See {ERC2771Context}.
Expand All @@ -14,7 +14,7 @@ import "../utils/cryptography/EIP712.sol";
* functioning forwarding system with good properties requires more complexity. We suggest you look at other projects
* such as the GSN which do have the goal of building a system like that.
*/
contract MinimalForwarder is EIP712 {
contract MinimalForwarder is SequentialOperations {
using ECDSA for bytes32;

struct ForwardRequest {
Expand All @@ -29,19 +29,20 @@ contract MinimalForwarder is EIP712 {
bytes32 private constant _TYPEHASH =
keccak256("ForwardRequest(address from,address to,uint256 value,uint256 gas,uint256 nonce,bytes data)");

mapping(address => uint256) private _nonces;

constructor() EIP712("MinimalForwarder", "0.0.1") {}

function getNonce(address from) public view returns (uint256) {
return _nonces[from];
return operationNonces(_TYPEHASH, from);
}

function verify(ForwardRequest calldata req, bytes calldata signature) public view returns (bool) {
address signer = _hashTypedDataV4(
keccak256(abi.encode(_TYPEHASH, req.from, req.to, req.value, req.gas, req.nonce, keccak256(req.data)))
).recover(signature);
return _nonces[req.from] == req.nonce && signer == req.from;
function verify(ForwardRequest calldata req, bytes calldata signature) public returns (bool) {
address signer = _validateSequentialOperation(
_TYPEHASH,
keccak256(abi.encode(_TYPEHASH, req.from, req.to, req.value, req.gas, req.nonce, keccak256(req.data))),
req.nonce,
signature
);
return signer == req.from;
}

function execute(ForwardRequest calldata req, bytes calldata signature)
Expand All @@ -50,8 +51,7 @@ contract MinimalForwarder is EIP712 {
returns (bool, bytes memory)
{
require(verify(req, signature), "MinimalForwarder: signature does not match request");
_nonces[req.from] = req.nonce + 1;


(bool success, bytes memory returndata) = req.to.call{gas: req.gas, value: req.value}(
abi.encodePacked(req.data, req.from)
);
Expand Down
27 changes: 13 additions & 14 deletions contracts/token/ERC20/extensions/ERC20Permit.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ pragma solidity ^0.8.0;
import "./IERC20Permit.sol";
import "../ERC20.sol";
import "../../../utils/cryptography/ECDSA.sol";
import "../../../utils/cryptography/EIP712.sol";
import "../../../utils/cryptography/SequentialOperations.sol";
import "../../../utils/Nonces.sol";

/**
Expand All @@ -19,9 +19,7 @@ import "../../../utils/Nonces.sol";
*
* _Available since v3.4._
*/
abstract contract ERC20Permit is ERC20, IERC20Permit, EIP712 {
using Nonces for Nonces.Data;

abstract contract ERC20Permit is ERC20, IERC20Permit, SequentialOperations {
// solhint-disable-next-line var-name-mixedcase
bytes32 private constant _PERMIT_TYPEHASH =
keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
Expand All @@ -33,7 +31,6 @@ abstract contract ERC20Permit is ERC20, IERC20Permit, EIP712 {
*/
// solhint-disable-next-line var-name-mixedcase
bytes32 private _PERMIT_TYPEHASH_DEPRECATED_SLOT;
Nonces.Data private _nonces;

/**
* @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`.
Expand All @@ -55,22 +52,24 @@ abstract contract ERC20Permit is ERC20, IERC20Permit, EIP712 {
bytes32 s
) public virtual override {
require(block.timestamp <= deadline, "ERC20Permit: expired deadline");

bytes32 structHash = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, _nonces.useNonce(owner), deadline));

bytes32 hash = _hashTypedDataV4(structHash);

address signer = ECDSA.recover(hash, v, r, s);
require(signer == owner, "ERC20Permit: invalid signature");

uint256 nonce = operationNonces(_PERMIT_TYPEHASH, owner);
address signer = _validateSequentialOperation(
_PERMIT_TYPEHASH,
keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, nonce, deadline)),
nonce,
v,
r,
s
);
require(owner == signer, "ERC20Permit: invalid signature");
_approve(owner, spender, value);
}

/**
* @dev See {IERC20Permit-nonces}.
*/
function nonces(address owner) public view virtual override returns (uint256) {
return _nonces.nonces(owner);
return operationNonces(_PERMIT_TYPEHASH, owner);
}

/**
Expand Down
64 changes: 64 additions & 0 deletions contracts/utils/cryptography/ParallelOperations.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "../Context.sol";
import "../Nonces.sol";
import "./EIP712.sol";
import "./ECDSA.sol";

/**
* @dev Provides tracking nonces per address and operation. Operation ids should be unique.
*/
abstract contract ParallelOperations is Context, EIP712 {
mapping(bytes32 => mapping(address => mapping(uint256 => bool))) private _usedOperationIds;

function isOperationIdAvailable(bytes32 operationTypehash, address owner, uint256 operationId) public view virtual returns (bool) {
return !_usedOperationIds[operationTypehash][owner][operationId];
}

function useOperationId(bytes32 operationTypehash, uint256 operationId) public virtual {
_useOperationId(operationTypehash, _msgSender(), operationId);
}

function _validateParallelOperation(
bytes32 operationTypehash,
bytes32 operationHash,
uint256 operationId,
bytes memory signature
) internal virtual returns(address signer) {
signer = ECDSA.recover(_hashTypedDataV4(operationHash), signature);
_useOperationId(operationTypehash, signer, operationId);
}

function _validateParallelOperation(
bytes32 operationTypehash,
bytes32 operationHash,
uint256 operationId,
uint8 v,
bytes32 r,
bytes32 s
) internal virtual returns(address signer) {
signer = ECDSA.recover(_hashTypedDataV4(operationHash), v, r, s);
_useOperationId(operationTypehash, signer, operationId);
}

function _validateParallelOperation(
bytes32 operationTypehash,
bytes32 operationHash,
uint256 operationId,
bytes32 r,
bytes32 vs
) internal virtual returns(address signer) {
signer = ECDSA.recover(_hashTypedDataV4(operationHash), r, vs);
_useOperationId(operationTypehash, signer, operationId);
}

/// @dev Method made non-virtual to deny changing logic of parallel operations invalidation.
function _useOperationId(bytes32 operationTypehash, address owner, uint256 operationId) internal {
require(
!_usedOperationIds[operationTypehash][owner][operationId],
"ParallelOperations: invalid operation id"
);
_usedOperationIds[operationTypehash][owner][operationId] = true;
}
}
62 changes: 62 additions & 0 deletions contracts/utils/cryptography/SequentialOperations.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "../Context.sol";
import "../Nonces.sol";
import "./EIP712.sol";
import "./ECDSA.sol";

/**
* @dev Provides tracking nonces per address and operation. Nonces will only increment.
*/
abstract contract SequentialOperations is Context, EIP712 {
using Nonces for Nonces.Data;

mapping(bytes32 => Nonces.Data) private _nonces;

function operationNonces(bytes32 operationTypehash, address owner) public view virtual returns (uint256) {
return _nonces[operationTypehash].nonces(owner);
}

function useOperationNonce(bytes32 operationTypehash, uint256 nonce) public virtual {
_useOperationNonce(operationTypehash, _msgSender(), nonce);
}

function _validateSequentialOperation(
bytes32 operationTypehash,
bytes32 operationHash,
uint256 nonce,
bytes memory signature
) internal virtual returns(address signer) {
signer = ECDSA.recover(_hashTypedDataV4(operationHash), signature);
_useOperationNonce(operationTypehash, signer, nonce);
}

function _validateSequentialOperation(
bytes32 operationTypehash,
bytes32 operationHash,
uint256 nonce,
uint8 v,
bytes32 r,
bytes32 s
) internal virtual returns(address signer) {
signer = ECDSA.recover(_hashTypedDataV4(operationHash), v, r, s);
_useOperationNonce(operationTypehash, signer, nonce);
}

function _validateSequentialOperation(
bytes32 operationTypehash,
bytes32 operationHash,
uint256 nonce,
bytes32 r,
bytes32 vs
) internal virtual returns(address signer) {
signer = ECDSA.recover(_hashTypedDataV4(operationHash), r, vs);
_useOperationNonce(operationTypehash, signer, nonce);
}

/// @dev Method made non-virtual to deny changing logic of sequential operations invalidation.
function _useOperationNonce(bytes32 operationTypehash, address owner, uint256 nonce) internal {
require(nonce == _nonces[operationTypehash].useNonce(owner), "SequentialOperations: invalid nonce");
}
}
4 changes: 2 additions & 2 deletions test/governance/utils/Votes.behavior.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ function shouldBehaveLikeVotes () {

await expectRevert(
this.votes.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s),
'Votes: invalid nonce',
'SequentialOperations: invalid nonce',
);
});

Expand Down Expand Up @@ -127,7 +127,7 @@ function shouldBehaveLikeVotes () {
));
await expectRevert(
this.votes.delegateBySig(delegatorAddress, nonce + 1, MAX_UINT256, v, r, s),
'Votes: invalid nonce',
'SequentialOperations: invalid nonce',
);
});

Expand Down
4 changes: 2 additions & 2 deletions test/metatx/ERC2771Context.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ contract('ERC2771Context', function (accounts) {
};

const sign = ethSigUtil.signTypedMessage(this.wallet.getPrivateKey(), { data: { ...this.data, message: req } });
expect(await this.forwarder.verify(req, sign)).to.equal(true);
expect(await this.forwarder.contract.methods.verify(req, sign).call()).to.equal(true);

const { tx } = await this.forwarder.execute(req, sign);
await expectEvent.inTransaction(tx, ERC2771ContextMock, 'Sender', { sender: this.sender });
Expand All @@ -99,7 +99,7 @@ contract('ERC2771Context', function (accounts) {
};

const sign = ethSigUtil.signTypedMessage(this.wallet.getPrivateKey(), { data: { ...this.data, message: req } });
expect(await this.forwarder.verify(req, sign)).to.equal(true);
expect(await this.forwarder.contract.methods.verify(req, sign).call()).to.equal(true);

const { tx } = await this.forwarder.execute(req, sign);
await expectEvent.inTransaction(tx, ERC2771ContextMock, 'Data', { data, integerValue, stringValue });
Expand Down

0 comments on commit 4f3422c

Please sign in to comment.