Team Finance V2→V3 Migration Lock-Identity Mismatch / Refund Drain (2022-10-27)

  • Name: Team Finance V2→V3 migration lock-identity mismatch / refund drain
  • Date: 2022-10-27
  • Protocol: Team Finance (TrustSwap)
  • Chain: Ethereum
  • Category: migration trust-boundary failure / legacy-position identity mismatch / LP-lock migration authorization bypass / refund-path drain
  • Loss: roughly $14.5M to $15.8M across four locked Uniswap v2 positions, with public breakdowns centered on CAW, TSUKA, KNDX, and FEG pools
  • Summary: Team Finance’s liquidity-locker contract 0xE2fE530C047f2d85298b07D9333C05737f1435fB offered a migrate() path to move locked Uniswap v2 LP positions into Uniswap v3. The decisive mistake was that the migration auth checks were satisfied by attacker-created dummy locks, while the assets actually burned and migrated came from unrelated victim LP positions already custodied by Team Finance. Public analyses describe the attacker first creating four trivial locks for an arbitrary token, then using those lock IDs plus attacker-controlled migration parameters to migrate real victim LP positions. Because the migration flow only placed a thin slice of the withdrawn assets into the new Uniswap v3 positions and refunded the leftover balances to the attacker-controlled withdraw address, Team Finance turned locked third-party liquidity into attacker-owned token refunds.
  • Trigger / entry point:
    • The vulnerable entry point was Team Finance’s Uniswap v2→v3 LP migration flow in the liquidity-lock contract at 0xE2fE530C047f2d85298b07D9333C05737f1435fB.
    • Public analyses identify the exploit transaction as 0xb2e3ea72d353da43a2ac9a8f1670fd16463ab370e563b9b5b26119b2601277ce.
    • The attacker prepared four attacker-owned lock IDs by locking a self-created token and pointing the withdraw address at exploit contract 0xCFF07C4e6aa9E2fEc04DAaF5f41d1b10f3adAdF4.
    • They then invoked migrate() using those attacker-owned lock records while shaping the migration around unrelated victim LP positions Team Finance was already holding on behalf of other projects.
  • Root cause:
    • Team Finance authenticated who is allowed to call migrate with one lock record but did not bind that authorization tightly enough to which exact LP asset is being migrated.
    • The migration flow accepted attacker-controlled migration context — including the effective LP target and Uniswap v3 initialization / refund envelope — without enforcing that the attacker-owned lock and the LP being burned were the same position.
    • Because the migration helper refunded leftover assets after establishing the new Uniswap v3 position, any mismatch between authorized lock and actual LP being migrated became directly monetizable.
    • In other words, Team Finance treated migration code like locker UX, when it was really custody-critical balance-sheet code.
  • Exploit path:
    1. The attacker funded EOA 0x161cebB807Ac181d5303A4cCec2FC580CC5899Fd and deployed exploit contract 0xCFF07C4e6aa9E2fEc04DAaF5f41d1b10f3adAdF4.
    2. They deployed or used a self-controlled token contract and created four low-value locks inside Team Finance, reportedly obtaining lock IDs 15324, 15325, 15326, and 15327.
    3. Those locks used attacker-controlled settings, including a withdraw address that pointed back to the exploit contract, and the attacker extended the lock durations to satisfy Team Finance’s migration-side checks.
    4. The attacker then called migrate() with those attacker-owned lock IDs but with migration parameters aimed at real victim LP positions already locked in Team Finance for projects including CAW, TSUKA, KNDX, and FEG.
    5. Public analyses describe the migration path as burning the victim Uniswap v2 LP, creating or initializing a Uniswap v3 pool using attacker-shaped parameters, and depositing only a small portion of the withdrawn assets into the new v3 position.
    6. The large leftover balances from that migration were treated as ordinary refund outputs and were sent to the attacker-controlled withdraw address instead of back to the projects whose v2 LP had just been consumed.
    7. The attacker repeated the pattern across four victim positions, consolidating proceeds in address 0xBa399a2580785A2dEd740F5e30EC89Fb3E617e6E.
  • What the attacker needed to be true:
    • Team Finance’s migration checks had to be satisfiable by an attacker-owned lock record that was not the same LP position later burned by the migrator.
    • The contract had to let caller-controlled migration parameters shape the v2→v3 conversion and its leftover-refund behavior.
    • The refund path had to send residual assets to the withdraw address associated with the attacker-owned lock rather than to the owner of the victim LP position being consumed.
    • Team Finance had to already custody large victim LP positions, so the authorization mismatch could be cashed out immediately.
  • Technical anatomy worth remembering:
    • This was not just user chose a bad sqrtPriceX96. Multiple postmortems stress that the sharper failure was identity binding: the migration logic did not prove that the lock record authorizing the operation matched the LP position actually being migrated.
    • The attacker did not need to steal existing lock NFTs from victims. They only needed to create valid-looking locks of their own and use those to satisfy Team Finance’s migrate-side checks.
    • The payout surface was the refund rail. Once Team Finance burned a victim’s v2 LP under attacker-shaped migration parameters, most of the underlying assets could come back as a legitimate-looking leftover payout to the attacker.
    • This is a strong migration-pattern exemplar because the protocol did not create value from thin air; it reassigned real custody assets after trusting the wrong legacy-position identity.
  • Representative transactions and artifacts:
    • Vulnerable Team Finance liquidity-lock contract: 0xE2fE530C047f2d85298b07D9333C05737f1435fB
    • Main exploiter EOA: 0x161cebB807Ac181d5303A4cCec2FC580CC5899Fd
    • Attacker consolidation address: 0xBa399a2580785A2dEd740F5e30EC89Fb3E617e6E
    • Attacker exploit contract: 0xCFF07C4e6aa9E2fEc04DAaF5f41d1b10f3adAdF4
    • Attack transaction: 0xb2e3ea72d353da43a2ac9a8f1670fd16463ab370e563b9b5b26119b2601277ce
    • Preparatory lock-building transaction cited by SlowMist: 0xe8f17ee00906cd0cfb61671937f11bd3d26cdc47c1534fedc43163a7e89edc6f
  • Response / mitigation:
    • Team Finance acknowledged the exploit and disabled the migration feature.
    • Public reporting later described partial fund returns after negotiation with the exploiter, but the important protocol lesson is upstream of that recovery story.
    • The contract-level fix is to bind migration authorization to the exact legacy asset being consumed: the lock ID, stored LP token, beneficiary, and migrated reserves must all refer to one canonical position.
    • The healthier long-term shape is one single-use migration-closeout record that binds which exact lock is being consumed, which exact LP token and reserves are being burned, which destination position may be created, and where any leftover assets must settle before helper code can move value.
    • The migration helper also should never let attacker-controlled pool-initialization or refund parameters decide how much of a victim’s withdrawn LP reserves come back as a caller-owned residual.

