Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions contracts/governance/utils/IVotes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ interface IVotes {
*/
function getPastTotalSupply(uint256 blockNumber) external view returns (uint256);

/**
* @dev Returns the delegation nonce for `owner`.
*/
function delegationNonces(address owner) external view returns (uint256);

/**
* @dev Returns the delegate that `account` has chosen.
*/
Expand Down
18 changes: 13 additions & 5 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/SignatureOperations.sol";
import "./IVotes.sol";
import "../../utils/math/SafeCast.sol";

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

bytes32 private constant _DELEGATION_TYPEHASH =
Expand Down Expand Up @@ -80,6 +80,13 @@ abstract contract Votes is IVotes, Context, EIP712, Nonces {
return _totalCheckpoints.latest();
}

/**
* @dev Returns the delegation nonce for `owner`.
*/
function delegationNonces(address owner) public view virtual override returns (uint256) {
return operationNonces(_DELEGATION_TYPEHASH, owner);
}

/**
* @dev Returns the delegate that `account` has chosen.
*/
Expand Down Expand Up @@ -107,13 +114,14 @@ abstract contract Votes is IVotes, Context, EIP712, Nonces {
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 == _useNonce(signer), "Votes: invalid nonce");
_delegate(signer, delegatee);
}

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

/**
* @dev Provides tracking nonces per address and operation. Nonces will only increment.
*/
interface ISignatureOperations {
/// @dev Returns next nonce for the signer in the context of the operation typehash and operation beneficiary
/// @param typehash The operation typehash
/// @param signer The signer address
function operationNonces(bytes32 typehash, address signer) external view returns (uint256);

/// @dev Returns next id for the signer in the context of the operation typehash and operation beneficiary
/// @param typehash The operation typehash
/// @param signer The signer address
/// @param beneficiary The address of the spender, delegate, or other beneficiary of the transaction
function operationIds(bytes32 typehash, address signer, address beneficiary) external view returns (uint256);

/// @dev Increments nonce for the caller in the context of the operation typehash and operation beneficiary
/// @param typehash The operation typehash
/// @param nonce The operation nonce
function useOperationNonce(bytes32 typehash, uint256 nonce) external;

/// @dev Increments id for the caller in the context of the operation typehash and operation beneficiary
/// @param typehash The operation typehash
/// @param beneficiary The address of the spender, delegate, or other beneficiary of the transaction
/// @param id The operation nonce
function useOperationIds(bytes32 typehash, address beneficiary, uint256 id) external;
}
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/SignatureOperations.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 SignatureOperations {
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
12 changes: 10 additions & 2 deletions contracts/mocks/NoncesImpl.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,16 @@ pragma solidity ^0.8.0;

import "../utils/Nonces.sol";

contract NoncesImpl is Nonces {
contract NoncesImpl {
using Nonces for Nonces.Data;

Nonces.Data private _nonces;

function nonces(address owner) public view returns (uint256) {
return _nonces.nonces(owner);
}

function useNonce(address owner) public {
super._useNonce(owner);
_nonces.useNonce(owner);
}
}
26 changes: 14 additions & 12 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/SignatureOperations.sol";
import "../../../utils/Nonces.sol";

/**
Expand All @@ -19,7 +19,7 @@ import "../../../utils/Nonces.sol";
*
* _Available since v3.4._
*/
abstract contract ERC20Permit is ERC20, IERC20Permit, EIP712, Nonces {
abstract contract ERC20Permit is ERC20, IERC20Permit, SignatureOperations {
// 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 Down Expand Up @@ -52,22 +52,24 @@ abstract contract ERC20Permit is ERC20, IERC20Permit, EIP712, Nonces {
bytes32 s
) public virtual override {
require(block.timestamp <= deadline, "ERC20Permit: expired deadline");

bytes32 structHash = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, _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(IERC20Permit, Nonces) returns (uint256) {
return super.nonces(owner);
function nonces(address owner) public view virtual override returns (uint256) {
return operationNonces(_PERMIT_TYPEHASH, owner);
}

/**
Expand Down
14 changes: 8 additions & 6 deletions contracts/utils/Nonces.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,27 @@ import "./Counters.sol";
/**
* @dev Provides tracking nonces for addresses. Nonces will only increment.
*/
abstract contract Nonces {
library Nonces {
using Counters for Counters.Counter;

mapping(address => Counters.Counter) private _nonces;
struct Data {
mapping(address => Counters.Counter) _nonces;
}

/**
* @dev Returns an address nonce.
*/
function nonces(address owner) public view virtual returns (uint256) {
return _nonces[owner].current();
function nonces(Data storage self, address owner) internal view returns (uint256) {
return self._nonces[owner].current();
}

/**
* @dev Consumes a nonce.
*
* Returns the current value and increments nonce.
*/
function _useNonce(address owner) internal virtual returns (uint256 current) {
Counters.Counter storage nonce = _nonces[owner];
function useNonce(Data storage self, address owner) internal returns (uint256 current) {
Counters.Counter storage nonce = self._nonces[owner];
current = nonce.current();
nonce.increment();
}
Expand Down
113 changes: 113 additions & 0 deletions contracts/utils/cryptography/SignatureOperations.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "../../interfaces/ISignatureOperations.sol";
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 SignatureOperations is Context, EIP712, ISignatureOperations {
using Nonces for Nonces.Data;

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

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

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

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

function useOperationIds(bytes32 operationTypehash, address beneficiary, uint256 nonce) public virtual {
_useOperationIds(operationTypehash, _msgSender(), beneficiary, 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);
}

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

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

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

/// @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), "SignatureOperations: invalid nonce");
}

/// @dev Method made non-virtual to deny changing logic of sequential operations invalidation.
function _useOperationIds(bytes32 operationTypehash, address owner, address beneficiary, uint256 id) internal {
require(id == _ids[operationTypehash][beneficiary].useNonce(owner), "SignatureOperations: invalid id");
}
}
6 changes: 3 additions & 3 deletions test/governance/utils/Votes.behavior.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const version = '1';
function shouldBehaveLikeVotes () {
describe('run votes workflow', function () {
it('initial nonce is 0', async function () {
expect(await this.votes.nonces(this.account1)).to.be.bignumber.equal('0');
expect(await this.votes.delegationNonces(this.account1)).to.be.bignumber.equal('0');
});

it('domain separator', async function () {
Expand Down Expand Up @@ -95,7 +95,7 @@ function shouldBehaveLikeVotes () {

await expectRevert(
this.votes.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s),
'Votes: invalid nonce',
'SignatureOperations: 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',
'SignatureOperations: invalid nonce',
);
});

Expand Down
Loading