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

💥 Implement 0x00 Version of EIP-191 in ECDSA Library #81

Merged
merged 5 commits into from
Feb 22, 2023
Merged
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
196 changes: 100 additions & 96 deletions .gas-snapshot

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion lib/create-util
Submodule create-util updated 2 files
+353 −1,399 package-lock.json
+4 −4 package.json
2 changes: 1 addition & 1 deletion lib/forge-std
Submodule forge-std updated 1 files
+20 −20 src/StdAssertions.sol
2 changes: 1 addition & 1 deletion lib/prb-test
Submodule prb-test updated 1 files
+6 −1 .solhint.json
121 changes: 81 additions & 40 deletions src/utils/ECDSA.vy
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
@license GNU Affero General Public License v3.0
@author pcaversaccio
@notice These functions can be used to verify that a message was signed
by the holder of the private key of a given address. The implementation
is inspired by OpenZeppelin's implementation here:
by the holder of the private key of a given address. Additionally,
we provide helper functions to handle signed data in Ethereum
contracts based on EIP-191: https://eips.ethereum.org/EIPS/eip-191.
The implementation is inspired by OpenZeppelin's implementation here:
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/ECDSA.sol.
@custom:security Signatures must not be used as unique identifiers since the
`ecrecover` opcode allows for malleable (non-unique) signatures.
Expand All @@ -16,6 +18,12 @@ _MALLEABILITY_THRESHOLD: constant(bytes32) = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5
_SIGNATURE_INCREMENT: constant(bytes32) = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF


# @dev A Vyper contract cannot call directly between two `external` functions.
# To bypass this, we can use an interface.
interface IECDSA:
def to_data_with_intended_validator_hash(validator: address, data: Bytes[1024]) -> bytes32: pure


@external
@payable
def __init__():
Expand Down Expand Up @@ -65,6 +73,77 @@ def recover_sig(hash: bytes32, signature: Bytes[65]) -> address:
return empty(address)


@external
@pure
def to_eth_signed_message_hash(hash: bytes32) -> bytes32:
"""
@dev Returns an Ethereum signed message from a 32-byte
message digest `hash`.
@notice This function returns a 32-byte hash that
corresponds to the one signed with the JSON-RPC method:
https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_sign.
This method is part of EIP-191:
https://eips.ethereum.org/EIPS/eip-191.
@param hash The 32-byte message digest.
@return bytes32 The 32-byte Ethereum signed message.
"""
return keccak256(concat(b"\x19Ethereum Signed Message:\n32", hash))


@external
@pure
def to_typed_data_hash(domain_separator: bytes32, struct_hash: bytes32) -> bytes32:
"""
@dev Returns an Ethereum signed typed data from a 32-byte
`domain_separator` and a 32-byte `struct_hash`.
@notice This function returns a 32-byte hash that
corresponds to the one signed with the JSON-RPC method:
https://eips.ethereum.org/EIPS/eip-712#specification-of-the-eth_signtypeddata-json-rpc.
This method is part of EIP-712:
https://eips.ethereum.org/EIPS/eip-712.
@param domain_separator The 32-byte domain separator that is
used as part of the EIP-712 encoding scheme.
@param struct_hash The 32-byte struct hash that is used as
part of the EIP-712 encoding scheme. See the definition:
https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct.
@return bytes32 The 32-byte Ethereum signed typed data.
"""
return keccak256(concat(b"\x19\x01", domain_separator, struct_hash))


@external
@view
def to_data_with_intended_validator_hash_self(data: Bytes[1024]) -> bytes32:
"""
@dev Returns an Ethereum signed data with this contract
as the intended validator and a maximum 1024-byte
payload `data`.
@notice This function structures the data according to
the version `0x00` of EIP-191:
https://eips.ethereum.org/EIPS/eip-191#version-0x00.
@param data The maximum 1024-byte data to be signed.
@return bytes32 The 32-byte Ethereum signed data.
"""
return IECDSA(self).to_data_with_intended_validator_hash(self, data)


@external
@pure
def to_data_with_intended_validator_hash(validator: address, data: Bytes[1024]) -> bytes32:
"""
@dev Returns an Ethereum signed data with `validator` as
the intended validator and a maximum 1024-byte payload
`data`.
@notice This function structures the data according to
the version `0x00` of EIP-191:
https://eips.ethereum.org/EIPS/eip-191#version-0x00.
@param validator The 20-byte intended validator address.
@param data The maximum 1024-byte data to be signed.
@return bytes32 The 32-byte Ethereum signed data.
"""
return keccak256(concat(b"\x19\x00", convert(validator, bytes20), data))


