The problem with the code above is that you're taking the address apart and converting it to a string of chars 1 hex digit at a time. In hexadecimal, there is not distinction between capital and small symbols (e.g., 0xA is equal to 0xa which in turn is equal to the decimal value of 10). All hex characters above the decimal value of 10 (non-decimal chars from a-f) are being converted into ASCII chars by incrementing their hex value with 0x57 to result in letters (a-z). And since input values like 0xa and 0xA are basically the same value, both would result in the same lowercase letter a.
The checksum details are lost in that conversion and the resulting string is all lowercase. If you wanted to keep the checksum info, you'll need to include the hex digit's index in the address into account, as the checksum algorithm is described as follows:
convert the address to hex, but if the ith digit is a letter (ie. it's one of abcdef) print it in uppercase if the 4*ith bit of the hash of the lowercase hexadecimal address is 1 () otherwise print it in lowercase.
Basically, you'll first need to convert the address to a lowercase string, then take the keccak256 hash of it and apply the algorithm above. More info about the checksum algorithm here.
Answer from Ahmed Tawfeeq on Stack ExchangeThe problem with the code above is that you're taking the address apart and converting it to a string of chars 1 hex digit at a time. In hexadecimal, there is not distinction between capital and small symbols (e.g., 0xA is equal to 0xa which in turn is equal to the decimal value of 10). All hex characters above the decimal value of 10 (non-decimal chars from a-f) are being converted into ASCII chars by incrementing their hex value with 0x57 to result in letters (a-z). And since input values like 0xa and 0xA are basically the same value, both would result in the same lowercase letter a.
The checksum details are lost in that conversion and the resulting string is all lowercase. If you wanted to keep the checksum info, you'll need to include the hex digit's index in the address into account, as the checksum algorithm is described as follows:
convert the address to hex, but if the ith digit is a letter (ie. it's one of abcdef) print it in uppercase if the 4*ith bit of the hash of the lowercase hexadecimal address is 1 () otherwise print it in lowercase.
Basically, you'll first need to convert the address to a lowercase string, then take the keccak256 hash of it and apply the algorithm above. More info about the checksum algorithm here.
Thanks for your response. I think you are correct. I found the code for what you are talking about at: Address checksum Solidity implementation
Ethereum wallet addresses are in hex [0-9A-F]*. While the address itself is case-insensitive (A is the same as a to the network), the case sensitivity is used as a (optional) checksum. It was built as an after-thought to an addressing scheme that lacked basic checksum validation.
https://github.com/ethereum/EIPs/issues/55#issuecomment-187159063
The checksum works like so:
- lowercase address and remove 0x prefix
- sha3 hash result from #1
- change nth letter of address according to the nth letter of the hash:
- 0,1,2,3,4,5,6,7 → Lowercase
- 8, 9, a, b, c, d, e, f → Uppercase
So, you sha3 hash the address, and look at each Nth character of the sha result. If it's 7 or below, the Nth character in the address is lowercase. If it is 8 or above, that character is uppercase.
(Brought from an old Ethereum forum whose link is now broken.)
Capitalization simply means the address has a checksum. You should use the capitalization address because of this, but both will work.
Use abi.encodePacked(x)
where x is the address. (Thanks @k06a)
I was not able to read the ABI-encoded string with web3.js. Therefore, I added some conversion to the ASCII characters:
function toAsciiString(address x) internal pure returns (string memory) {
bytes memory s = new bytes(40);
for (uint i = 0; i < 20; i++) {
bytes1 b = bytes1(uint8(uint(uint160(x)) / (2**(8*(19 - i)))));
bytes1 hi = bytes1(uint8(b) / 16);
bytes1 lo = bytes1(uint8(b) - 16 * uint8(hi));
s[2*i] = char(hi);
s[2*i+1] = char(lo);
}
return string(s);
}
function char(bytes1 b) internal pure returns (bytes1 c) {
if (uint8(b) < 10) return bytes1(uint8(b) + 0x30);
else return bytes1(uint8(b) + 0x57);
}
The memory layout can be tricky.
When you do bytes memory bytesInput = bytes(input);, you're essentially only shallow-copying the string (instantiating a bytes variable with bytes indexes pointing to the input's memory pointers).
To make a deep copy, considering using the updated code below:
function copyBytes(bytes memory _bytes) private pure returns (bytes memory) {
bytes memory copy = new bytes(_bytes.length);
uint256 max = _bytes.length + 31;
for (uint256 i=32; i<=max; i+=32)
{
assembly { mstore(add(copy, i), mload(add(_bytes, i))) }
}
return copy;
}
function _toLowercase(string memory inputNonModifiable) public pure returns (string memory) {
bytes memory bytesInput = copyBytes(bytes(inputNonModifiable));
for (uint i = 0; i < bytesInput.length; i++) {
// checks for valid ascii characters // will allow unicode after building a string library
require (uint8(bytesInput[i]) > 31 && uint8(bytesInput[i]) < 127, "Only ASCII characters");
// Uppercase character...
if (uint8(bytesInput[i]) > 64 && uint8(bytesInput[i]) < 91) {
// add 32 to make it lowercase
bytesInput[i] = bytes1(uint8(bytesInput[i]) + 32);
}
}
return string(bytesInput);
}
This is a fixed "transform string to lower case" version for Solidity above ^0.5.0 (that won't modify the original string in the memory).
A complete working example tailored to your specific logic:
pragma solidity ^0.8.13;
contract Test {
mapping(string => uint256) public _idOfName;
mapping(address => uint256) public _idOfAddress;
struct UserData {
address indexUser;
uint256 registrationDate;
address defaultAddress;
string userName;
string userBio;
string imgUrl;
string[] linkedProfiles;
}
mapping(uint256 => UserData) public _userData;
event UserDataUpdated(address defaultAddress, string userName, string userBio, string imgUrl, string[] linkedProfiles);
/*constructor() {
string[] memory linkedProfiles = new string;
linkedProfiles[0] = "bbbb";
linkedProfiles[1] = "aaaa";
UserData memory initialData = UserData({
indexUser: address(this),
registrationDate: block.timestamp,
defaultAddress: address(this),
userName: "Private User",
userBio: "This user has chosen not to index their account",
imgUrl: "/assets/users/incognito.jpeg",
linkedProfiles: linkedProfiles
});
_userData[1] = initialData;
}*/
function _setUserData(uint256 id, UserData memory newData) internal virtual returns (UserData memory) {
_idOfName[_toLowercase(newData.userName)] = id;
_idOfAddress[newData.defaultAddress] = id;
_userData[id].indexUser = newData.indexUser;
_userData[id].registrationDate = newData.registrationDate;
_userData[id].defaultAddress = newData.defaultAddress;
_userData[id].userName = newData.userName;
_userData[id].userBio = newData.userBio;
_userData[id].imgUrl = newData.imgUrl;
for (uint256 i; i < newData.linkedProfiles.length; i++) {
if (i >= _userData[id].linkedProfiles.length) {
_userData[id].linkedProfiles.push(newData.linkedProfiles[i]);
} else {
_userData[id].linkedProfiles[i] = newData.linkedProfiles[i];
}
}
emit UserDataUpdated(newData.defaultAddress, newData.userName, newData.userBio, newData.imgUrl, newData.linkedProfiles);
return _userData[id];
}
function copyBytes(bytes memory _bytes) private pure returns (bytes memory) {
bytes memory copy = new bytes(_bytes.length);
uint256 max = _bytes.length + 31;
for (uint256 i=32; i<=max; i+=32)
{
assembly { mstore(add(copy, i), mload(add(_bytes, i))) }
}
return copy;
}
function _toLowercase(string memory inputNonModifiable) public pure returns (string memory) {
bytes memory bytesInput = copyBytes(bytes(inputNonModifiable));
for (uint i = 0; i < bytesInput.length; i++) {
// checks for valid ascii characters // will allow unicode after building a string library
require (uint8(bytesInput[i]) > 31 && uint8(bytesInput[i]) < 127, "Only ASCII characters");
// Uppercase character...
if (uint8(bytesInput[i]) > 64 && uint8(bytesInput[i]) < 91) {
// add 32 to make it lowercase
bytesInput[i] = bytes1(uint8(bytesInput[i]) + 32);
}
}
return string(bytesInput);
}
function test() external returns(UserData memory) {
string[] memory linkedProfiles = new string;
linkedProfiles[0] = "1234";
linkedProfiles[1] = "5678";
UserData memory newData = UserData({
indexUser: address(0),
registrationDate: block.timestamp,
defaultAddress: address(0),
userName: "Private User",
userBio: "This user has chosen not to index their account",
imgUrl: "/assets/users/incognito.jpeg",
linkedProfiles: linkedProfiles
});
return _setUserData(1, newData);
}
}

The event log confirms that the function now works as expected, without the extra string overwrites:
"args": {
"0": "0x0000000000000000000000000000000000000000",
"1": "Private User",
"2": "This user has chosen not to index their account",
"3": "/assets/users/incognito.jpeg",
"4": [
"1234",
"5678"
],
"defaultAddress": "0x0000000000000000000000000000000000000000",
"userName": "Private User",
"userBio": "This user has chosen not to index their account",
"imgUrl": "/assets/users/incognito.jpeg",
"linkedProfiles": [
"1234",
"5678"
]
}
Finally, let's confirm that the key was transformed to lower case successfully:

As Mila has answered, the value gets modified in the outer method because it is passed to the _toLowerCase as a memory pointer so that any modifications will affect the original value. This is how the internal calls work to optimize the performance.
I would like to add, that instead of a manual memory copy, you could save a bunch of gas, if you would handle the UserData as calldata. Then passing the value to _toLowerCase will copy the value into memory natively.
function _setUserData(uint256 id, UserData calldata newData) internal virtual {
_idOfName[_toLowercase(newData.userName)] = id;
// ...
}
Though it could be not always possible, as the method, which calls
_setUserDatashould have the value also from thecalldata. But if you can, it is better to go this way.
There is also another native way to force memory copy, but I would not suggest doing this, just to know how things work. If you use this keyword to call the method, then it won't be an internal call anymore, the extra CALL is used and therefor the value gets copied:
function _setUserData(uint256 id, UserData memory newData) internal virtual {
_idOfName[this._toLowercase(newData.userName)] = id;
// ...
}
function _toLowercase(string memory input) public pure returns (string memory) {
// ...
}