# A malicious user can bypass limit order trading fees via cross-function re-entrancy

LeverageModule mints NFT via _safeMint before setPosition, letting attacker re-enter announceLimitOrder with uninitialized position and pay zero trade fee.

- Payout: Contest reward
- Severity: high
- Program: Flatmoney
- Credited: LTDingZhen
- Disclosed: 2023-12-11
- Write-up quality: 7/10
- Original: https://github.com/sherlock-audit/2023-12-flatmoney-judging/issues/75
- Library page: https://bugbountylibrary.com/r/sherlock%3A2023-12-flatmoney%3AH-2
- Tags: domain:web3 impact:funds-loss kind:contest platform:sherlock sev:high tech:evm tech:solidity technique:chained technique:source-review vuln:business-logic vuln:fee-bypass vuln:reentrancy

**Target:** LeverageModule / LimitOrder smart contracts
**Root cause:** _mint calls _safeMint (external call to receiver) before vault.setPosition writes _positions[_tokenId], so during the callback the position's additionalSize reads as 0 and getTradeFee returns 0.
**Steps:**
1. Deploy attacker contract implementing IERC721Receiver.
2. Attacker contract calls announceLeverageOpen; keeper executes executeOpen, which calls _mint -> _safeMint to attacker contract.
3. In onERC721Received, call `limitOrderProxy.announceLimitOrder(tokenId, 750e18, 1250e18)` while position state is still unset.
4. tradeFee computed from `vault.getPosition(tokenId).additionalSize` = 0, so fee = 0.
5. Return `msg.sig` to satisfy ERC721 receiver check; limit order stored with tradeFee 0.
6. Run `forge test --mt test_tradingFeeBypass -vv`; assert orderData.tradeFee == 0.
**How found:** Code reading of LeverageModule::executeOpen revealed _mint/_safeMint external call occurring before vault.setPosition state write; noticed LimitOrder::announceLimitOrder reads vault.getPosition(tokenId).additionalSize, which would be default 0 during the callback.
**Exploitation:** Attacker opens a leveraged position from a contract; during the ERC721 mint callback it registers a limit order (stop loss/take profit) for the same tokenId before setPosition executes, so the limit order's trade fee is zero. Repeated at scale via a service, siphoning fees owed to LPs.
**Escalation:** Scale via a service opening many leverage positions with limit orders at 0 trade fee, draining accumulated fees from LPs.
**Insight:** When an ERC721 _safeMint precedes state initialization, any function reading that tokenId's position during onERC721Received sees zeroed/default values — check fee, size, or price calculations that read position state inside mint callbacks.
