# twAML::participate - reentrancy via _safeMint can be used to brick reward distribution

Reentrancy through _safeMint's onERC721Received callback in twAML.participate lets an attacker exit before expiry and inflate netActiveVotes, bricking reward distribution.

- Payout: Contest reward
- Severity: high
- Program: Tapioca DAO
- Credited: cergyk
- Disclosed: 2023-11-16
- Write-up quality: 7/10
- Original: https://github.com/code-423n4/2023-07-tapioca-findings/issues/329
- Library page: https://bugbountylibrary.com/r/c4%3A2023-07-tapioca%3AH-57
- Tags: domain:web3 impact:dos impact:integrity kind:contest platform:code4rena sev:high tech:evm tech:solidity technique:source-review vuln:accounting-error vuln:reentrancy vuln:reward-distribution

**Target:** twAML participate / twTAP NFT minting and reward distribution
**Root cause:** participate calls _safeMint before updating weekTotals, and exitPosition only checks position.expiry which is not yet set, so the ERC721 receiver callback can reenter and exit the position while state is inconsistent.
**Steps:**
1. Attacker contract calls twAML.participate to lock tokens; _safeMint triggers onERC721Received on attacker contract.
2. In callback, reenter exitPosition for the just-minted tokenId; require(position.expiry <= block.timestamp) passes because expiry is still 0.
3. Position is released before weekTotals[w0+1].netActiveVotes += votes and weekTotals[w1+1].netActiveVotes -= votes execute.
4. Repeat to inflate weekTotals[w0+1].netActiveVotes without locking tokens.
5. When operator calls distributeReward, division by inflated netActiveVotes makes totalDistPerVote zero, bricking distribution.
**How found:** Code reading of participate: noticed _safeMint executes an external callback before weekTotals effects, and that exitPosition's expiry check passes because expiry is unset at that point.
**Exploitation:** Attacker deploys a contract that participates then reenters exitPosition in onERC721Received, repeatedly inflating netActiveVotes for a week without locking tokens; later distributeReward divides by the inflated total, zeroing totalDistPerVote and denying rewards.
**Insight:** When an ERC721 _safeMint precedes state writes, check whether any exit/withdraw path validates a field (like expiry) that is only set after the mint callback—reentering there lets you exit before the position is fully initialized.
