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

ETH can be locked into the contract with the withdrawalTokens() function leading to loss of funds

Summary

The issue was found in the Starklane smart contract under the withdrawTokens function, where the function is marked as payable but does not correctly handle cases where ETH is sent to it. This could potentially lead to unexpected behaviors or the loss of funds if ETH is sent without proper handling.

Vulnerability Details

The vulnerability stems from the fact that the withdrawTokens function is marked as payable, allowing ETH to be sent along with the transaction. However, the contract does not have mechanisms in place to utilize, refund, or explicitly reject the ETH, which means it can accumulate in the contract without any intended purpose.

Impact

A user could accidentally send ETH, which would lead to the funds being effectively locked and unrecoverable.

Proof of Code:

Step 1) Set up the testing contract:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
import "openzeppelin-contracts/contracts/token/ERC721/IERC721.sol";
import "openzeppelin-contracts/contracts/token/ERC1155/IERC1155.sol";
import "forge-std/Test.sol";
import "../src/IStarklane.sol";
import "../src/IStarklaneEvent.sol";
import "../src/Bridge.sol";
import "../src/sn/Cairo.sol";
import "../src/sn/StarknetMessagingLocal.sol";
import "../src/sn/IStarknetMessagingLocal.sol";
import "../src/token/Deployer.sol";
import "../src/token/TokenUtil.sol";
import "./utils/Users.sol";
import "./token/ERC721MintFree.sol";
import "./token/IERC721MintRangeFree.sol";
/**
* @title Bridge testing.
*/
contract BridgeTest is Test, IStarklaneEvent {
address bridge;
address erc721C1;
address erc1155C1;
address snCore;
address payable internal alice;
address payable internal bob;
//
function setUp() public {
Users genusers = new Users();
address payable[] memory users = genusers.create(5);
alice = users[0];
bob = users[1];
// "Regular" collections, not controlled by the bridge.
erc721C1 = address(new ERC721MintFree("name 1", "S1"));
snCore = address(new StarknetMessagingLocal());
address impl = address(new Starklane());
bytes memory dataInit =
abi.encodeWithSelector(Starklane.initialize.selector, abi.encode(address(this), snCore, 0x1, 0x2));
bridge = address(new ERC1967Proxy(impl, dataInit));
IStarklane(bridge).enableBridge(true);
}

Step 2) Set up the function to test. The function will send ETH, effectively locking it away:

/*
This function tests that ETH can be sent to the bridge thru the withdrawTokens
function and therefore locked in the bridge.
*/
function test_withdrawTokensAndLockETH() public {
// Build the request and compute its "would be" message hash.
felt252 header = Protocol.requestHeaderV1(CollectionType.ERC721, false, false);
Request memory req = buildRequestDeploy(header, 888, bob);
uint256[] memory reqSerialized = Protocol.requestSerialize(req);
bytes32 msgHash = computeMessageHashFromL2(reqSerialized);
uint256[] memory hashes = new uint256[](1);
hashes[0] = uint256(msgHash);
IStarknetMessagingLocal(snCore).addMessageHashesFromL2(hashes);
// Send 1 ETH to the bridge with the withdrawTokens function
uint256 withdrawAmount = 1 ether;
vm.deal(bob, withdrawAmount);
vm.prank(bob);
address collection = IStarklane(bridge).withdrawTokens{value: 1 ether}(reqSerialized);
assertEq(IERC721(collection).ownerOf(888), bob);
// Check if ETH is locked in the bridge
uint256 bridgeBalance = address(bridge).balance;
console.log("Bridge balance: %s", bridgeBalance);
assertEq(bridgeBalance, withdrawAmount, "ETH was not locked in the bridge");
}

Tools Used

Manual review

Recommendations

  1. If the contract is not intended to receive ETH, the payable modifier should be removed from the withdrawTokens function.

  2. Implement proper logic to either refund the user or add a withdrawETH function.

// Refund the user
function withdrawTokens(...) {
if (msg.value > 0) {
(bool success, ) = msg.sender.call{value: msg.value}("");
require(success, "ETH transfer failed");
}
// rest of withdraw code
}
// Or, add a function to withdraw the ETH
function withdrawETH() external onlyOwner {
(bool success, ) = msg.sender.call{value: address(this).balance}("");
require(success, "ETH transfer failed");
}
Updates

Lead Judging Commences

n0kto Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

Informational / Gas

Please, do not suppose impacts, think about the real impact of the bug and check the CodeHawks documentation to confirm: https://docs.codehawks.com/hawks-auditors/how-to-determine-a-finding-validity A PoC always helps to understand the real impact possible.

Support

FAQs

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