Full Report
Reentrancy is a fundamental attack in the Solidity security space. This is when a user can recursively call a contract while it has not had it's state fully updated. Developers should follow the Check, Effect, Interactions (CEI) pattern. However, the term cross-chain caught my eye for this post. The application the author was testing was a cross-chain NFT contract. Naturally, NFTs should only exist on a single chain at once. So, verification has to be done on both chains before minting. To send funds from one contract to another, a bridge is needed. Since the contract owns the asset, it can mint to create an asset or burn to remove it from existence. Then, using a proof, the other chain would know whether we owned an asset on it or not. The code for the mint function did not follow the CEI pattern. The increment should occur before the call to _safeMint. This worked as follows: function mint(address to) public returns (uint256) { uint256 newWarriorId = tokenIds.current(); _safeMint(to, newWarriorId); tokenIds.increment(); return newWarriorId; } The _safeMint() function has an external function call for onERC721Received inside of it. Since the CEI pattern is not followed and the contract is not using the standard library for preventing reentrancy, there is a major problem. How can this be exploited? If we simply call the _mint function again, it will fail because the token id wasn't incremented and already exists. However, there are other functions within the contract that could be interesting to us. The function crossChainTransfer() is used to send the assets from one chain to another. Calling this with a particular token ID will send the token from one chain to another. Since the ID is not incremented until after the mint call, we can exploit this to get two copies of it. Call mint() with the attacker contract as the recipient. NOTE: The ID hasn't been incremented. Re-enter the contract to call crossChainTransfer to transfer the new id to chain B. Call mint() again from the recipient contract to mint the same NFT once again. Cross-contract reentrancy is a little bit deceiving but technically correct here. Overall, a usual bug class exploited in a unique way!
Analysis Summary
# Vulnerability: Cross-Chain NFT Reentrancy via Unsafe External Call in Mint Function
## CVE Details
- CVE ID: Not specified in the text (N/A)
- CVSS Score: Not specified in the text (N/A)
- CWE: CWE-838: Improper Restriction of Recursion (Reentrancy)
## Affected Systems
- Products: Custom Cross-Chain NFT Contract (e.g., `crossChainWarrior.sol`), specifically implementations missing the Check-Effect-Interaction (CEI) pattern and relying on standard `_safeMint` functionality without reentrancy guards.
- Versions: Not specified in the text (N/A)
- Configurations: Contracts where the state variable increment (`tokenIds.increment()`) occurs *after* an external call introduced by `_safeMint()`.
## Vulnerability Description
The vulnerability stems from the failure to adhere to the Check-Effect-Interactions (CEI) pattern within the contract's `mint` function.
The vulnerable `mint` function structure is:
1. **Check:** (Implicit, state checks omitted for brevity)
2. **Interaction (Unsafe External Call):** `_safeMint(to, newWarriorId);` This function internally calls `onERC721Received` on the recipient address (`to`) if it is a contract, which constitutes an external call.
3. **Effect:** `tokenIds.increment();` The `tokenIds` counter is updated *after* the external call.
Because the token ID is not incremented before the external call, an attacker controlling the recipient contract can recursively call the `mint` function before the state update is finalized. In the cross-chain context, an attacker can exploit this by calling `mint` to receive a new NFT ID, then immediately calling a function like `crossChainTransfer()` (which uses an external validation/transfer) to move that un-incremented asset to another chain, and then re-entering `mint()` again to claim a *second* NFT with the exact same token ID, effectively duplicating assets.
## Exploitation
- Status: PoC available (The author details the steps for a successful exploit involving two chains and an attacker contract.)
- Complexity: Medium (Requires understanding of cross-chain logic, the CEI pattern, and attacker contract development.)
- Attack Vector: Network (Exploitation relies on controlled execution flow triggered by an external call.)
## Impact
- Confidentiality: None (Not applicable to data exposure)
- Integrity: High (Allows for unauthorized duplication of NFTs, violating the core property of a non-fungible token.)
- Availability: Low (Minimal impact on overall contract availability, but token functionality is degraded.)
## Remediation
### Patches
- The critical patch is to refactor the `mint` function to strictly follow the **Check-Effect-Interaction (CEI)** pattern:
1. Increment the state variable (`tokenIds.increment();`).
2. Perform the external call (`_safeMint(to, newWarriorId);`).
- Additionally, implement reentrancy protection mechanisms, such as using OpenZeppelin's ReentrancyGuard modifiers on sensitive functions.
### Workarounds
- Temporarily remove the external call functionality related to `_safeMint` checks if possible, or only allow Minting to Externally Owned Accounts (EOAs) until a full patch can be deployed.
- If applicable, freeze asset transfers mediated by the bridge until the minting contract is patched.
## Detection
- Indicators of Compromise: Rapid, sequential calls to the `mint` function originating from a contract address, followed quickly by calls to `crossChainTransfer` for the same initial token ID ranges.
- Detection Methods and Tools: Static analysis tools configured to flag missing CEI patterns or external calls made before critical state updates. Monitoring RPC logs for unexpected recursion depth or overlapping transaction executions on the same token ID.
## References
- Vendor Advisories: None provided in the source text.
- Relevant Links:
- Article discussing the vulnerability: hXXps://medium.com/p/cross-chain-re-entrancy-7f082782c432