Pattern tags

  • migration-trust-boundary-failure
  • migration-cutover-authenticity-and-single-record-consumption-discipline
  • helper-path-invariant-reuse-and-fail-closed-discipline
  • Team Finance let one attacker-owned lock answer who may migrate while a different victim-owned LP position answered what real reserves get burned, so migration authorization and consumed-asset identity quietly split apart.
  • The refund rail was part of the exploit, not cleanup after it. Once the helper burned a victim LP under attacker-shaped parameters, the leftover-token path treated those reserves as if they belonged to the attacker-owned lock that passed the checks.
  • Public reporting around sqrtPriceX96 matters, but the sharper invariant failure was earlier: price-initialization freedom only became valuable because the contract never proved that the authorized lock and the consumed LP position were the same record.

Why the exploit worked

  • Team Finance authenticated the wrong object. The migrate-side checks were satisfied by attacker-created dummy locks, but the contract did not force those locks to be the same LP positions later burned and re-homed through the Uniswap v2→v3 path.
  • The migration helper treated pool creation, initialization, and refund plumbing as if they were downstream implementation details. In practice they were part of the custody boundary, because once a victim LP was consumed those helper-local parameters decided how much value stayed in the replacement position and how much came back as leftover tokens.
  • The attacker did not need to counterfeit reserves or bypass custody directly. Team Finance already held real victim LP positions, so an authorization mismatch plus attacker-shaped migration parameters was enough to convert custodied third-party value into attacker-owned refunds.
  • The decisive failure was identity binding, not just bad pool initialization. As SlowMist emphasized and rekt / Beosin both implicitly show, the exploit only paid because authorized lock, actual LP being migrated, and refund destination were allowed to resolve to different answers.
  • This is the migration-trust-boundary family in its burn-and-recreate form: the protocol did not mint fake assets from nowhere; it consumed a real legacy position after trusting the wrong legacy-position identity and then let the attacker inherit the residual settlement rail.

