Platform: Code4rena
Start Date: 17/03/2023
Pot Size: $36,500 USDC
Total HM: 10
Participants: 98
Period: 3 days
Judge: leastwood
Total Solo HM: 5
Id: 223
League: ETH
Rank: 19/98
Findings: 3
Award: $209.14
π Selected for report: 0
π Solo Findings: 0
19.8705 USDC - $19.87
https://github.com/code-423n4/2023-03-canto-identity/blob/main/canto-bio-protocol/src/Bio.sol#L121-L123 https://github.com/code-423n4/2023-03-canto-identity/blob/main/canto-bio-protocol/src/Bio.sol#L43-L44
The special character filtering was not applied to the input _bio, resulting in tokenURI returning data that cannot be correctly parsed into JSON and SVG formats. For example, the input: ΝΊgmqU^
_bio = ΝΊgmqU^
function mint(string calldata _bio) external { // We check the length in bytes, so will be higher for UTF-8 characters. But sufficient for this check if (bytes(_bio).length == 0 || bytes(_bio).length > 200) revert InvalidBioLength(bytes(_bio).length); uint256 tokenId = ++numMinted; bio[tokenId] = _bio; _mint(msg.sender, tokenId); emit BioAdded(msg.sender, tokenId, _bio); }
function tokenURI(uint256 _id) public view override returns (string memory) { if (_ownerOf[_id] == address(0)) revert TokenNotMinted(_id); string memory bioText = bio[_id]; bytes memory bioTextBytes = bytes(bioText); uint lengthInBytes = bioTextBytes.length; // Insert a new line after 40 characters, taking into account unicode character uint lines = (lengthInBytes - 1) / 40 + 1; string[] memory strLines = new string[](lines); bool prevByteWasContinuation; uint256 insertedLines; // Because we do not split on zero-width joiners, line in bytes can technically be much longer. Will be shortened to the needed length afterwards bytes memory bytesLines = new bytes(80); uint bytesOffset; for (uint i; i < lengthInBytes; ++i) { bytes1 character = bioTextBytes[i]; bytesLines[bytesOffset] = character; bytesOffset++; if ((i > 0 && (i + 1) % 40 == 0) || prevByteWasContinuation || i == lengthInBytes - 1) { bytes1 nextCharacter; if (i != lengthInBytes - 1) { nextCharacter = bioTextBytes[i + 1]; } if (nextCharacter & 0xC0 == 0x80) { // Unicode continuation byte, top two bits are 10 prevByteWasContinuation = true; } else { // Do not split when the prev. or next character is a zero width joiner. Otherwise, π¨βπ§βπ¦ could become π¨>βπ§βπ¦ // Furthermore, do not split when next character is skin tone modifier to avoid π€¦ββοΈ\nπ» if ( // Note that we do not need to check i < lengthInBytes - 4, because we assume that it's a valid UTF8 string and these prefixes imply that another byte follows (nextCharacter == 0xE2 && bioTextBytes[i + 2] == 0x80 && bioTextBytes[i + 3] == 0x8D) || (nextCharacter == 0xF0 && bioTextBytes[i + 2] == 0x9F && bioTextBytes[i + 3] == 0x8F && uint8(bioTextBytes[i + 4]) >= 187 && uint8(bioTextBytes[i + 4]) <= 191) || (i >= 2 && bioTextBytes[i - 2] == 0xE2 && bioTextBytes[i - 1] == 0x80 && bioTextBytes[i] == 0x8D) ) { prevByteWasContinuation = true; continue; } assembly { mstore(bytesLines, bytesOffset) } strLines[insertedLines++] = string(bytesLines); bytesLines = new bytes(80); prevByteWasContinuation = false; bytesOffset = 0; } } } string memory svg = '<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet" viewBox="0 0 400 100"><style>text { font-family: sans-serif; font-size: 12px; }</style>'; string memory text = '<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle">'; for (uint i; i < lines; ++i) { text = string.concat(text, '<tspan x="50%" dy="20">', strLines[i], "</tspan>"); } string memory json = Base64.encode( bytes( string.concat( '{"name": "Bio #', LibString.toString(_id), '", "description": "', bioText, '", "image": "data:image/svg+xml;base64,', Base64.encode(bytes(string.concat(svg, text, "</text></svg>"))), '"}' ) ) ); return string(abi.encodePacked("data:application/json;base64,", json)); }
decode tokenURI
{"name": "Bio #1", "description": "ΝΊgmqU^", "image": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaW5ZTWluIG1lZXQiIHZpZXdCb3g9IjAgMCA0MDAgMTAwIj48c3R5bGU+dGV4dCB7IGZvbnQtZmFtaWx5OiBzYW5zLXNlcmlmOyBmb250LXNpemU6IDEycHg7IH08L3N0eWxlPjx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBkb21pbmFudC1iYXNlbGluZT0ibWlkZGxlIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj48dHNwYW4geD0iNTAlIiBkeT0iMjAiPs36m5BntRRtcb9VBl72oo08L3RzcGFuPjwvdGV4dD48L3N2Zz4="}
Foundry
Filter or process special characters.
#0 - c4-judge
2023-03-28T03:57:02Z
0xleastwood marked the issue as duplicate of #212
#1 - c4-judge
2023-04-11T19:35:33Z
0xleastwood marked the issue as satisfactory
π Selected for report: Sathish9098
Also found by: 0xAgro, 0xSmartContract, 0xdaydream, 0xnev, Awesome, Aymen0909, BRONZEDISC, Bauchibred, Deathstore, Diana, IceBear, Jerry0x, Kresh, Matin, Rolezn, Stryder, T1MOH, Udsen, adriro, alejandrocovrr, atharvasama, codeslide, cryptonue, descharre, igingu, jack, joestakey, libratus, lukris02, luxartvinsec, nadin, nasri136, reassor, scokaf, shark, slvDev, tnevler
177.2442 USDC - $177.24
https://github.com/code-423n4/2023-03-canto-identity/blob/main/canto-namespace-protocol/src/Namespace.sol#L113
uint256 fusingCosts = 2**(13 - numCharacters) * 1e18;
To
uint256 fusingCosts = 2**(13 - numCharacters) * 10**note.decimals();
Or modify the changeNoteAddress function.
function changeNoteAddress(address _newNoteAddress) external onlyOwner { require(ERC20(_newNoteAddress).decimals() == 18, "Invalid decimals for $NOTE token"); address currentNoteAddress = address(note); note = ERC20(_newNoteAddress); emit NoteAddressUpdate(currentNoteAddress, _newNoteAddress); }
It is impossible to determine whether the modified ERC20 decimal places are the same.
#0 - 0xleastwood
2023-04-11T16:02:59Z
Duplicate of #145
#1 - c4-judge
2023-04-11T20:44:48Z
0xleastwood changed the severity to 2 (Med Risk)
#2 - c4-judge
2023-04-11T20:44:48Z
0xleastwood changed the severity to 2 (Med Risk)
#3 - c4-judge
2023-04-11T20:45:00Z
0xleastwood marked the issue as duplicate of #145
#4 - c4-judge
2023-04-11T20:45:05Z
0xleastwood marked the issue as satisfactory
#5 - c4-judge
2023-04-18T23:04:04Z
0xleastwood changed the severity to QA (Quality Assurance)
#6 - c4-judge
2023-04-18T23:05:19Z
This previously downgraded issue has been upgraded by 0xleastwood
#7 - c4-judge
2023-04-18T23:05:45Z
0xleastwood changed the severity to QA (Quality Assurance)
#8 - c4-judge
2023-04-18T23:13:40Z
0xleastwood marked the issue as grade-a
π Selected for report: 0xSmartContract
Also found by: 0xdaydream, 0xnev, Aymen0909, Deekshith99, Diana, EvanW, Fanz, JCN, Jerry0x, K42, Kresh, Madalad, MiniGlome, Polaris_tow, Rageur, ReyAdmirado, Rolezn, SAAJ, SaeedAlipoor01988, Sathish9098, Shubham, Udsen, Viktor_Cortess, Walter, anodaram, arialblack14, atharvasama, caspersolangii, codeslide, descharre, fatherOfBlocks, felipe, ginlee, igingu, lukris02, nadin, slvDev, tnevler, turvy_fuzz, viking71
12.034 USDC - $12.03
struct TileData { /// @notice Allowed values between 0 (emoji) and 9 (font5 rare) uint8 fontClass; /// @notice For Emojis (font class 0) between 0..NUM_CHARS_EMOJIS - 1, otherwise between 0..NUM_CHARS_LETTERS - 1 uint16 characterIndex; /// @notice For generative fonts with randomness (Zalgo), we generate and fix this on minting. For some emojis, it can be set by the user to influence the skin color uint8 characterModifier; }
To
struct TileData { /// @notice Allowed values between 0 (emoji) and 9 (font5 rare) uint8 fontClass; /// @notice For Emojis (font class 0) between 0..NUM_CHARS_EMOJIS - 1, otherwise between 0..NUM_CHARS_LETTERS - 1 uint16 characterIndex; /// @notice For generative fonts with randomness (Zalgo), we generate and fix this on minting. For some emojis, it can be set by the user to influence the skin color uint232 characterModifier; }
Make the structure occupy exactly uint256.
Gas decreased from 231292 to 231208.
#0 - c4-judge
2023-04-11T00:18:23Z
0xleastwood marked the issue as grade-b