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

Add batch delegate with sig ERC1271 #113

Closed
wants to merge 5 commits into from
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
19 changes: 19 additions & 0 deletions src/lib/interfaces/IERC1271.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (interfaces/IERC1271.sol)

pragma solidity ^0.8.0;

/**
* @dev Interface of the ERC1271 standard signature validation method for
* contracts as defined in https://eips.ethereum.org/EIPS/eip-1271[ERC-1271].
*
* _Available since v4.1._
*/
interface IERC1271 {
/**
* @dev Should return whether the signature provided is valid for the provided data
* @param hash Hash of the data to be signed
* @param signature Signature byte array associated with _data
*/
function isValidSignature(bytes32 hash, bytes memory signature) external view returns (bytes4 magicValue);
}
104 changes: 104 additions & 0 deletions src/lib/token/ERC721Votes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity 0.8.16;

import { IERC721Votes } from "../interfaces/IERC721Votes.sol";
import { IERC1271 } from "../interfaces/IERC1271.sol";
import { ERC721 } from "../token/ERC721.sol";
import { EIP712 } from "../utils/EIP712.sol";

Expand All @@ -20,6 +21,9 @@ abstract contract ERC721Votes is IERC721Votes, EIP712, ERC721 {
/// @dev The EIP-712 typehash to delegate with a signature
bytes32 internal constant DELEGATION_TYPEHASH = keccak256("Delegation(address from,address to,uint256 nonce,uint256 deadline)");

/// @dev The EIP-712 typehash to batch delegate with a signature
bytes32 internal constant BATCH_DELEGATION_TYPEHASH = keccak256("Delegation(address[] from,address[] to,uint256[] nonce,uint256[] deadline)");

/// ///
/// STORAGE ///
/// ///
Expand Down Expand Up @@ -53,6 +57,46 @@ abstract contract ERC721Votes is IERC721Votes, EIP712, ERC721 {
}
}

function getBatchDelegateBySigTypedDataHash(
address[] calldata _fromAddresses,
address[] calldata _toAddresses,
uint256[] calldata _deadlines
) public view returns (bytes32) {
uint256 length = _fromAddresses.length;

// Cannot realistically overflow
unchecked {
// Store nonces for each from address
uint256[] memory currentNonces = new uint256[](length);

for (uint256 i = 0; i < length; ++i) {
// Ensure the signature has not expired
if (block.timestamp > _deadlines[i]) revert EXPIRED_SIGNATURE();

// Add the addresses current nonce to the list of nonces
currentNonces[i] = nonces[_fromAddresses[i]];
}

// Compute the hash of the domain seperator with the typed delegation data
return
keccak256(
abi.encodePacked(
"\x19\x01",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@neokry openzeppelin has a nice lib for eip712 to verify this.

DOMAIN_SEPARATOR(),
keccak256(
abi.encode(
BATCH_DELEGATION_TYPEHASH,
keccak256(abi.encodePacked(_fromAddresses)),
keccak256(abi.encodePacked(_toAddresses)),
keccak256(abi.encodePacked(currentNonces)),
keccak256(abi.encodePacked(_deadlines))
)
)
)
);
}
}

/// @notice The number of votes for an account at a past timestamp
/// @param _account The account address
/// @param _timestamp The past timestamp
Expand Down Expand Up @@ -173,6 +217,66 @@ abstract contract ERC721Votes is IERC721Votes, EIP712, ERC721 {
_delegate(_from, _to);
}

function batchDelegateBySigERC1271(
address[] calldata _fromAddresses,
address[] calldata _toAddresses,
uint256[] calldata _deadlines,
bytes memory _signature
) external {
uint256 length = _fromAddresses.length;

// Used to store the digest
bytes32 digest;

// Cannot realistically overflow
unchecked {
// Store nonces for each from address
uint256[] memory currentNonces = new uint256[](length);

for (uint256 i = 0; i < length; ++i) {
// Ensure the signature has not expired
if (block.timestamp > _deadlines[i]) revert EXPIRED_SIGNATURE();

// Add the addresses current nonce to the list of nonces
currentNonces[i] = nonces[_fromAddresses[i]]++;
}

// Compute the hash of the domain seperator with the typed delegation data
digest = keccak256(
abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR(),
keccak256(
abi.encode(
BATCH_DELEGATION_TYPEHASH,
keccak256(abi.encodePacked(_fromAddresses)),
keccak256(abi.encodePacked(_toAddresses)),
keccak256(abi.encodePacked(currentNonces)),
keccak256(abi.encodePacked(_deadlines))
)
)
)
);

for (uint256 i = 0; i < length; ++i) {
address cachedFromAddress = _fromAddresses[i];

// Call the ERC1271 isValidSignature function
(bool success, bytes memory result) = cachedFromAddress.staticcall(
abi.encodeWithSelector(IERC1271.isValidSignature.selector, digest, _signature)
);

// Ensure the signature is valid
if (success && result.length >= 32 && abi.decode(result, (bytes32)) == bytes32(IERC1271.isValidSignature.selector)) {
// Update the delegate
_delegate(cachedFromAddress, _toAddresses[i]);
} else {
revert INVALID_SIGNATURE();
}
}
}
}