@internal
@pure
def _recover_vrs(hash: bytes32, v: uint256, r: uint256, s: uint256) -> address:
Expand Down Expand Up @@ -126,41 +205,3 @@ def _try_recover_vrs(hash: bytes32, v: uint256, r: uint256, s: uint256) -> addre
raise "ECDSA: invalid signature"

return signer


@external
@pure
def to_eth_signed_message_hash(hash: bytes32) -> bytes32:
"""
@dev Returns an Ethereum signed message from a 32-byte
message digest `hash`.
@notice This function returns a 32-byte hash that
corresponds to the one signed with the JSON-RPC method:
https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_sign.
This method is part of EIP-191:
https://eips.ethereum.org/EIPS/eip-191.
@param hash The 32-byte message digest.
@return bytes32 The 32-byte Ethereum signed message.
"""
return keccak256(concat(b"\x19Ethereum Signed Message:\n32", hash))


@external
@pure
def to_typed_data_hash(domain_separator: bytes32, struct_hash: bytes32) -> bytes32:
"""
@dev Returns an Ethereum signed typed data from a 32-byte
`domain_separator` and a 32-byte `struct_hash`.
@notice This function returns a 32-byte hash that
corresponds to the one signed with the JSON-RPC method:
https://eips.ethereum.org/EIPS/eip-712#specification-of-the-eth_signtypeddata-json-rpc.
This method is part of EIP-712:
https://eips.ethereum.org/EIPS/eip-712.
@param domain_separator The 32-byte domain separator that is
used as part of the EIP-712 encoding scheme.
@param struct_hash The 32-byte struct hash that is used as
part of the EIP-712 encoding scheme. See the definition:
https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct.
@return bytes32 The 32-byte Ethereum signed typed data.
"""
return keccak256(concat(b"\x19\x01", domain_separator, struct_hash))
49 changes: 49 additions & 0 deletions test/utils/ECDSA.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ contract ECDSATest is Test {

address private self = address(this);
address private zeroAddress = address(0);
// solhint-disable-next-line var-name-mixedcase
address private ECDSAAddr;

/**
* @dev Transforms a standard signature into an EIP-2098
Expand All @@ -50,6 +52,7 @@ contract ECDSATest is Test {

function setUp() public {
ECDSA = IECDSA(vyperDeployer.deployContract("src/utils/", "ECDSA"));
ECDSAAddr = address(ECDSA);
}

function testRecoverWithValidSignature() public {
Expand Down Expand Up @@ -252,6 +255,28 @@ contract ECDSATest is Test {
assertEq(digest1, digest2);
}

function testToDataWithIntendedValidatorHash() public {
address validator = makeAddr("intendedValidator");
bytes memory data = new bytes(42);
bytes32 digest1 = ECDSA.to_data_with_intended_validator_hash(
validator,
data
);
bytes32 digest2 = keccak256(
abi.encodePacked("\x19\x00", validator, data)
);
assertEq(digest1, digest2);
}

function testToDataWithIntendedValidatorHashSelf() public {
bytes memory data = new bytes(42);
bytes32 digest1 = ECDSA.to_data_with_intended_validator_hash_self(data);
bytes32 digest2 = keccak256(
abi.encodePacked("\x19\x00", ECDSAAddr, data)
);
assertEq(digest1, digest2);
}

function testFuzzRecoverWithValidSignature(
string calldata signer,
string calldata message
Expand Down Expand Up @@ -357,4 +382,28 @@ contract ECDSATest is Test {
);
assertEq(digest1, digest2);
}

function testFuzzToDataWithIntendedValidatorHash(
address validator,
bytes calldata data
) public {
bytes32 digest1 = ECDSA.to_data_with_intended_validator_hash(
validator,
data
);
bytes32 digest2 = keccak256(
abi.encodePacked("\x19\x00", validator, data)
);
assertEq(digest1, digest2);
}

function testFuzzToDataWithIntendedValidatorHashSelf(
bytes calldata data
) public {
bytes32 digest1 = ECDSA.to_data_with_intended_validator_hash_self(data);
bytes32 digest2 = keccak256(
abi.encodePacked("\x19\x00", ECDSAAddr, data)
);
assertEq(digest1, digest2);
}
}
9 changes: 9 additions & 0 deletions test/utils/interfaces/IECDSA.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,13 @@ interface IECDSA {
bytes32 domainSeparator,
bytes32 structHash
) external pure returns (bytes32);

function to_data_with_intended_validator_hash_self(
bytes calldata data
) external view returns (bytes32);

function to_data_with_intended_validator_hash(
address validator,
bytes calldata data
) external pure returns (bytes32);
}