Detection and audit pivots

  • Trace burn-and-recreate migrations per record, not per function. Ask whether the same stored lock or NFT answers all four questions: who may call, which LP token is being consumed, who owns the replacement, and who receives any leftovers.
  • Fuzz with deliberately mismatched identities: attacker-owned low-value locks, victim-owned LP positions already in custody, attacker-chosen refund recipients, and attacker-shaped pool-init parameters. If any pair can be mixed, the migration path is already split.
  • Treat residual-token refunds as first-class custody logic. Reviewers should prove that leftover assets are derived from the consumed legacy record rather than from whatever record happened to satisfy authorization checks.
  • Test whether helper-local parameters like sqrtPriceX96, token0, token1, pair metadata, or noLiquidity flags can amplify a pre-existing identity mismatch into a payout rail. Those parameters may not be root cause, but they often decide how much of the victim’s value exits through refund.
  • Compare authorized legacy record against actual reserves burned in traces and postconditions. A migration should fail if those identities ever diverge, even if every individual subcall looks locally valid.
  • Review cutover helpers as custody code, not convenience UX. Team Finance shows that an audited migration wrapper can still be the real balance-sheet boundary if it is the thing that consumes and reroutes live locked liquidity.

Failure-prevention checklist

  • Store and consume one canonical migration record that binds lock ID, LP token address, beneficiary, reserve identity, replacement-position owner, and residual recipient together.
  • Derive the LP pair being migrated from the authenticated lock record itself; never let adjacent calldata choose a different asset than the one the lock actually custodies.
  • Make leftover-asset routing inherit from the consumed legacy record, not from whichever attacker-controlled address was convenient for initiating migration.
  • Fail closed when migration parameters imply pair/token mismatches, fresh pool creation for an unrelated lock, or replacement positions that cannot be proven to descend from the authenticated legacy LP.
  • Treat price-initialization and pool-creation parameters as bounded transformation inputs, not as authority over how much victim value may exit as refund.
  • Add per-record invariants such as replacement position value + refunded value <= value withdrawn from this exact lock-owned LP position.
  • Separate rollout convenience from live custody authority: if migration needs bespoke helper logic, give it narrow scope, kill switches, and explicit sunset rather than letting it remain a general-purpose LP re-homing surface.

Audit prompts

  • What exact stored record authorizes this migration, and is it provably the same record whose LP tokens are being burned?
  • Can a user satisfy migrate-side auth with one lock or NFT while pointing the helper at a different LP pair, reserve set, or beneficiary?
  • If the migration creates a new pool or initializes a price, what stops those parameters from converting someone else’s locked reserves into attacker-owned leftovers?
  • Where does the refund recipient come from: the consumed legacy record, or unrelated caller-shaped migration context?
  • Can authorized lock, pair being migrated, replacement owner, and refund destination ever resolve to different identities inside one successful call?
  • If the contract were paused after the victim LP burn but before refund settlement, would accounting still reconcile per consumed lock record?
  • Which helper-local assumptions were only socially true (users will migrate their own LP, frontend will match the right pair) rather than contract-enforced?

Lessons that should already be standard

  • Migration auth has to bind to the exact asset being consumed, not just to a nearby user-owned record.
  • Leftover refund is part of the solvency boundary whenever one position is burned and another is created.
  • User-controlled transformation parameters are dangerous, but they usually monetize a deeper identity-binding failure rather than replacing it.
  • If a protocol already custodies the victim asset, a migration helper can become the theft primitive even without any counterfeit mint or oracle break.
  • Burn-and-recreate flows should be reviewed as consume one exact right, create one bounded successor right, route one tightly derived residual — not as flexible UX plumbing.
  • An audited helper is still unsafe if it authenticates a story instead of a single consumed record.

Why this incident matters

  • It adds a distinct cousin to the migration corpus beyond STAX’s arbitrary-old-contract injection and Prisma’s spoofed callback context.
  • Team Finance shows that migration code can fail even when the protocol already has custody of the real asset and is only supposed to re-home it. If the contract authenticates the wrong position identity, the migration step itself becomes the theft primitive.
  • It also preserves a useful AMM-specific lesson: when migration helpers burn one LP representation and mint another, the leftover-token refund path is part of the security boundary, not just convenience plumbing.
  • The sharper healthy-side boundary is that authorization, consumed LP-lock identity, replacement-position creation, and leftover-refund routing all needed to collapse onto one canonical migration-closeout object. Team Finance instead let an attacker-owned lock certify a different victim LP position and then inherit the refund rail for the mismatch.

Internal linkages

Control surface