Summary
The _callBaseUri
function performs a staticcall
to the ERC721 contract to check if it implements either the _baseUri()
or baseUri
function and returns a valid string(URI). It will return true
if the function call is successful, but it will also return true
if the function returns an empty string ""
.
Vulnerability Details
Inside _callBaseUri
, the Ark Project code will use staticcall
to check if the ERC721 contract implements either _baseUri
or baseUri
. The staticcall
will not revert if the ERC721 contract does not implement the called functions.
function _callBaseUri(
address collection
)
internal
view
returns (bool, string memory)
{
bool success;
uint256 returnSize;
uint256 returnValue;
bytes memory ret;
bytes[2] memory encodedSignatures = [abi.encodeWithSignature("_baseUri()"), abi.encodeWithSignature("baseUri()")];
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)
ret := mload(0x40)
mstore(ret, returnSize)
returndatacopy(add(ret, 0x20), 0, returnSize)
mstore(0x40, add(add(ret, 0x20), add(returnSize, 0x20)))
}
}
@1>-- if (success && returnSize >= 0x20 && returnValue > 0) {
@>2-- return (true, abi.decode(ret, (string)));
}
}
return (false, "");
}
One thing to note is that the default implementation of _baseURI
will return an empty string (""
). If such an ERC721 collection is added, all its NFTs metadata will be lost on Layer 2 Starknet due to incorrect checks @1>--
and @>2--
.
POC:
Add following simple code to ethereum/test/TestUri.t.sol
folder:
pragma solidity 0.8.22;
import "forge-std/Test.sol";
contract ERC721Contract {
constructor() {}
function baseUri() external returns (string memory) {
return "";
}
}
contract TestUri is Test {
ERC721Contract public contractERC721;
function setUp() external {
contractERC721 = new ERC721Contract();
}
function _callBaseUri() internal returns (bool, string memory) {
ERC721Contract _erc721TestContract = contractERC721;
bool success;
uint256 returnSize;
uint256 returnValue;
bytes memory ret;
bytes[2] memory encodedSignatures = [
abi.encodeWithSignature("_baseUri()"),
abi.encodeWithSignature("baseUri()")
];
for (uint256 i = 0; i < 2; i++) {
bytes memory encodedParams = encodedSignatures[i];
assembly {
success := staticcall(
gas(),
_erc721TestContract,
add(encodedParams, 0x20),
mload(encodedParams),
0x00,
0x20
)
if success {
returnSize := returndatasize()
returnValue := mload(0x00)
ret := mload(0x40)
mstore(ret, returnSize)
returndatacopy(add(ret, 0x20), 0, returnSize)
mstore(0x40, add(add(ret, 0x20), add(returnSize, 0x20)))
}
}
if (success && returnSize >= 0x20 && returnValue > 0) {
return (true, abi.decode(ret, (string)));
}
}
return (false, "");
}
function test_no_uri_true() external {
(bool success, string memory val) = checkCase();
assertEq(val, "");
assertEq(success, true);
}
}
run with command : forge test --mt test_no_uri_true -vvv
Impact
The _callBaseUri
function will return true
even if the baseUri
function returns an empty string (""
). This can result in the loss of NFTs metadata on Layer 2 due to the absence of a URI attached to the NFT.
Tools Used
Manual Review , Foundry
Recommendations
Check that the value returned from the low-level call is a valid string and that its length is greater than 0. One potential fix would be to add the following check when success
is true
:
@@ -164,7 +164,9 @@ library TokenUtil {
}
}
if (success && returnSize >= 0x20 && returnValue > 0) {
- return (true, abi.decode(ret, (string)));
+ string memory s = abi.decode(ret, (string));
+ if(bytes(s).length > 0 ) return (true , s);
+ else return (false, "");
}
}