-
Notifications
You must be signed in to change notification settings - Fork 12.4k
Add utility function for converting an address to checksummed string #5067
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
Changes from 7 commits
215452f
3ef22ec
db76d54
e05ab88
b0967a8
719978b
f2ce027
ac713f0
8f11b32
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| 'openzeppelin-solidity': minor | ||
| --- | ||
|
|
||
| `Strings`: Added a utility function for converting an address to checksummed string. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,7 @@ 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; | ||
|
|
||
| /** | ||
|
|
@@ -63,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); | ||
| } | ||
| _unsafeSetHexString(buffer, 2, value); | ||
|
|
||
| return string(buffer); | ||
| } | ||
|
|
||
|
|
@@ -85,10 +84,46 @@ 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 lowercase = new bytes(40); | ||
| uint160 addrValue = uint160(addr); | ||
| _unsafeSetHexString(lowercase, 0, addrValue); | ||
| bytes32 hashedAddr = keccak256(abi.encodePacked(lowercase)); | ||
|
||
|
|
||
| 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; | ||
| hashValue >>= 4; | ||
| } | ||
| return string(abi.encodePacked(buffer)); | ||
| } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This entier function should be done "in place". Allocating two buffers is a waste. |
||
|
|
||
| /** | ||
| * @dev Returns true if the two strings are equal. | ||
| */ | ||
| 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]; | ||
Amxx marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| value >>= 4; | ||
| } | ||
| } | ||
| } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's a rust implementation of this checksum algorithm in Foundry (as seen in cast), so it should be relatively trivial to make a PR and request for it to be exposed through VM.sol as with Base64. With that, we can fuzz the implementation, which would be extremely valuable.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. totally -- fuzzing should be used in some of the other utils as well.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agree, feel free to open PRs adding fuzzing or Halmos FV to those utils you consider make sense |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Redesigned
toChecksumHexStringto avoid double allocation._unsafeSetHexStringHEX_DIGITS_UPPERCASEThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right this is extremely cleaner. Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
great changes, thxs!