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

`L2Bridge` is incompatible with ERC721 that returns `felt252` for strings

Vulnerability Details

When retrieving ERC721 collection metadata values (name, symbol, token_uri), we are retrieving their returned data to be ByteArray. Since these functions will return string values we need to understand what is the data type of string value in Cairo.

Cairo is a low-level programming language, it does not support strings, and strings are represented using either felt252 or ByteArray.

If the string length is smaller than 31 length it can be represented in only one felt252.

First we need to understand why there are two types for retrieving strings.

The original type was normal felt252, but on March 2024 Starknet introduced ByteArray type.

This means that all NFT collections (ERC721 tokens) are created from the launching of the Starknet blockchain till the type of supporting ByteArray having an interface that returns the name, symbol, and token_uri as felt252.

OpenZepeplin starknet contract versions from the beginning to v0.9.0 use felt252 for their ERC721 tokens.

OZ::0.9.0::erc721.cairo#L219-L221

fn name(self: @ComponentState<TContractState>) -> felt252 {
self.ERC721_name.read()
}

And from v0.10.0 to v0.15.0 they use ByteArray. The protocol uses v0.11.0, which assigns the returned value to ByteArray.

OZ::0.11.0::erc721.cairo#L221-L223

fn name(self: @ComponentState<TContractState>) -> ByteArray {
self.ERC721_name.read()
}

The Starknet launched November 2021 or February 2022, and ByteArray was supported on March 2024. so all ERC721 tokens created in these 2 years have an interface that returns felt252 for name, symbol, and token_uri.

The protocol is not handling such a case and deals with NFTs on layer2 as they return ByteArray when calling name, symbol, which is totally incorrect, as the majority of NFTs were created before supporting ByteArray so dealing with the returned value when calling <collection>.name() will only work for ERC721 tokens that was created after supporting ByteArray, and use it.

When making calls there are two types of calls in starknet:

  1. low-level call: similar to .call in solidity.

  2. High-level calls: interface calls.

In solidity the returned data is in Bytes whatever its type was, the same is in Starknet when doing low-level call the returned type is Span what ever the type was.

We are handling retrieving token_uri correctly, where we are doing an internal call when getting them, which result in a return value as Span<felt252>

collection_manager.cairo#L107-L123

match starknet::call_contract_syscall(
collection_address,
token_uri_selector,
calldata,
) {
❌️ Result::Ok(span) => span.try_into(),
Result::Err(_e) => {
match starknet::call_contract_syscall(
collection_address, tokenURI_selector, calldata,
) {
❌️ Result::Ok(span) => span.try_into(),
Result::Err(_e) => {
Option::None
}
}
}
}

Since the returned value is felt252, we will convert it into ByteArray without problems and there is a custom implementation for try_into than handles converting felt252 into ByteArray correctly in byte_array_extra.

The issue is that when getting token name and symbol we are retrieving them with High-level call.

collection_manager.cairo#L69-L70

#[derive(Drop)]
struct ERC721Metadata {
name: ByteArray,
symbol: ByteArray,
base_uri: ByteArray,
uris: Span<ByteArray>,
}
...
Option::Some(
ERC721Metadata {
❌️ name: erc721.name(),
❌️ symbol: erc721.symbol(),
base_uri: "",
uris
}
)

The interface we are defining ERC721Metadata declares string values as ByteArray we explained why uris will get handled correctly, and for base_uri it is written manually, but for name() and symbol() we are calling them directly and expect the returned value to be of type ByteArray.

erc721 is the collection address we are going to Bridge the token from it from L2 to L1, and since not all NFT collections on L2, but actually the majority of NFT collections on L2 return felt252, making erc721.name() will result in an exception because of incorrect assigning. the execution will stop even we have an Option return value because this is an execution error that occurred before reaching the end of the function.

Proof of Concept

Doing a POC for such an issue is complicated, but to make it achievable we separated it into two parts:

  1. In the Starknet development environment make another erc721_collection that returns name(), symbol(), token_uri() as strings. Achieving this needs a lot of modifications to erc721_collection.cairo file so we made another file erc721_collection_2.cairo and make it as an NFT collection that returns felt252.

  2. Add the following test script in the last of apps/blockchain/starknet/src/tests/bridge_t.cairo.