/// @dev Updates delegate addresses
/// @param _from The address delegating votes from
/// @param _to The address delegating votes to
Expand Down
175 changes: 175 additions & 0 deletions test/Token.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity 0.8.16;

import { NounsBuilderTest } from "./utils/NounsBuilderTest.sol";
import { MockERC1271 } from "./utils/mocks/MockERC1271.sol";

import { IManager, Manager } from "../src/manager/Manager.sol";
import { IToken, Token } from "../src/token/Token.sol";
Expand All @@ -11,8 +12,19 @@ import { TokenTypesV2 } from "../src/token/types/TokenTypesV2.sol";
contract TokenTest is NounsBuilderTest, TokenTypesV1 {
mapping(address => uint256) public mintedTokens;

address internal delegator;
uint256 internal delegatorPK;

function setUp() public virtual override {
super.setUp();
createDelegator();
}

function createDelegator() internal {
delegatorPK = 0xABE;
delegator = vm.addr(delegatorPK);

vm.deal(delegator, 100 ether);
}

function test_MockTokenInit() public {
Expand Down Expand Up @@ -808,4 +820,167 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 {
vm.expectRevert(abi.encodeWithSignature("INVALID_OWNER()"));
token.ownerOf(tokenId);
}

function test_BatchDelegateWithSig() public {
deployMock();

address[] memory fromAddresses = new address[](2);
fromAddresses[0] = address(new MockERC1271(delegator));
fromAddresses[1] = address(new MockERC1271(delegator));

address[] memory toAddresses = new address[](2);
toAddresses[0] = delegator;
toAddresses[1] = delegator;

uint256[] memory deadlines = new uint256[](2);
deadlines[0] = block.timestamp + 100;
deadlines[1] = block.timestamp + 100;

bytes32 digest = token.getBatchDelegateBySigTypedDataHash(fromAddresses, toAddresses, deadlines);

(uint8 v, bytes32 r, bytes32 s) = vm.sign(delegatorPK, digest);
bytes memory signature = abi.encodePacked(r, s, v);

vm.startPrank(address(auction));
token.mintTo(fromAddresses[0]);
token.mintTo(fromAddresses[1]);
vm.stopPrank();

assertEq(token.getVotes(delegator), 0);

token.batchDelegateBySigERC1271(fromAddresses, toAddresses, deadlines, signature);

assertEq(token.getVotes(delegator), 2);
}

function testRevert_BatchDelegateWithSigSignatureExpired() public {
deployMock();

address[] memory fromAddresses = new address[](2);
fromAddresses[0] = address(new MockERC1271(delegator));
fromAddresses[1] = address(new MockERC1271(delegator));

address[] memory toAddresses = new address[](2);
toAddresses[0] = delegator;
toAddresses[1] = delegator;

uint256[] memory deadlines = new uint256[](2);
deadlines[0] = block.timestamp + 100;
deadlines[1] = block.timestamp + 100;

bytes32 digest = token.getBatchDelegateBySigTypedDataHash(fromAddresses, toAddresses, deadlines);

(uint8 v, bytes32 r, bytes32 s) = vm.sign(delegatorPK, digest);
bytes memory signature = abi.encodePacked(r, s, v);

vm.startPrank(address(auction));
token.mintTo(fromAddresses[0]);
token.mintTo(fromAddresses[1]);
vm.stopPrank();

vm.warp(block.timestamp + 101);

assertEq(token.getVotes(delegator), 0);

vm.expectRevert(abi.encodeWithSignature("EXPIRED_SIGNATURE()"));
token.batchDelegateBySigERC1271(fromAddresses, toAddresses, deadlines, signature);

assertEq(token.getVotes(delegator), 0);
}

function test_BatchDelegateWithSigInvalidSignature() public {
deployMock();

address[] memory fromAddresses = new address[](2);
fromAddresses[0] = address(new MockERC1271(delegator));
fromAddresses[1] = address(new MockERC1271(delegator));

address[] memory toAddresses = new address[](2);
toAddresses[0] = delegator;
toAddresses[1] = delegator;

uint256[] memory deadlines = new uint256[](2);
deadlines[0] = block.timestamp + 100;
deadlines[1] = block.timestamp + 100;

bytes memory signature = new bytes(0);

vm.startPrank(address(auction));
token.mintTo(fromAddresses[0]);
token.mintTo(fromAddresses[1]);
vm.stopPrank();

assertEq(token.getVotes(delegator), 0);

vm.expectRevert(abi.encodeWithSignature("INVALID_SIGNATURE()"));
token.batchDelegateBySigERC1271(fromAddresses, toAddresses, deadlines, signature);

assertEq(token.getVotes(delegator), 0);
}

function test_BatchDelegateWithSigInvalidSignatureValidation() public {
deployMock();

address[] memory fromAddresses = new address[](2);
fromAddresses[0] = address(1);
fromAddresses[1] = address(new MockERC1271(delegator));

address[] memory toAddresses = new address[](2);
toAddresses[0] = delegator;
toAddresses[1] = delegator;

uint256[] memory deadlines = new uint256[](2);
deadlines[0] = block.timestamp + 100;
deadlines[1] = block.timestamp + 100;

bytes32 digest = token.getBatchDelegateBySigTypedDataHash(fromAddresses, toAddresses, deadlines);

(uint8 v, bytes32 r, bytes32 s) = vm.sign(delegatorPK, digest);
bytes memory signature = abi.encodePacked(r, s, v);

vm.startPrank(address(auction));
token.mintTo(fromAddresses[0]);
token.mintTo(fromAddresses[1]);
vm.stopPrank();

assertEq(token.getVotes(delegator), 0);

vm.expectRevert(abi.encodeWithSignature("INVALID_SIGNATURE()"));
token.batchDelegateBySigERC1271(fromAddresses, toAddresses, deadlines, signature);

assertEq(token.getVotes(delegator), 0);
}

function test_BatchDelegateWithSigOwnerNotDelegator() public {
deployMock();

address[] memory fromAddresses = new address[](2);
fromAddresses[0] = address(new MockERC1271(address(1)));
fromAddresses[1] = address(new MockERC1271(delegator));

address[] memory toAddresses = new address[](2);
toAddresses[0] = delegator;
toAddresses[1] = delegator;

uint256[] memory deadlines = new uint256[](2);
deadlines[0] = block.timestamp + 100;
deadlines[1] = block.timestamp + 100;

bytes32 digest = token.getBatchDelegateBySigTypedDataHash(fromAddresses, toAddresses, deadlines);

(uint8 v, bytes32 r, bytes32 s) = vm.sign(delegatorPK, digest);
bytes memory signature = abi.encodePacked(r, s, v);

vm.startPrank(address(auction));
token.mintTo(fromAddresses[0]);
token.mintTo(fromAddresses[1]);
vm.stopPrank();

assertEq(token.getVotes(delegator), 0);

vm.expectRevert(abi.encodeWithSignature("INVALID_SIGNATURE()"));
token.batchDelegateBySigERC1271(fromAddresses, toAddresses, deadlines, signature);

assertEq(token.getVotes(delegator), 0);
}
}
25 changes: 25 additions & 0 deletions test/utils/mocks/MockERC1271.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.16;

import { IERC1271 } from "../../../src/lib/interfaces/IERC1271.sol";
import { SignatureChecker } from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";

contract MockERC1271 is IERC1271 {
address owner;

constructor(address _owner) {
owner = _owner;
}

function isValidSignature(bytes32 hash, bytes memory signature) external view returns (bytes4 magicValue) {
bool isValid = SignatureChecker.isValidSignatureNow(owner, hash, signature);

if (isValid) {
return IERC1271.isValidSignature.selector;
}

return "";
}

receive() external payable {}
}
Loading