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

Checking `baseURI` value will succeed even if the returned string is empty

Vulnerability Details

When Bridging tokens we are getting their URIs. We first try to get the baseURI, and if it contains value we just return it and ignore getting each NFT tokenURI separately.

TokenUtil.sol#L89-L92

// How the URI must be handled.
// if a base URI is already present, we ignore individual URI
// else, each token URI must be bridged and then the owner of the collection
// can decide what to do

If we check how the returned string value is compared we will find out that the returnedValue will always pass the check.

TokenUtil.sol#L158

function _callBaseUri(address collection) ... {
...
for (uint256 i = 0; i < 2; i++) {
bytes memory encodedParams = encodedSignatures[i];
assembly {
❌️ success := staticcall(gas(), collection, add(encodedParams, 0x20), mload(encodedParams), 0x00, 0x20)
if success {
returnSize := returndatasize()
❌️ returnValue := mload(0x00)
...
}
}
❌️ if (success && returnSize >= 0x20 && returnValue > 0) {
return (true, abi.decode(ret, (string)));
}
}
return (false, "");
}
  • when we make the call we are storing the returned data at slot 0x00(returnOffset) with length 0x20(returnSize)

  • Then, we store this value in returnValue

  • Then, we compare it to be greater than 0

Now the issue is that the first 32 bytes in the returned data of a string variable is not its value nor its length it is the offset we are starting from.

To make it simpler if the string is < 32 bytes in length the returned bytes will be as follows:

[0x00 => 0x20]: String Length Offset
[0x20 => 0x40]: String Length value
[0x40 => 0x60]: String value in Hexa

Here is the returned bytes when the returned value is baseURI():

[000]: 0000000000000000000000000000000000000000000000000000000000000020
[020]: 0000000000000000000000000000000000000000000000000000000000000009
[040]: 6261736555524928290000000000000000000000000000000000000000000000

So when copying the first 32 bytes from slot 0x00 we are storing the offset not the length, which will make the value always be > 0, leading to pass the check even if baseUri() returned an empty string.

We should keep in mind that an empty string base URI is the default value for the base URI according to ERC721, so returning an empty string means it is not set, and we should not treat it as a valid baseURI.

Impact

Passing baseURI even if it returns an empty string (not present), and not getting each tokenURIs value separately, which is not how the function should work.

Tools Used

Manual Review and Foundry

Recommendations

Assign returnValue to be the string length value, not the offset loading the value from the offset.

This can be made by copying the first 0x40 bytes of the string, where the first 0x20 bytes will contain the offset and the second 0x20 byteswill contain the length (in normal Solidity encoding).

So we will store 0x40 bytes at memory slot 0x00 this is normal as 0x00 and 0x20 are not used, and then mload(0x20) to get the length.

diff --git a/apps/blockchain/ethereum/src/token/TokenUtil.sol b/apps/blockchain/ethereum/src/token/TokenUtil.sol
index 41cc17d..1c0a26c 100644
--- a/apps/blockchain/ethereum/src/token/TokenUtil.sol
+++ b/apps/blockchain/ethereum/src/token/TokenUtil.sol
@@ -152,10 +152,10 @@ library TokenUtil {
for (uint256 i = 0; i < 2; i++) {
bytes memory encodedParams = encodedSignatures[i];
assembly {
- success := staticcall(gas(), collection, add(encodedParams, 0x20), mload(encodedParams), 0x00, 0x20)
+ success := staticcall(gas(), collection, add(encodedParams, 0x20), mload(encodedParams), 0x00, 0x40)
if success {
returnSize := returndatasize()
- returnValue := mload(0x00)
+ returnValue := mload(0x20)
ret := mload(0x40)
mstore(ret, returnSize)
returndatacopy(add(ret, 0x20), 0, returnSize)
Updates

Lead Judging Commences

n0kto Lead Judge 9 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.