# Repeated join-exit value extraction

Round-trip deposit/withdraw or borrow/redeem cycles that net a small profit each time from shared reserves

_Area: DeFi accounting and math bugs. Seen in 3 disclosed reports; latest 2025-01-13. Updated 2026-10-24._

## How it works
A contract tracks an actor's claim on shared reserves in a state variable (shares, deposited amount, debt). Each join or exit updates that variable and moves value. The assumption that breaks is that one cycle is value-neutral: that a deposit-then-withdraw round trip returns the accounting to the same state. When the update is proportional, rounded, or ordered wrongly, each cycle can leave the actor slightly ahead, and repeating it drains the pool. The actor's capital only needs to cover one cycle, so a flash loan can supply it [E1](https://bugbountylibrary.com/r/dhl%3A20250113-mosca2.md)[E2](https://bugbountylibrary.com/r/dhl%3A20250106-mosca.md)[E3](https://bugbountylibrary.com/r/dhl%3A20240314-mo.md). In the disclosed reports the exact root cause is described as likely, not confirmed, so treat the mechanism as a hypothesis to test, not a proven fact.

## Where it shows up
- Join and exit functions that update balances independently; a neutral round trip is not enforced (suspected in [E1](https://bugbountylibrary.com/r/dhl%3A20250113-mosca2.md)[E2](https://bugbountylibrary.com/r/dhl%3A20250106-mosca.md)).
- Fiat or price conversion inside join/withdraw, where the conversion and the stored deposit can disagree (suspected in [E1](https://bugbountylibrary.com/r/dhl%3A20250113-mosca2.md)).
- Reward or refund math that may be computed per call rather than per net position (suspected in [E2](https://bugbountylibrary.com/r/dhl%3A20250106-mosca.md)).
- Borrow and redeem callable in the same transaction with no cooldown or state lock (suspected in [E3](https://bugbountylibrary.com/r/dhl%3A20240314-mo.md)).
- Repeated joins accepted without a state update that would make a second join behave differently [E1](https://bugbountylibrary.com/r/dhl%3A20250113-mosca2.md).

## How to review
The property to test: for any sequence of join/exit (or borrow/redeem) calls by one actor within one transaction, total value out should not exceed total value in, and any rounding should not favor the actor. Check that every state variable read on exit was fully written on join, and that a same-block round trip is either blocked or shown non-profitable. Trace one full cycle by hand with concrete numbers: e.g. deposit 1000, withdraw, and confirm the returned amount is <= 1000 after fees and rounding. Check the rounding direction both ways (deposit and withdraw), since a bias in either can leak value. If the numbers show a per-cycle gain, the loop is the bug.

## Variants seen
- Repeated join with inflated withdrawFiat payout, amplified by flash loan [E1](https://bugbountylibrary.com/r/dhl%3A20250113-mosca2.md).
- Repeated join/exitProgram cycles draining staking refunds [E2](https://bugbountylibrary.com/r/dhl%3A20250106-mosca.md).
- Repeated borrow/redeem loop draining lending liquidity; analogous round-trip shape, not join/exit [E3](https://bugbountylibrary.com/r/dhl%3A20240314-mo.md).

## Impact and severity
High: large drains, from ~19K USDC [E2](https://bugbountylibrary.com/r/dhl%3A20250106-mosca.md) to ~413K USDT [E3](https://bugbountylibrary.com/r/dhl%3A20240314-mo.md), and a reported ~37.6K profit in [E1](https://bugbountylibrary.com/r/dhl%3A20250113-mosca2.md) (profit figure as reported, root cause unconfirmed). Severity rises when a flash loan removes the capital barrier and when no cooldown or per-block limit exists [E1](https://bugbountylibrary.com/r/dhl%3A20250113-mosca2.md)[E3](https://bugbountylibrary.com/r/dhl%3A20240314-mo.md).

## Evidence
- E1: https://bugbountylibrary.com/r/dhl%3A20250113-mosca2.md
- E2: https://bugbountylibrary.com/r/dhl%3A20250106-mosca.md
- E3: https://bugbountylibrary.com/r/dhl%3A20240314-mo.md
