Canto Identity Subprotocols contest - Jerry0x's results

Subprotocols for Canto Identity Protocol.

General Information

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

Canto Identity Subprotocols

Findings Distribution

Researcher Performance

Rank: 19/98

Findings: 3

Award: $209.14

QA:
grade-a
Gas:
grade-b

🌟 Selected for report: 0

πŸš€ Solo Findings: 0

Findings Information

Awards

19.8705 USDC - $19.87

Labels

bug
2 (Med Risk)
satisfactory
duplicate-212

External Links

Lines of code

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

Vulnerability details

Impact

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^

Proof of Concept

_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="}

Tools Used

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

Awards

177.2442 USDC - $177.24

Labels

bug
downgraded by judge
grade-a
QA (Quality Assurance)
satisfactory
duplicate-145
Q-30

External Links

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

https://github.com/code-423n4/2023-03-canto-identity/blob/main/canto-namespace-protocol/src/Tray.sol#L57-L64

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

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax Β© 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter