From 215452f7e5c36d17314b638eac6e1d24ebbe6ec1 Mon Sep 17 00:00:00 2001 From: cairo <101215230+cairoeth@users.noreply.github.com> Date: Mon, 3 Jun 2024 11:04:24 -0700 Subject: [PATCH 1/9] add implementation and tests --- contracts/utils/Strings.sol | 31 +++++++++++++++++++++++++++++++ test/utils/Strings.test.js | 12 ++++++++++++ 2 files changed, 43 insertions(+) diff --git a/contracts/utils/Strings.sol b/contracts/utils/Strings.sol index b2c0a40fb2a..4932f729414 100644 --- a/contracts/utils/Strings.sol +++ b/contracts/utils/Strings.sol @@ -11,6 +11,7 @@ import {SignedMath} from "./math/SignedMath.sol"; */ library Strings { bytes16 private constant HEX_DIGITS = "0123456789abcdef"; + bytes16 private constant HEX_DIGITS_CAPITAL = "0123456789ABCDEF"; uint8 private constant ADDRESS_LENGTH = 20; /** @@ -85,6 +86,36 @@ library Strings { return toHexString(uint256(uint160(addr)), ADDRESS_LENGTH); } + /** + * @dev Converts an `address` with fixed length of 20 bytes to its checksummed ASCII `string` hexadecimal + * representation, according to EIP-55. + */ + function toChecksumHexString(address addr) internal pure returns (string memory) { + uint160 localValue = uint160(addr); + bytes memory lowercase = new bytes(40); + for (uint256 i = 40; i > 0; --i) { + lowercase[i - 1] = HEX_DIGITS[localValue & 0xf]; + localValue >>= 4; + } + bytes32 hashedAddr = keccak256(abi.encodePacked(lowercase)); + + bytes memory buffer = new bytes(42); + buffer[0] = "0"; + buffer[1] = "x"; + uint160 addrValue = uint160(addr); + uint160 hashValue = uint160(bytes20(hashedAddr)); + for (uint256 i = 41; i > 1; --i) { + if (hashValue & 0xf > 7) { + buffer[i] = HEX_DIGITS_CAPITAL[addrValue & 0xf]; + } else { + buffer[i] = HEX_DIGITS[addrValue & 0xf]; + } + addrValue >>= 4; + hashValue >>= 4; + } + return string(abi.encodePacked(buffer)); + } + /** * @dev Returns true if the two strings are equal. */ diff --git a/test/utils/Strings.test.js b/test/utils/Strings.test.js index 643172bcbf5..78fa1404828 100644 --- a/test/utils/Strings.test.js +++ b/test/utils/Strings.test.js @@ -120,6 +120,18 @@ describe('Strings', function () { }); }); + describe('toChecksumHexString address', function () { + it('converts a random address', async function () { + const addr = '0xa9036907dccae6a1e0033479b12e837e5cf5a02f'; + expect(await this.mock.getFunction('$toChecksumHexString(address)')(addr)).to.equal(ethers.getAddress(addr)); + }); + + it('converts an address with leading zeros', async function () { + const addr = '0x0000e0ca771e21bd00057f54a68c30d400000000'; + expect(await this.mock.getFunction('$toChecksumHexString(address)')(addr)).to.equal(ethers.getAddress(addr)); + }); + }); + describe('equal', function () { it('compares two empty strings', async function () { expect(await this.mock.$equal('', '')).to.be.true; From 3ef22ecf056d929c8ed4579038da8df12bf9ae76 Mon Sep 17 00:00:00 2001 From: cairo <101215230+cairoeth@users.noreply.github.com> Date: Mon, 3 Jun 2024 11:11:16 -0700 Subject: [PATCH 2/9] add changeset --- .changeset/forty-dodos-visit.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/forty-dodos-visit.md diff --git a/.changeset/forty-dodos-visit.md b/.changeset/forty-dodos-visit.md new file mode 100644 index 00000000000..7d5ae747335 --- /dev/null +++ b/.changeset/forty-dodos-visit.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`Strings`: Added a utility function for converting an address to checksummed string. From db76d546a18402de2cdf5ed2b1db782f5ae2fbbf Mon Sep 17 00:00:00 2001 From: cairo <101215230+cairoeth@users.noreply.github.com> Date: Mon, 3 Jun 2024 14:43:38 -0700 Subject: [PATCH 3/9] use shared logic with _setHexString --- contracts/utils/Strings.sol | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/contracts/utils/Strings.sol b/contracts/utils/Strings.sol index 4932f729414..007655ef9e7 100644 --- a/contracts/utils/Strings.sol +++ b/contracts/utils/Strings.sol @@ -64,17 +64,15 @@ library Strings { * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length. */ function toHexString(uint256 value, uint256 length) internal pure returns (string memory) { - uint256 localValue = value; + if (length < Math.log256(value) + 1) { + revert StringsInsufficientHexLength(value, length); + } + bytes memory buffer = new bytes(2 * length + 2); buffer[0] = "0"; buffer[1] = "x"; - for (uint256 i = 2 * length + 1; i > 1; --i) { - buffer[i] = HEX_DIGITS[localValue & 0xf]; - localValue >>= 4; - } - if (localValue != 0) { - revert StringsInsufficientHexLength(value, length); - } + _setHexString(buffer, 2, value); + return string(buffer); } @@ -91,12 +89,8 @@ library Strings { * representation, according to EIP-55. */ function toChecksumHexString(address addr) internal pure returns (string memory) { - uint160 localValue = uint160(addr); bytes memory lowercase = new bytes(40); - for (uint256 i = 40; i > 0; --i) { - lowercase[i - 1] = HEX_DIGITS[localValue & 0xf]; - localValue >>= 4; - } + _setHexString(lowercase, 0, uint160(addr)); bytes32 hashedAddr = keccak256(abi.encodePacked(lowercase)); bytes memory buffer = new bytes(42); @@ -122,4 +116,14 @@ library Strings { function equal(string memory a, string memory b) internal pure returns (bool) { return bytes(a).length == bytes(b).length && keccak256(bytes(a)) == keccak256(bytes(b)); } + + /** + * @dev Sets the hexadecimal representation of a value in the specified buffer starting from the given offset. + */ + function _setHexString(bytes memory buffer, uint256 offset, uint256 value) private pure { + for (uint256 i = buffer.length; i > offset; --i) { + buffer[i - 1] = HEX_DIGITS[value & 0xf]; + value >>= 4; + } + } } From e05ab88ac302713e3efd73ce0a834db411699ad9 Mon Sep 17 00:00:00 2001 From: cairo <101215230+cairoeth@users.noreply.github.com> Date: Mon, 3 Jun 2024 15:46:18 -0700 Subject: [PATCH 4/9] add addrValue to simplify MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ernesto García --- contracts/utils/Strings.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/utils/Strings.sol b/contracts/utils/Strings.sol index 007655ef9e7..0c3e21584d2 100644 --- a/contracts/utils/Strings.sol +++ b/contracts/utils/Strings.sol @@ -90,7 +90,8 @@ library Strings { */ function toChecksumHexString(address addr) internal pure returns (string memory) { bytes memory lowercase = new bytes(40); - _setHexString(lowercase, 0, uint160(addr)); + uint160 addrValue = uint160(address) + _setHexString(lowercase, 0, addrValue); bytes32 hashedAddr = keccak256(abi.encodePacked(lowercase)); bytes memory buffer = new bytes(42); From b0967a88cd38d06404caf7d75f757d74aec8b21a Mon Sep 17 00:00:00 2001 From: cairo <101215230+cairoeth@users.noreply.github.com> Date: Mon, 3 Jun 2024 15:46:52 -0700 Subject: [PATCH 5/9] use `addrValue` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ernesto García --- contracts/utils/Strings.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/utils/Strings.sol b/contracts/utils/Strings.sol index 0c3e21584d2..7c0dd5e1159 100644 --- a/contracts/utils/Strings.sol +++ b/contracts/utils/Strings.sol @@ -97,7 +97,6 @@ library Strings { bytes memory buffer = new bytes(42); buffer[0] = "0"; buffer[1] = "x"; - uint160 addrValue = uint160(addr); uint160 hashValue = uint160(bytes20(hashedAddr)); for (uint256 i = 41; i > 1; --i) { if (hashValue & 0xf > 7) { From 719978b17debb9ffe0f9015576a1d921e65b08ef Mon Sep 17 00:00:00 2001 From: ernestognw Date: Mon, 3 Jun 2024 16:46:57 -0600 Subject: [PATCH 6/9] Apply PR recommendations --- contracts/utils/Strings.sol | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/contracts/utils/Strings.sol b/contracts/utils/Strings.sol index 7c0dd5e1159..578ccce345a 100644 --- a/contracts/utils/Strings.sol +++ b/contracts/utils/Strings.sol @@ -71,7 +71,7 @@ library Strings { bytes memory buffer = new bytes(2 * length + 2); buffer[0] = "0"; buffer[1] = "x"; - _setHexString(buffer, 2, value); + _unsafeSetHexString(buffer, 2, value); return string(buffer); } @@ -90,8 +90,8 @@ library Strings { */ function toChecksumHexString(address addr) internal pure returns (string memory) { bytes memory lowercase = new bytes(40); - uint160 addrValue = uint160(address) - _setHexString(lowercase, 0, addrValue); + uint160 addrValue = uint160(addr); + _unsafeSetHexString(lowercase, 0, addrValue); bytes32 hashedAddr = keccak256(abi.encodePacked(lowercase)); bytes memory buffer = new bytes(42); @@ -99,11 +99,8 @@ library Strings { buffer[1] = "x"; uint160 hashValue = uint160(bytes20(hashedAddr)); for (uint256 i = 41; i > 1; --i) { - if (hashValue & 0xf > 7) { - buffer[i] = HEX_DIGITS_CAPITAL[addrValue & 0xf]; - } else { - buffer[i] = HEX_DIGITS[addrValue & 0xf]; - } + uint8 digit = uint8(addrValue & 0xf); + buffer[i] = hashValue & 0xf > 7 ? HEX_DIGITS_CAPITAL[digit] : HEX_DIGITS[digit]; addrValue >>= 4; hashValue >>= 4; } @@ -119,8 +116,11 @@ library Strings { /** * @dev Sets the hexadecimal representation of a value in the specified buffer starting from the given offset. + * + * NOTE: This function does not check that the `buffer` can allocate `value` without overflowing. Make sure + * to check whether `Math.log256(value) + 1` is larger than the specified `length`. */ - function _setHexString(bytes memory buffer, uint256 offset, uint256 value) private pure { + function _unsafeSetHexString(bytes memory buffer, uint256 offset, uint256 value) private pure { for (uint256 i = buffer.length; i > offset; --i) { buffer[i - 1] = HEX_DIGITS[value & 0xf]; value >>= 4; From f2ce027ca90c46c39470532b718b34c62b3b08dd Mon Sep 17 00:00:00 2001 From: cairo <101215230+cairoeth@users.noreply.github.com> Date: Mon, 3 Jun 2024 15:58:02 -0700 Subject: [PATCH 7/9] rename to HEX_DIGITS_UPPERCASE --- contracts/utils/Strings.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/utils/Strings.sol b/contracts/utils/Strings.sol index 578ccce345a..14c76bb9c67 100644 --- a/contracts/utils/Strings.sol +++ b/contracts/utils/Strings.sol @@ -11,7 +11,7 @@ import {SignedMath} from "./math/SignedMath.sol"; */ library Strings { bytes16 private constant HEX_DIGITS = "0123456789abcdef"; - bytes16 private constant HEX_DIGITS_CAPITAL = "0123456789ABCDEF"; + bytes16 private constant HEX_DIGITS_UPPERCASE = "0123456789ABCDEF"; uint8 private constant ADDRESS_LENGTH = 20; /** @@ -100,7 +100,7 @@ library Strings { uint160 hashValue = uint160(bytes20(hashedAddr)); for (uint256 i = 41; i > 1; --i) { uint8 digit = uint8(addrValue & 0xf); - buffer[i] = hashValue & 0xf > 7 ? HEX_DIGITS_CAPITAL[digit] : HEX_DIGITS[digit]; + buffer[i] = hashValue & 0xf > 7 ? HEX_DIGITS_UPPERCASE[digit] : HEX_DIGITS[digit]; addrValue >>= 4; hashValue >>= 4; } From ac713f08eeba37d830f467fd8436d896318e70b9 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Mon, 3 Jun 2024 17:08:56 -0600 Subject: [PATCH 8/9] Improve tests --- test/utils/Strings.test.js | 51 ++++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/test/utils/Strings.test.js b/test/utils/Strings.test.js index 78fa1404828..6353fd886db 100644 --- a/test/utils/Strings.test.js +++ b/test/utils/Strings.test.js @@ -108,27 +108,42 @@ describe('Strings', function () { }); }); - describe('toHexString address', function () { - it('converts a random address', async function () { - const addr = '0xa9036907dccae6a1e0033479b12e837e5cf5a02f'; - expect(await this.mock.getFunction('$toHexString(address)')(addr)).to.equal(addr); - }); - - it('converts an address with leading zeros', async function () { - const addr = '0x0000e0ca771e21bd00057f54a68c30d400000000'; - expect(await this.mock.getFunction('$toHexString(address)')(addr)).to.equal(addr); - }); - }); + describe('addresses', function () { + const addresses = [ + '0xa9036907dccae6a1e0033479b12e837e5cf5a02f', // Random address + '0x0000e0ca771e21bd00057f54a68c30d400000000', // Leading and trailing zeros + // EIP-55 reference + '0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed', + '0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359', + '0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB', + '0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb', + '0xfb6916095ca1df60bb79ce92ce3ea74c37c5d359', + '0x52908400098527886E0F7030069857D2E4169EE7', + '0x8617E340B3D01FA5F11F306F4090FD50E238070D', + '0xde709f2102306220921060314715629080e2fb77', + '0x27b1fdb04752bbc536007a920d24acb045561c26', + '0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed', + '0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359', + '0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB', + '0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb', + ]; - describe('toChecksumHexString address', function () { - it('converts a random address', async function () { - const addr = '0xa9036907dccae6a1e0033479b12e837e5cf5a02f'; - expect(await this.mock.getFunction('$toChecksumHexString(address)')(addr)).to.equal(ethers.getAddress(addr)); + describe('toHexString', function () { + for (const addr of addresses) { + it(`converts ${addr}`, async function () { + expect(await this.mock.getFunction('$toHexString(address)')(addr)).to.equal(addr.toLowerCase()); + }); + } }); - it('converts an address with leading zeros', async function () { - const addr = '0x0000e0ca771e21bd00057f54a68c30d400000000'; - expect(await this.mock.getFunction('$toChecksumHexString(address)')(addr)).to.equal(ethers.getAddress(addr)); + describe('toChecksumHexString', function () { + for (const addr of addresses) { + it(`converts ${addr}`, async function () { + expect(await this.mock.getFunction('$toChecksumHexString(address)')(addr)).to.equal( + ethers.getAddress(addr.toLowerCase()), + ); + }); + } }); }); From 8f11b326bdc7bdbbcc82d956fe38d07b820f7906 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 4 Jun 2024 12:23:26 +0200 Subject: [PATCH 9/9] checksum in place to avoid double allocation + simplification --- contracts/utils/Strings.sol | 53 +++++++++++++++---------------------- 1 file changed, 21 insertions(+), 32 deletions(-) diff --git a/contracts/utils/Strings.sol b/contracts/utils/Strings.sol index 14c76bb9c67..164d8acd07d 100644 --- a/contracts/utils/Strings.sol +++ b/contracts/utils/Strings.sol @@ -11,7 +11,6 @@ import {SignedMath} from "./math/SignedMath.sol"; */ library Strings { bytes16 private constant HEX_DIGITS = "0123456789abcdef"; - bytes16 private constant HEX_DIGITS_UPPERCASE = "0123456789ABCDEF"; uint8 private constant ADDRESS_LENGTH = 20; /** @@ -64,15 +63,17 @@ library Strings { * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length. */ function toHexString(uint256 value, uint256 length) internal pure returns (string memory) { - if (length < Math.log256(value) + 1) { - revert StringsInsufficientHexLength(value, length); - } - + uint256 localValue = value; bytes memory buffer = new bytes(2 * length + 2); buffer[0] = "0"; buffer[1] = "x"; - _unsafeSetHexString(buffer, 2, value); - + for (uint256 i = 2 * length + 1; i > 1; --i) { + buffer[i] = HEX_DIGITS[localValue & 0xf]; + localValue >>= 4; + } + if (localValue != 0) { + revert StringsInsufficientHexLength(value, length); + } return string(buffer); } @@ -89,22 +90,23 @@ library Strings { * representation, according to EIP-55. */ function toChecksumHexString(address addr) internal pure returns (string memory) { - bytes memory lowercase = new bytes(40); - uint160 addrValue = uint160(addr); - _unsafeSetHexString(lowercase, 0, addrValue); - bytes32 hashedAddr = keccak256(abi.encodePacked(lowercase)); + bytes memory buffer = bytes(toHexString(addr)); + + // hash the hex part of buffer (skip length + 2 bytes, length 40) + uint256 hashValue; + assembly ("memory-safe") { + hashValue := shr(96, keccak256(add(buffer, 0x22), 40)) + } - bytes memory buffer = new bytes(42); - buffer[0] = "0"; - buffer[1] = "x"; - uint160 hashValue = uint160(bytes20(hashedAddr)); for (uint256 i = 41; i > 1; --i) { - uint8 digit = uint8(addrValue & 0xf); - buffer[i] = hashValue & 0xf > 7 ? HEX_DIGITS_UPPERCASE[digit] : HEX_DIGITS[digit]; - addrValue >>= 4; + // possible values for buffer[i] are 48 (0) to 57 (9) and 97 (a) to 102 (f) + if (hashValue & 0xf > 7 && uint8(buffer[i]) > 96) { + // case shift by xoring with 0x20 + buffer[i] ^= 0x20; + } hashValue >>= 4; } - return string(abi.encodePacked(buffer)); + return string(buffer); } /** @@ -113,17 +115,4 @@ library Strings { function equal(string memory a, string memory b) internal pure returns (bool) { return bytes(a).length == bytes(b).length && keccak256(bytes(a)) == keccak256(bytes(b)); } - - /** - * @dev Sets the hexadecimal representation of a value in the specified buffer starting from the given offset. - * - * NOTE: This function does not check that the `buffer` can allocate `value` without overflowing. Make sure - * to check whether `Math.log256(value) + 1` is larger than the specified `length`. - */ - function _unsafeSetHexString(bytes memory buffer, uint256 offset, uint256 value) private pure { - for (uint256 i = buffer.length; i > offset; --i) { - buffer[i - 1] = HEX_DIGITS[value & 0xf]; - value >>= 4; - } - } }