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

Incorrect linked-list implementation on L2 prevents admin from removing collection from whitelist

Summary

The linked-list for the whitelist on L2 is incorrectly implemented, preventing admins from removing any collections after the second one from the linked list (whitelist).

Vulnerability Details

Currently the whitelist on L2 is implemented as a linked list. This implementation adds new elements at the end of it and allows for removing arbitrary elements and always being up to date with which collections are whitelisted.
To add/remove elements, the function _white_list_collection is used with the enabled flag set to either true for adding or false for removing.

fn _white_list_collection(ref self: ContractState, collection: ContractAddress, enabled: bool) {
let no_value = starknet::contract_address_const::<0>();
let (current, _) = self.white_listed_list.read(collection);
if current != enabled {
let mut prev = self.white_listed_head.read();
if enabled {
// [...] adding element here
} else {
// change head
if prev == collection {
let (_, next) = self.white_listed_list.read(prev);
self.white_listed_list.write(collection, (false, no_value));
self.white_listed_head.write(next);
return;
}
// removed element from linked list
loop {
let (active, next) = self.white_listed_list.read(prev); // [1]
if next.is_zero() {
// end of list
break;
}
if !active {
break;
}
if next == collection {
let (_, target) = self.white_listed_list.read(collection);
self.white_listed_list.write(prev, (active, target));
break;
}
// @audit-issue endless loop as we do not update prev
};
self.white_listed_list.write(collection, (false, no_value));
}
}
}

The logic for adding an element is fine but if we look at the part where we remove elements from the list, there is an issue.
Going through the logic, we want to do the following:

  • check if the element to remove is the head

    • if this is the case, we just update the head to point to the previous head's next element

    • if this is NOT the case, we loop over the list, looking for collection

  • once we find our collection in the list we unlink (remove) it from the list

  • if we do not find the collection in the list, we just return

Now the problem lies in the loop that traverses the linked list. More concretely, the prev element never gets updated to the current found element.
This means that we do the following:

  1. check the head as above

  2. if we enter the loop, we check the head's next element (as seen at [1])

  3. we do NOT update the prev element so once we get to the next iteration, we basically go to 2) again, checking the head's next element again

This means we are stuck in an endless loop, meaning execution of the function will fail.

Impact

The impact of this is that admins cannot remove collections after the second one from the whitelist. This is quite severe as adding and removing collections from the whitelist is a critical part of the bridge.
Since the bridge is expected to be usable for more tokens other than Everai, this will be the case.

Proof of Concept

In order to show that elements after the second cannot be removed, please add the following test to bridge_t.cairo and execute it with snforge test third_whitelisted_cannot_be_removed from the apps/blockchain/starknet/ directory. This will fail showing that we cannot remove the wanted collection from the whitelist.

#[test]
fn third_whitelisted_cannot_be_removed() {
let erc721b_contract_class = declare("erc721_bridgeable");
let BRIDGE_ADMIN = starknet::contract_address_const::<'starklane'>();
let BRIDGE_L1 = EthAddress { address: 'starklane_l1' };
let bridge_address = deploy_starklane(BRIDGE_ADMIN, BRIDGE_L1, erc721b_contract_class.class_hash);
let bridge = IStarklaneDispatcher { contract_address: bridge_address };
start_prank(CheatTarget::One(bridge_address), BRIDGE_ADMIN);
bridge.enable_white_list(true);
stop_prank(CheatTarget::One(bridge_address));
let collection1 = starknet::contract_address_const::<'collection1'>();
let collection2 = starknet::contract_address_const::<'collection2'>();
let collection3 = starknet::contract_address_const::<'collection3'>();
start_prank(CheatTarget::One(bridge_address), BRIDGE_ADMIN);
bridge.white_list_collection(collection1, true);
stop_prank(CheatTarget::One(bridge_address));
let white_listed = bridge.get_white_listed_collections();
assert_eq!(white_listed.len(), 1, "White list shall contain 1 element");
assert_eq!(*white_listed.at(0), collection1, "Wrong collection address in white list");
start_prank(CheatTarget::One(bridge_address), BRIDGE_ADMIN);
bridge.white_list_collection(collection2, true);
bridge.white_list_collection(collection3, true);
stop_prank(CheatTarget::One(bridge_address));
let white_listed = bridge.get_white_listed_collections();
assert_eq!(white_listed.len(), 3, "White list shall contain 3 elements");
assert_eq!(*white_listed.at(0), collection1, "Wrong collection address in white list");
assert_eq!(*white_listed.at(1), collection2, "Wrong collection address in white list");
assert_eq!(*white_listed.at(2), collection3, "Wrong collection address in white list");
assert!(bridge.is_white_listed(collection1), "Collection1 should be whitelisted");
assert!(bridge.is_white_listed(collection2), "Collection2 should be whitelisted");
assert!(bridge.is_white_listed(collection3), "Collection3 should be whitelisted");
println!("Before trying to remove collection3");
start_prank(CheatTarget::One(bridge_address), BRIDGE_ADMIN);
bridge.white_list_collection(collection3, false);
stop_prank(CheatTarget::One(bridge_address));
println!("After removing collection3");
let collection_none = starknet::contract_address_const::<0>();
assert_eq!(white_listed.len(), 2, "White list shall contain 2 elements");
assert_eq!(*white_listed.at(0), collection1, "Wrong collection address in white list");
assert_eq!(*white_listed.at(1), collection2, "Wrong collection address in white list");
assert_eq!(*white_listed.at(2), collection_none, "Wrong collection address in white list");
}

Tools Used

Manual review

Recommended Mitigation

This can be easily fixed by updating the prev element when traversing the list by adding a single line like so:

fn _white_list_collection(ref self: ContractState, collection: ContractAddress, enabled: bool) {
let no_value = starknet::contract_address_const::<0>();
let (current, _) = self.white_listed_list.read(collection);
if current != enabled {
let mut prev = self.white_listed_head.read();
if enabled {
self.white_listed_list.write(collection, (enabled, no_value));
if prev.is_zero() {
self.white_listed_head.write(collection);
return;
}
// find last element
loop {
let (_, next) = self.white_listed_list.read(prev);
if next.is_zero() {
break;
}
let (active, _) = self.white_listed_list.read(next);
if !active {
break;
}
prev = next;
};
self.white_listed_list.write(prev, (true, collection));
} else {
// change head
if prev == collection {
let (_, next) = self.white_listed_list.read(prev);
self.white_listed_list.write(collection, (false, no_value));
self.white_listed_head.write(next);
return;
}
// removed element from linked list
loop {
let (active, next) = self.white_listed_list.read(prev);
if next.is_zero() {
// end of list
break;
}
if !active {
break;
}
if next == collection {
let (_, target) = self.white_listed_list.read(collection);
self.white_listed_list.write(prev, (active, target));
break;
}
@> prev = next; // <-----
};
self.white_listed_list.write(collection, (false, no_value));
}
}
}
Updates

Lead Judging Commences

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

finding-L2-unwhitelist-from-third collection-impossible

Likelyhood: High, owner can only unwhitelist the 2 first collections. Impact: Medium/High, owner has to empty the list to remove any collection, and replace all the new ones.

Support

FAQs

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