Omniscia Nuklai Audit
FragmentNFT Manual Review Findings
FragmentNFT Manual Review Findings
FNF-01M: Proposal Tag ID Corruption Re-Entrancy
Type | Severity | Location |
---|---|---|
Language Specific | FragmentNFT.sol:L293-L296 |
Description:
The FragmentNFT::proposeMany
function contains a re-entrancy attack vector that manifests when the VerifierManager
calls the FragmentNFT::accept
function in the same call.
Specifically, the FragmentNFT::_proposeManyMessageHash
function will validate a signed payload with a specific _mintCounter
range, however, this range can be invalidated by re-entering the contract during an interim IVerifierManager::propose
call and invoking f.e. DatasetNFT::proposeFragment
, causing the _mintCounter
to be incremented while the loop within FragmentNFT::proposeMany
has not concluded and thus leading to out-of-order tag and pending fragment owner IDs.
Impact:
The current FragmentNFT::proposeMany
mechanism is insecure as the signed message hash validated in FragmentNFT::_proposeManyMessageHash
can be "invalidated" via a re-entrancy when the IVerifierManager
instantly "accepts" proposals and thus leads to the ERC721::_safeMint
operation being executed within FragmentNFT::accept
which is re-entrant.
Example:
276function proposeMany(277 address[] memory owners,278 bytes32[] memory tags_,279 bytes calldata signature280) external onlyDatasetNFT {281 if (tags_.length != owners.length) revert ARRAY_LENGTH_MISMATCH();282 bytes32 msgHash = _proposeManyMessageHash(_mintCounter + 1, _mintCounter + tags_.length, owners, tags_);283 address signer = ECDSA.recover(msgHash, signature);284 if (!dataset.isSigner(signer)) revert BAD_SIGNATURE(msgHash, signer);285
286 for (uint256 i; i < owners.length; i++) {287 uint256 id = ++_mintCounter;288 bytes32 tag = tags_[i];289 pendingFragmentOwners[id] = owners[i];290 tags[id] = tag;291 emit FragmentPending(id, tag);292
293 // Here we call VeriferManager and EXPECT it to call `accept()`294 // during this call OR at any following transaction.295 // DO NOT implement any state changes after this point!296 IVerifierManager(dataset.verifierManager(datasetId)).propose(id, tag);297 }298}
Recommendation:
We advise the code to cache the _mintCounter
to a local cachedMintCounter
variable before the for
loop's execution and to validate it afterwards as being strictly equal to the _mintCounter
(i.e. cachedMintCounter + tags_.length == _mintCounter
).
This mechanism will prevent re-entrancy attacks from affecting the sole storage
related entry utilized within the vulnerable for
loop within FragmentNFT::proposeMany
.
Alleviation (fb50b5c39665f7df086b2de1fdbf93ba2d836bf9):
The code was updated according to our recommendation, caching the _mintCounter
to a local lastMintCounter
variable that is consequently utilized at the end of the function's body in a validation check ensuring that its post-loop state (_mintCounter
) matches the value expected if all tags_
have been properly consumed and minted.
As such, we consider this exhibit fully alleviated.