NFTBridge
60,000 USDC
View results
Submission Details
Severity: low
Valid

`TokenUtil` Fails To Serialize TokenURIs When Handling Collections With Empty BaseURIs

Summary

When a collection exposes a publicly-accessible baseURI(), but it returns an empty string, the bridge fails to procure any meaningful metadata.

Vulnerability Details

When bridging ERC-721s, the Bridge will attempt to fetch the metadata for the token being bridged via a call to TokenUtil::erc721Metadata.

This operation handles the presence of a baseURI differently depending upon the token. If a baseURI (or a _baseURI) function is accessible and returns valid data, it will prefer to use this data when evaluating tokens instead of querying for each individual token URI manually:

(bool success, string memory _baseUri) = _callBaseUri(collection);
if (success) {
/// @audit If an attempt to call the `baseURI()` or `_baseURI()` function
/// @audit was successful, just return the evaluated `baseUri` instead of
/// @audit each individual token.
return (c.name(), c.symbol(), _baseUri, new string[](0));
}
else {
/// @audit If the collection doesn't define a `baseURI()` or `_baseURI()`,
/// @audit then we should iterate each individual token.
string[] memory URIs = new string[](tokenIds.length);
for (uint256 i = 0; i < tokenIds.length; i++) {
URIs[i] = c.tokenURI(tokenIds[i]);
}
return (c.name(), c.symbol(), "", URIs);
}

An oversight in this implementation is what happens when _callBaseUri indeed defines a baseURI() function, but that function returns an empty string.

If we look at the OpenZeppelin ERC-721 implementation, we can see that the baseURI() function is always present, regardless of whether a collection defines individual custom URIs per token or not.

Therefore, it is commonplace for many real-world collections to define a tokenURI() function which returns an empty string.

Even the project's own ERC721Bridgeable operates in this manner.

In the Proof of Concept below, we can determine that for an empty baseURI() function implementation, the invocation to _callBaseUri indeed returns successfully for an empty string:

TokenUtils.t.sol

contract TestWithUnderscoreBaseUri is IUnderscoreBaseUri {
function _baseUri() view external returns (string memory) {
return ""; /// @audit create a fake token which returns an empty string
}
}
function test_with_empty_underscorebaseuri() public {
bool success = false;
string memory uri;
address toCheck = address(new TestWithUnderscoreBaseUri());
(success, uri) = TokenUtil._callBaseUri(toCheck);
console.log(success);
console.log(uri.length);
}

forge test --match-test "test_with_empty_underscorebaseuri" -vv results in the output:

Ran 1 test for test/TokenUtils.t.sol:TokenUtilTest
[PASS] test_with_empty_underscorebaseuri() (gas: 79517)
Logs:
true
0
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 322.79µs (145.58µs CPU time)

Therefore, _callBaseUri returns successfully with an empty string.

Looking back to the implementation ofTokenUtil::erc721Metadata, we can see the empty string continues to satisfy the baseURI case, so we opt to not define an empty baseURI and an empty list of tokenURIs:

(bool success, string memory _baseUri) = _callBaseUri(collection);
if (success) { /// @audit we enter this case for empty baseURIs, and return no useful content
return (c.name(), c.symbol(), _baseUri, new string[](0));
}

This means no useful metadata is returned.

Impact

The majority of bridged tokens will emerge with non-emptytokenURIs, despite great efforts to the contrary.

Tools Used

Manual Review

Recommendations

If _callBaseUri returns successfully with an empty string, continue to process the token URIs individually as if it were not defined:

(bool success, string memory _baseUri) = _callBaseUri(collection);
/// @notice If we found a non-empty string baseURI, use this as the base.
if (success && bytes(_baseUri).length > 0)
return (c.name(), c.symbol(), _baseUri, new string[](0));
string[] memory URIs = new string[](tokenIds.length);
for (uint256 i = 0; i < tokenIds.length; i++) {
URIs[i] = c.tokenURI(tokenIds[i]);
}
return (c.name(), c.symbol(), "", URIs);

it should be stressed to the Ark team that the baseURI alone is not enough when trying to reconstruct tokens, since these can optionally be concatenated with custom metadata (and not simply the tokenID):

baseURI() → string

Returns the base URI set via _setBaseURI. This will be automatically added as a prefix in tokenURI to each token’s URI, or to the token ID if no specific URI is set for that token ID.

In this respect, it may be more accurate to unconditionally query individual tokenURIs for each token being bridged, regardless of the presence of a (non-standard) baseURI.

Updates

Lead Judging Commences

n0kto Lead Judge about 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

finding-empty-baseURI-do-not-use-tokenURI

Likelyhood: Low, baseURI is not in the ERC721 standard. Impact: Low, function won’t work when it has to. But URI can be set later.

Appeal created

auditweiler Submitter
about 1 year ago
n0kto Lead Judge
12 months ago
n0kto Lead Judge 12 months ago
Submission Judgement Published
Validated
Assigned finding tags:

finding-empty-baseURI-do-not-use-tokenURI

Likelyhood: Low, baseURI is not in the ERC721 standard. Impact: Low, function won’t work when it has to. But URI can be set later.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.