Summary
An attacker could continuously bridge L2 NFTs to L1, causing the _collections
array to grow excessively and potentially leading to an out-of-gas DoS attack when users calling withdrawTokens
.
Vulnerability Details
When users bridge an NFT collection from L2 to L1 and the corresponding L1 collection does not exist, it will be created, added to the whitelist, and included in the _collections
array by calling _whiteListCollection
.
https://github.com/Cyfrin/2024-07-ark-project/blob/main/apps/blockchain/ethereum/src/Bridge.sol#L183-L192
function withdrawTokens(
uint256[] calldata request
)
external
payable
returns (address)
{
address collectionL1 = _verifyRequestAddresses(req.collectionL1, req.collectionL2);
CollectionType ctype = Protocol.collectionTypeFromHeader(header);
if (collectionL1 == address(0x0)) {
if (ctype == CollectionType.ERC721) {
collectionL1 = _deployERC721Bridgeable(
req.name,
req.symbol,
req.collectionL2,
req.hash
);
>>> _whiteListCollection(collectionL1, true);
} else {
revert NotSupportedYetError();
}
}
}
https://github.com/Cyfrin/2024-07-ark-project/blob/main/apps/blockchain/ethereum/src/Bridge.sol#L340-L356
function _whiteListCollection(address collection, bool enable) internal {
if (enable && !_whiteList[collection]) {
bool toAdd = true;
uint256 i = 0;
while(i < _collections.length) {
if (collection == _collections[i]) {
toAdd = false;
break;
}
i++;
}
if (toAdd) {
>>> _collections.push(collection);
}
}
_whiteList[collection] = enable;
}
If the whitelist functionality is disabled on L2, an attacker could repeatedly call deposit_tokens
with newly created NFT collections. Additionally, because there is no minimum token_ids
check, the attacker can provide empty token_ids
to make the attack even cheaper.
https://github.com/Cyfrin/2024-07-ark-project/blob/main/apps/blockchain/starknet/src/bridge.cairo#L242-L306
fn deposit_tokens(
ref self: ContractState,
salt: felt252,
collection_l2: ContractAddress,
owner_l1: EthAddress,
token_ids: Span<u256>,
use_withdraw_auto: bool,
use_deposit_burn_auto: bool,
) {
let collection_l1 = self.l2_to_l1_addresses.read(collection_l2);
let req = Request {
header: compute_request_header_v1(ctype, use_deposit_burn_auto, use_withdraw_auto),
hash: compute_request_hash(salt, collection_l2, owner_l1, token_ids),
collection_l1,
collection_l2,
owner_l1,
owner_l2: from,
name,
symbol,
base_uri,
ids: token_ids,
values: array![].span(),
uris,
new_owners: array![].span(),
};
let mut buf: Array<felt252> = array![];
req.serialize(ref buf);
starknet::send_message_to_l1_syscall(
self.bridge_l1_address.read().into(),
buf.span(),
)
.unwrap_syscall();
self.emit(DepositRequestInitiated {
hash: req.hash,
block_timestamp: starknet::info::get_block_timestamp(),
req_content: req
});
}
This scenario could also eventually occur without any malicious intent if the whitelist is disabled and enough different collections are added on L1, leading to an out-of-gas situation and potentially bricking the withdrawTokens
functionality.
Impact
the withdrawTokens
functionality will be DoSed.
Tools Used
Manual review
Recommendations
Remove the addition of newly created collections to _collections
within with drawTokens
to avoid this situation, and add functionality to remove collections from _collections
.