fn deploy_erc721b_old(
erc721b_contract_class: ContractClass,
name: felt252,
symbol: felt252,
bridge_addr: ContractAddress,
collection_owner: ContractAddress,
) -> ContractAddress {
let mut calldata: Array<felt252> = array![];
let base_uri: felt252 = 'https://my.base.uri/';
name.serialize(ref calldata);
symbol.serialize(ref calldata);
base_uri.serialize(ref calldata);
calldata.append(bridge_addr.into());
calldata.append(collection_owner.into());
erc721b_contract_class.deploy(@calldata).unwrap()
}
#[test]
fn deposit_token_auditor_old_erc721() {
// Need to declare here to get the class hash before deploy anything.
let erc721b_contract_class = declare("erc721_bridgeable");
let erc721b_old_contract_class = declare("erc721_bridgeable_2");
let BRIDGE_ADMIN = starknet::contract_address_const::<'starklane'>();
let BRIDGE_L1 = EthAddress { address: 'starklane_l1' };
let COLLECTION_OWNER = starknet::contract_address_const::<'collection owner'>();
let OWNER_L1 = EthAddress { address: 'owner_l1' };
let bridge_address = deploy_starklane(BRIDGE_ADMIN, BRIDGE_L1, erc721b_contract_class.class_hash);
let erc721b_address = deploy_erc721b_old(
erc721b_old_contract_class,
'everai',
'DUO',
bridge_address,
COLLECTION_OWNER
);
let erc721 = IERC721Dispatcher { contract_address: erc721b_address };
mint_range(erc721b_address, COLLECTION_OWNER, COLLECTION_OWNER, 0, 10);
let bridge = IStarklaneDispatcher { contract_address: bridge_address };
start_prank(CheatTarget::One(erc721b_address), COLLECTION_OWNER);
erc721.set_approval_for_all(bridge_address, true);
stop_prank(CheatTarget::One(erc721b_address));
start_prank(CheatTarget::One(bridge_address), COLLECTION_OWNER);
println!("We will call bridge.deposit_tokens()...");
// This should revert
bridge.deposit_tokens(
0x123,
erc721b_address,
OWNER_L1,
array![0, 1].span(),
false,
false);
println!("bridge.deposit_tokens() finished calling successfully");
stop_prank(CheatTarget::One(bridge_address));
}

Run the following command.

snforge test deposit_token_auditor_old_erc721

Output

[FAIL] starklane::tests::bridge_t::tests::deposit_token_auditor_old_erc721
Failure data:
Got an exception while executing a hint: Hint Error: 0x4661696c656420746f20646573657269616c697a6520706172616d202331 ('Failed to deserialize param #1')

NOTE: To not make the report too large, we mentioned in point 1 what we need in order for the POC to work, all you have to do is to deploy an erc721_collection that returns felt252 for name(), and we preferred to leave this to you as mention how to make this needs a lot of modifications. And for the POC, you need to make sure that you are deploying the correct address, our POC works when there are two contracts erc721_collection and erc721_collection_2 and the 2 version is the one that returns felt252, you need to import these contract and make sure to add them in order to get them in artificats and not receive path errors. And ofc having the sponsor with you when setting it up is the best

Impact

  • The majourity of NFTs on L2, which returns felt252 when calling name() or symbol() can't get briged.

Tools Used

Manual review + Starknet Foundry

Recommendations

reterive erc721.name() and erc721.symbol() using low-level to get the result as Span<felt252> then convert them into ByteArray.

Updates

Lead Judging Commences

n0kto Lead Judge 9 months ago
Submission Judgement Published
Validated
Assigned finding tags:

finding-openzeppelin-version<0.10.0-felt-instead-byteArrays

Impact: Medium/High, no token loss but protocol do not work with a lot of ERC721 collections. Likelyhood: Very High, old collections created before the 08 Mar 2024 will not work, and according to those statistics, it is the huge majority of them: <https://dune.com/starknet_foundation/starknet-activity>

Appeal created

alqaqa Submitter
9 months ago
alqaqa Submitter
9 months ago
n0kto Lead Judge
9 months ago
n0kto Lead Judge 9 months ago
Submission Judgement Published
Validated
Assigned finding tags:

finding-openzeppelin-version<0.10.0-felt-instead-byteArrays

Impact: Medium/High, no token loss but protocol do not work with a lot of ERC721 collections. Likelyhood: Very High, old collections created before the 08 Mar 2024 will not work, and according to those statistics, it is the huge majority of them: <https://dune.com/starknet_foundation/starknet-activity>

Support

FAQs

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