diff --git a/.changeset/forty-dodos-visit.md b/.changeset/forty-dodos-visit.md new file mode 100644 index 000000000..7d5ae7473 --- /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. diff --git a/contracts/utils/Strings.sol b/contracts/utils/Strings.sol index b2c0a40fb..164d8acd0 100644 --- a/contracts/utils/Strings.sol +++ b/contracts/utils/Strings.sol @@ -85,6 +85,30 @@ 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) { + 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)) + } + + for (uint256 i = 41; i > 1; --i) { + // 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(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 643172bcb..6353fd886 100644 --- a/test/utils/Strings.test.js +++ b/test/utils/Strings.test.js @@ -108,15 +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); + 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('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('$toHexString(address)')(addr)).to.equal(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()), + ); + }); + } }); });