I went ahead and put together a working example of EIP-55 in Solidity in case it's useful to anyone that comes across this. It should also be pretty straightforward to create a library that extends address
with a few of these methods.
pragma solidity ^0.5.1;
/**
* @dev This contract provides a set of pure functions for computing the EIP-55
* checksum of an account in formats friendly to both off-chain and on-chain
* callers, as well as for checking if a given string hex representation of an
* address has a valid checksum. These helper functions could also be repurposed
* as a library that extends the `address` type.
*/
contract AddressChecksumUtils {
/**
* @dev Get a checksummed string hex representation of an account address.
* @param account address The account to get the checksum for.
* @return The checksummed account string in ASCII format. Note that leading
* "0x" is not included.
*/
function getChecksum(
address account
) external pure returns (string memory accountChecksum) {
// call internal function for converting an account to a checksummed string.
return _toChecksumString(account);
}
/**
* @dev Get a fixed-size array of whether or not each character in an account
* will be capitalized in the checksum.
* @param account address The account to get the checksum capitalization
* information for.
* @return A fixed-size array of booleans that signify if each character or
* "nibble" of the hex encoding of the address will be capitalized by the
* checksum.
*/
function getChecksumCapitalizedCharacters(
address account
) external pure returns (bool[40] memory characterCapitalized) {
// call internal function for computing characters capitalized in checksum.
return _toChecksumCapsFlags(account);
}
/**
* @dev Determine whether a string hex representation of an account address
* matches the ERC-55 checksum of that address.
* @param accountChecksum string The checksummed account string in ASCII
* format. Note that a leading "0x" MUST NOT be included.
* @return A boolean signifying whether or not the checksum is valid.
*/
function isChecksumValid(
string calldata accountChecksum
) external pure returns (bool ok) {
// call internal function for validating checksum strings.
return _isChecksumValid(accountChecksum);
}
function _toChecksumString(
address account
) internal pure returns (string memory asciiString) {
// convert the account argument from address to bytes.
bytes20 data = bytes20(account);
// create an in-memory fixed-size bytes array.
bytes memory asciiBytes = new bytes(40);
// declare variable types.
uint8 b;
uint8 leftNibble;
uint8 rightNibble;
bool leftCaps;
bool rightCaps;
uint8 asciiOffset;
// get the capitalized characters in the actual checksum.
bool[40] memory caps = _toChecksumCapsFlags(account);
// iterate over bytes, processing left and right nibble in each iteration.
for (uint256 i = 0; i < data.length; i++) {
// locate the byte and extract each nibble.
b = uint8(uint160(data) / (2**(8*(19 - i))));
leftNibble = b / 16;
rightNibble = b - 16 * leftNibble;
// locate and extract each capitalization status.
leftCaps = caps[2*i];
rightCaps = caps[2*i + 1];
// get the offset from nibble value to ascii character for left nibble.
asciiOffset = _getAsciiOffset(leftNibble, leftCaps);
// add the converted character to the byte array.
asciiBytes[2 * i] = byte(leftNibble + asciiOffset);
// get the offset from nibble value to ascii character for right nibble.
asciiOffset = _getAsciiOffset(rightNibble, rightCaps);
// add the converted character to the byte array.
asciiBytes[2 * i + 1] = byte(rightNibble + asciiOffset);
}
return string(asciiBytes);
}
function _toChecksumCapsFlags(address account) internal pure returns (
bool[40] memory characterCapitalized
) {
// convert the address to bytes.
bytes20 a = bytes20(account);
// hash the address (used to calculate checksum).
bytes32 b = keccak256(abi.encodePacked(_toAsciiString(a)));
// declare variable types.
uint8 leftNibbleAddress;
uint8 rightNibbleAddress;
uint8 leftNibbleHash;
uint8 rightNibbleHash;
// iterate over bytes, processing left and right nibble in each iteration.
for (uint256 i; i < a.length; i++) {
// locate the byte and extract each nibble for the address and the hash.
rightNibbleAddress = uint8(a[i]) % 16;
leftNibbleAddress = (uint8(a[i]) - rightNibbleAddress) / 16;
rightNibbleHash = uint8(b[i]) % 16;
leftNibbleHash = (uint8(b[i]) - rightNibbleHash) / 16;
characterCapitalized[2 * i] = (
leftNibbleAddress > 9 &&
leftNibbleHash > 7
);
characterCapitalized[2 * i + 1] = (
rightNibbleAddress > 9 &&
rightNibbleHash > 7
);
}
}
function _isChecksumValid(
string memory provided
) internal pure returns (bool ok) {
// convert the provided string into account type.
address account = _toAddress(provided);
// return false in the event the account conversion returned null address.
if (
account == address(0)
) {
// ensure that provided address is not also the null address first.
bytes memory b = bytes(provided);
for (uint256 i; i < b.length; i++) {
if (b[i] != hex"30") {
return false;
}
}
}
// get the capitalized characters in the actual checksum.
string memory actual = _toChecksumString(account);
// compare provided string to actual checksum string to test for validity.
return (
keccak256(
abi.encodePacked(
actual
)
) == keccak256(
abi.encodePacked(
provided
)
)
);
}
function _getAsciiOffset(
uint8 nibble, bool caps
) internal pure returns (uint8 offset) {
// to convert to ascii characters, add 48 to 0-9, 55 to A-F, & 87 to a-f.
if (nibble < 10) {
offset = 48;
} else if (caps) {
offset = 55;
} else {
offset = 87;
}
}
function _toAddress(
string memory account
) internal pure returns (address accountAddress) {
// convert the account argument from address to bytes.
bytes memory accountBytes = bytes(account);
// create a new fixed-size byte array for the ascii bytes of the address.
bytes memory accountAddressBytes = new bytes(20);
// declare variable types.
uint8 b;
uint8 nibble;
uint8 asciiOffset;
// only proceed if the provided string has a length of 40.
if (accountBytes.length == 40) {
for (uint256 i; i < 40; i++) {
// get the byte in question.
b = uint8(accountBytes[i]);
// ensure that the byte is a valid ascii character (0-9, A-F, a-f)
if (b < 48) return address(0);
if (57 < b && b < 65) return address(0);
if (70 < b && b < 97) return address(0);
if (102 < b) return address(0); //bytes(hex"");
// find the offset from ascii encoding to the nibble representation.
if (b < 65) { // 0-9
asciiOffset = 48;
} else if (70 < b) { // a-f
asciiOffset = 87;
} else { // A-F
asciiOffset = 55;
}
// store left nibble on even iterations, then store byte on odd ones.
if (i % 2 == 0) {
nibble = b - asciiOffset;
} else {
accountAddressBytes[(i - 1) / 2] = (
byte(16 * nibble + (b - asciiOffset)));
}
}
// pack up the fixed-size byte array and cast it to accountAddress.
bytes memory packed = abi.encodePacked(accountAddressBytes);
assembly {
accountAddress := mload(add(packed, 20))
}
}
}
// based on https://ethereum.stackexchange.com/a/56499/48410
function _toAsciiString(
bytes20 data
) internal pure returns (string memory asciiString) {
// create an in-memory fixed-size bytes array.
bytes memory asciiBytes = new bytes(40);
// declare variable types.
uint8 b;
uint8 leftNibble;
uint8 rightNibble;
// iterate over bytes, processing left and right nibble in each iteration.
for (uint256 i = 0; i < data.length; i++) {
// locate the byte and extract each nibble.
b = uint8(uint160(data) / (2 ** (8 * (19 - i))));
leftNibble = b / 16;
rightNibble = b - 16 * leftNibble;
// to convert to ascii characters, add 48 to 0-9 and 87 to a-f.
asciiBytes[2 * i] = byte(leftNibble + (leftNibble < 10 ? 48 : 87));
asciiBytes[2 * i + 1] = byte(rightNibble + (rightNibble < 10 ? 48 : 87));
}
return string(asciiBytes);
}
}