# The reentrancy vulnerability in _safeMint can allow an attacker to steal all rewards

Reentrancy via _safeMint in XDEFI lock() lets attacker re-enter updateDistribution before totalDepositedXDEFI updates, inflating _pointsPerUnit and stealing rewards.

- Payout: Audit contest
- Severity: high
- Program: XDEFI
- Found by: cccz
- Disclosed: 2022-02-10
- Write-up quality: 8/10
- Original: https://github.com/code-423n4/2022-01-xdefi-findings/issues/25
- Library page: https://bugbountylibrary.com/r/c4%3A2022-01-xdefi%3AH-02
- Tags: domain:web3 impact:funds-loss kind:contest platform:code4rena sev:high tech:evm tech:solidity technique:chained technique:source-review vuln:accounting-error vuln:reentrancy vuln:reward-distribution

**Target:** XDEFIDistribution ERC721 staking contract
**Root cause:** _lock calls _safeMint (which invokes attacker's onERC721Received) before incrementing totalDepositedXDEFI; updateDistribution lacks noReenter and computes _pointsPerUnit from balance minus stale totalDepositedXDEFI.
**Steps:**
1. Attacker deploys contract implementing IERC721Receiver.onERC721Received.
2. Attacker first calls `lock(amount, duration, destination)` to hold a position NFT for later unlock.
3. Attacker calls `lock(...)` with destination = attacker contract; during `_safeMint`, onERC721Received fires before `totalDepositedXDEFI += amount_`.
4. In onERC721Received, attacker re-enters `updateDistribution()`; `_updateXDEFIBalance` sees balance inflated but totalDepositedXDEFI stale, so `_pointsPerUnit += (newXDEFI*_pointsMultiplier)/totalUnits` becomes abnormally large.
5. After expiry, attacker calls `unlock(tokenId)`; `_withdrawableGiven` uses inflated _pointsPerUnit to pay out excess XDEFI rewards.
**How found:** Code review of _safeMint ordering vs totalDepositedXDEFI update and noticing updateDistribution lacks the noReenter modifier applied to lock/unlock.
**Exploitation:** Attacker contract re-enters updateDistribution from onERC721Received during lock, inflating _pointsPerUnit; then unlocks a pre-existing position to withdraw inflated rewards. User deposits themselves are protected because unlock calls _updateXDEFIBalance.
**Insight:** When a contract uses a noReenter guard on state-changing entrypoints, check every other external function (especially permissionless accounting updaters like updateDistribution) for missing guards and whether _safeMint/_safeTransfer callbacks fire before accounting state is committed.
