diff --git a/.gas-snapshot b/.gas-snapshot index d61fb9d3..d92d06b9 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -23,6 +23,23 @@ DSTestPlusTest:testBrutalizeMemory() (gas: 823) DSTestPlusTest:testFailBoundMinBiggerThanMax() (gas: 287) DSTestPlusTest:testMeasuringGas() (gas: 24600) DSTestPlusTest:testRelApproxEqBothZeroesPasses() (gas: 391) +ECDSATest:testBytes32ToEthSignedMessageHash() (gas: 342) +ECDSATest:testBytesToEthSignedMessageHashEmpty() (gas: 610) +ECDSATest:testBytesToEthSignedMessageHashEmptyLong() (gas: 771) +ECDSATest:testBytesToEthSignedMessageHashShort() (gas: 629) +ECDSATest:testRecoverWithInvalidLongSignature() (gas: 5041) +ECDSATest:testRecoverWithInvalidShortSignature() (gas: 4915) +ECDSATest:testRecoverWithInvalidSignature() (gas: 5175) +ECDSATest:testRecoverWithV0SignatureWithShortEIP2098Format() (gas: 4962) +ECDSATest:testRecoverWithV0SignatureWithVersion00() (gas: 5077) +ECDSATest:testRecoverWithV0SignatureWithVersion27() (gas: 5074) +ECDSATest:testRecoverWithV0SignatureWithWrongVersion() (gas: 5076) +ECDSATest:testRecoverWithV1SignatureWithShortEIP2098Format() (gas: 4984) +ECDSATest:testRecoverWithV1SignatureWithVersion01() (gas: 5075) +ECDSATest:testRecoverWithV1SignatureWithVersion28() (gas: 5054) +ECDSATest:testRecoverWithV1SignatureWithWrongVersion() (gas: 5053) +ECDSATest:testRecoverWithValidSignature() (gas: 5150) +ECDSATest:testRecoverWithWrongSigner() (gas: 5130) ERC1155Test:testApproveAll() (gas: 31053) ERC1155Test:testBatchBalanceOf() (gas: 157552) ERC1155Test:testBatchBurn() (gas: 151044) diff --git a/src/test/ECDSA.t.sol b/src/test/ECDSA.t.sol new file mode 100644 index 00000000..6c0c2f28 --- /dev/null +++ b/src/test/ECDSA.t.sol @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.10; + +import {DSTestPlus} from "./utils/DSTestPlus.sol"; + +import {ECDSA} from "../utils/ECDSA.sol"; + +contract ECDSATest is DSTestPlus { + using ECDSA for bytes32; + using ECDSA for bytes; + + bytes32 constant TEST_MESSAGE = 0x7dbaf558b0a1a5dc7a67202117ab143c1d8605a983e4a743bc06fcc03162dc0d; + + bytes32 constant WRONG_MESSAGE = 0x2d0828dd7c97cff316356da3c16c68ba2316886a0e05ebafb8291939310d51a3; + + address constant SIGNER = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8; + + address constant V0_SIGNER = 0x2cc1166f6212628A0deEf2B33BEFB2187D35b86c; + + address constant V1_SIGNER = 0x1E318623aB09Fe6de3C9b8672098464Aeda9100E; + + function testRecoverWithInvalidShortSignature() public { + bytes memory signature = hex"1234"; + assertTrue(this.recover(TEST_MESSAGE, signature) == address(0)); + } + + function testRecoverWithInvalidLongSignature() public { + // prettier-ignore + bytes memory signature = hex"01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"; + assertTrue(this.recover(TEST_MESSAGE, signature) == address(0)); + } + + function testRecoverWithValidSignature() public { + // prettier-ignore + bytes memory signature = hex"8688e590483917863a35ef230c0f839be8418aa4ee765228eddfcea7fe2652815db01c2c84b0ec746e1b74d97475c599b3d3419fa7181b4e01de62c02b721aea1b"; + assertTrue(this.recover(TEST_MESSAGE.toEthSignedMessageHash(), signature) == SIGNER); + } + + function testRecoverWithWrongSigner() public { + // prettier-ignore + bytes memory signature = hex"8688e590483917863a35ef230c0f839be8418aa4ee765228eddfcea7fe2652815db01c2c84b0ec746e1b74d97475c599b3d3419fa7181b4e01de62c02b721aea1b"; + assertTrue(this.recover(WRONG_MESSAGE.toEthSignedMessageHash(), signature) != SIGNER); + } + + function testRecoverWithInvalidSignature() public { + // prettier-ignore + bytes memory signature = hex"332ce75a821c982f9127538858900d87d3ec1f9f737338ad67cad133fa48feff48e6fa0c18abc62e42820f05943e47af3e9fbe306ce74d64094bdf1691ee53e01c"; + assertTrue(this.recover(TEST_MESSAGE.toEthSignedMessageHash(), signature) != SIGNER); + } + + function testRecoverWithV0SignatureWithVersion00() public { + // prettier-ignore + bytes memory signature = hex"5d99b6f7f6d1f73d1a26497f2b1c89b24c0993913f86e9a2d02cd69887d9c94f3c880358579d811b21dd1b7fd9bb01c1d81d10e69f0384e675c32b39643be89200"; + assertTrue(this.recover(TEST_MESSAGE, signature) == address(0)); + } + + function testRecoverWithV0SignatureWithVersion27() public { + // prettier-ignore + bytes memory signature = hex"5d99b6f7f6d1f73d1a26497f2b1c89b24c0993913f86e9a2d02cd69887d9c94f3c880358579d811b21dd1b7fd9bb01c1d81d10e69f0384e675c32b39643be8921b"; + assertTrue(this.recover(TEST_MESSAGE, signature) == V0_SIGNER); + } + + function testRecoverWithV0SignatureWithWrongVersion() public { + // prettier-ignore + bytes memory signature = hex"5d99b6f7f6d1f73d1a26497f2b1c89b24c0993913f86e9a2d02cd69887d9c94f3c880358579d811b21dd1b7fd9bb01c1d81d10e69f0384e675c32b39643be89202"; + assertTrue(this.recover(TEST_MESSAGE, signature) == address(0)); + } + + function testRecoverWithV0SignatureWithShortEIP2098Format() public { + // prettier-ignore + bytes memory signature = hex"5d99b6f7f6d1f73d1a26497f2b1c89b24c0993913f86e9a2d02cd69887d9c94f3c880358579d811b21dd1b7fd9bb01c1d81d10e69f0384e675c32b39643be892"; + assertTrue(this.recover(TEST_MESSAGE, signature) == V0_SIGNER); + } + + function testRecoverWithV1SignatureWithVersion01() public { + // prettier-ignore + bytes memory signature = hex"331fe75a821c982f9127538858900d87d3ec1f9f737338ad67cad133fa48feff48e6fa0c18abc62e42820f05943e47af3e9fbe306ce74d64094bdf1691ee53e001"; + assertTrue(this.recover(TEST_MESSAGE, signature) == address(0)); + } + + function testRecoverWithV1SignatureWithVersion28() public { + // prettier-ignore + bytes memory signature = hex"331fe75a821c982f9127538858900d87d3ec1f9f737338ad67cad133fa48feff48e6fa0c18abc62e42820f05943e47af3e9fbe306ce74d64094bdf1691ee53e01c"; + assertTrue(this.recover(TEST_MESSAGE, signature) == V1_SIGNER); + } + + function testRecoverWithV1SignatureWithWrongVersion() public { + // prettier-ignore + bytes memory signature = hex"331fe75a821c982f9127538858900d87d3ec1f9f737338ad67cad133fa48feff48e6fa0c18abc62e42820f05943e47af3e9fbe306ce74d64094bdf1691ee53e002"; + assertTrue(this.recover(TEST_MESSAGE, signature) == address(0)); + } + + function testRecoverWithV1SignatureWithShortEIP2098Format() public { + // prettier-ignore + bytes memory signature = hex"331fe75a821c982f9127538858900d87d3ec1f9f737338ad67cad133fa48feffc8e6fa0c18abc62e42820f05943e47af3e9fbe306ce74d64094bdf1691ee53e0"; + assertTrue(this.recover(TEST_MESSAGE, signature) == V1_SIGNER); + } + + function testBytes32ToEthSignedMessageHash() public { + // prettier-ignore + assertTrue(TEST_MESSAGE.toEthSignedMessageHash() == bytes32(0x7d768af957ef8cbf6219a37e743d5546d911dae3e46449d8a5810522db2ef65e)); + } + + function testBytesToEthSignedMessageHashShort() public { + bytes memory message = hex"61626364"; + // prettier-ignore + assertTrue(message.toEthSignedMessageHash() == bytes32(0xefd0b51a9c4e5f3449f4eeacb195bf48659fbc00d2f4001bf4c088ba0779fb33)); + } + + function testBytesToEthSignedMessageHashEmpty() public { + bytes memory message = hex""; + // prettier-ignore + assertTrue(message.toEthSignedMessageHash() == bytes32(0x5f35dce98ba4fba25530a026ed80b2cecdaa31091ba4958b99b52ea1d068adad)); + } + + function testBytesToEthSignedMessageHashEmptyLong() public { + // prettier-ignore + bytes memory message = hex"4142434445464748494a4b4c4d4e4f505152535455565758595a6162636465666768696a6b6c6d6e6f707172737475767778797a3031323334353637383921402324255e262a28292d3d5b5d7b7d"; + // prettier-ignore + assertTrue(message.toEthSignedMessageHash() == bytes32(0xa46dbedd405cff161b6e80c17c8567597621d9f4c087204201097cb34448e71b)); + } + + function recover(bytes32 hash, bytes calldata signature) external view returns (address) { + return ECDSA.recover(hash, signature); + } +} diff --git a/src/utils/ECDSA.sol b/src/utils/ECDSA.sol new file mode 100644 index 00000000..03b6ce0d --- /dev/null +++ b/src/utils/ECDSA.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +/// @notice Gas optimized ECDSA wrapper. +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/utils/ECDSA.sol) +/// @author Modified from OpenZeppelin (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/ECDSA.sol) +library ECDSA { + function recover(bytes32 hash, bytes calldata signature) internal view returns (address result) { + assembly { + // Copy the free memory pointer so that we can restore it later. + let m := mload(0x40) + // Directly load `s` from the calldata. + let s := calldataload(add(signature.offset, 0x20)) + + switch signature.length + case 64 { + // Here, `s` is actually `vs` that needs to be recovered into `v` and `s`. + // Compute `v` and store it in the scratch space. + mstore(0x20, add(shr(255, s), 27)) + // prettier-ignore + s := and(s, 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff) + } + case 65 { + // Compute `v` and store it in the scratch space. + mstore(0x20, byte(0, calldataload(add(signature.offset, 0x40)))) + } + + // If `s` in lower half order, such that the signature is not malleable. + // prettier-ignore + if iszero(gt(s, 0x7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0)) { + mstore(0x00, hash) + calldatacopy(0x40, signature.offset, 0x20) // Directly copy `r` over. + mstore(0x60, s) + pop( + staticcall( + gas(), // Amount of gas left for the transaction. + 0x01, // Address of `ecrecover`. + 0x00, // Start of input. + 0x80, // Size of input. + 0x40, // Start of output. + 0x20 // Size of output. + ) + ) + // Restore the zero slot. + mstore(0x60, 0) + // `returndatasize()` will be `0x20` upon success, and `0x00` otherwise. + result := mload(sub(0x60, returndatasize())) + } + // Restore the free memory pointer. + mstore(0x40, m) + } + } + + function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32 result) { + assembly { + // Store into scratch space for keccak256. + mstore(0x20, hash) + mstore(0x00, "\x00\x00\x00\x00\x19Ethereum Signed Message:\n32") + // 0x40 - 0x04 = 0x3c + result := keccak256(0x04, 0x3c) + } + } + + function toEthSignedMessageHash(bytes memory s) internal pure returns (bytes32 result) { + assembly { + // We need at most 128 bytes for Ethereum signed message header. + // The max length of the ASCII reprenstation of a uint256 is 78 bytes. + // The length of "\x19Ethereum Signed Message:\n" is 26 bytes. + // The next multiple of 32 above 78 + 26 is 128. + + // Instead of allocating, we temporarily copy the 128 bytes before the + // start of `s` data to some variables. + let m3 := mload(sub(s, 0x60)) + let m2 := mload(sub(s, 0x40)) + let m1 := mload(sub(s, 0x20)) + // The length of `s` is in bytes. + let sLength := mload(s) + + let ptr := add(s, 0x20) + + // `end` marks the end of the memory which we will compute the keccak256 of. + let end := add(ptr, sLength) + + // Convert the length of the bytes to ASCII decimal representation + // and store it into the memory. + for { + let temp := sLength + ptr := sub(ptr, 1) + mstore8(ptr, add(48, mod(temp, 10))) + temp := div(temp, 10) + } temp { + temp := div(temp, 10) + } { + ptr := sub(ptr, 1) + mstore8(ptr, add(48, mod(temp, 10))) + } + + // Move the pointer 32 bytes lower to make room for the string. + // `start` marks the start of the memory which we will compute the keccak256 of. + let start := sub(ptr, 32) + // Copy the header over to the memory. + mstore(start, "\x00\x00\x00\x00\x00\x00\x19Ethereum Signed Message:\n") + start := add(start, 6) + + // Compute the keccak256 of the memory. + result := keccak256(start, sub(end, start)) + + // Restore the previous memory. + mstore(s, sLength) + mstore(sub(s, 0x20), m1) + mstore(sub(s, 0x40), m2) + mstore(sub(s, 0x60), m3) + } + } +}