Visor Finance vVISR Staking deposit() owner() Hook / Fake-from Unbacked-Share Mint Drain (2021-12-21)
- Name: Visor Finance vVISR staking
deposit()owner()hook / fake-fromunbacked-share mint drain - Date: 2021-12-21
- Protocol: Visor Finance
- Chain: Ethereum
- Category: account confusion / user-supplied payer verification failure / unbacked-share mint / issuance-redemption asymmetry
- Loss: about
8,812,958 VISR, worth roughly$8.2Mbefore the attacker’s dump crushed the token price - Summary: Visor’s
vVISRstaking contract letdeposit(uint256 visrDeposit, address from, address to)treat a caller-supplied contract as an authorized payer ifowner()on that contract returnedmsg.sender. It then calleddelegatedTransferERC20(...)on that same caller-supplied contract and mintedvVISRshares as if realVISRhad arrived. The attacker deployed helper contract0x10c509aa9ab291c76c45414e7cdbd375e1d5ace8, transferred its ownership to attacker EOA0x8efab89b497b887cdaa2fb08ff71e4b3827774b2, and used that helper as thefromaddress. Because Visor authenticated the attacker’s own contract as the transfer authority and then trusted the contract-authored transfer hook as payment proof, the attacker minted about195,000 vVISRwithout supplying correspondingVISR, burned those shares for roughly8.8M VISR, dumped theVISRinto ETH, and laundered proceeds through Tornado Cash. - Trigger / entry point:
- The vulnerable contract was Visor’s
vVISRrewards / staking contract at0xc9f27a50f82571c1c8423a42970613b8dbda14ef. deposit()accepted an arbitraryfromaddress and handled contract payers differently from EOAs.- If
fromwas a contract whoseowner()returned the transaction sender, Visor invokeddelegatedTransferERC20(...)on that contract rather than proving thatVISR.transferFrom(...)had actually succeeded from a real approved wallet. - The attacker controlled both the helper contract and the destination address receiving newly minted
vVISR.
- The vulnerable contract was Visor’s
- Root cause:
- Visor authenticated the wrong root object. A caller-supplied contract was allowed to self-certify that it was the legitimate payer by returning the caller from
owner(). - The contract then treated execution of a user-supplied
delegatedTransferERC20(...)hook as if it were proof that backingVISRhad entered the staking contract. - That collapsed the payment boundary on the issuance side:
vVISRminting no longer required a canonical payer or a canonical token-transfer receipt. - Public analyses preserve two compatible readings of the exploit path: some emphasize that a malicious
fromcontract could simply satisfy theowner()check and do nothing meaningful indelegatedTransferERC20(...), while others preserve a nested re-entrantdeposit()inside that hook that further amplified share issuance against stale supply. Either way, the decisive failure was already earlier — Visor let attacker-authored contract behavior stand in for proof of payment.
- Visor authenticated the wrong root object. A caller-supplied contract was allowed to self-certify that it was the legitimate payer by returning the caller from
- Exploit path:
- The attacker funded EOA
0x8efab89b497b887cdaa2fb08ff71e4b3827774b2from Tornado Cash in transaction0x660b97542ade1e533bd5e098dbe53da26b63d53a3c2c4dd46b481a91bd075ead. - They deployed / prepared helper contract
0x10c509aa9ab291c76c45414e7cdbd375e1d5ace8, which exposed theowner()anddelegatedTransferERC20(...)surface Visor expected from a delegated payer. - In transaction
0x27f2210536553392cf180c0b37055b3dc92094a5d585d7d2a51f790c9145e47c, the attacker transferred ownership of that helper contract to the exploit EOA so the laterowner() == msg.sendercheck would pass. - In exploit transaction
0x69272d8c84d67d1da2f6425b339192fa472898dce936f24818fda415c1c1ff3f, the attacker calleddeposit()on the vVISR contract with the malicious helper asfromand an attacker-controlled recipient asto. - Visor accepted the helper contract as an authorized payer because its
owner()method returned the attacker address. - Visor then executed
delegatedTransferERC20(...)on the helper contract and continued as if backingVISRhad arrived; public post-exploit analyses differ on whether the hook simply failed to transfer assets or also re-entereddeposit()to amplify minted share quantity, but they agree the mint path was no longer tied to realVISRreceipt. - The transaction minted about
195,000 vVISRto the attacker without equivalent backing entering the staking contract. - In transaction
0x6eabef1bf310a1361041d97897c192581cd9870f6a39040cd24d7de2335b4546, the attacker burned the freshly mintedvVISRand withdrew about8,812,958 VISRfrom the real staking reserves. - The attacker dumped
VISRinto ETH through the VISR-ETH liquidity pool, then sent mixed ETH to Tornado Cash, with0x0e16210218ecc487a35b9ff48fe3d9f3e9b0f50330f9e7805e38135732b85270one of the cited laundering transactions.
- The attacker funded EOA
- What the attacker needed to be true:
deposit()had to accept arbitrary contract payers rather than pinning a canonical token holder or canonical transfer mechanism.- The payer-authentication path had to trust a self-reported
owner()answer from the caller’s own contract. - The mint path had to continue after
delegatedTransferERC20(...)without independently proving thatVISRbalances actually moved into the staking contract. - The burn / withdraw side had to remain economically honest, redeeming
vVISRagainst realVISRreserves once the counterfeit shares existed. - The staking contract needed enough
VISRinventory that unbacked-share minting could be cashed out before intervention.
- Technical anatomy worth remembering:
- This was not just
bad access controlin the generic sense. The payment-side root of trust itself was caller-supplied. - The decisive mistake was letting an arbitrary contract both certify who the payer was and author the code path that supposedly performed payment.
- That made the exploit an issuance/redemption asymmetry too: the mint side stopped requiring real backing, while the burn side still honored the resulting claim against real reserves.
- Public retellings split between
fake delegated transfer was already enoughanddelegated transfer also re-entered deposit()variants. The safer preserved lesson is the shared one beneath both: issuance logic relied on attacker-authored contract behavior instead of immutable proof that assets arrived. - This is a useful bridge case between contract-authenticity failures and unbacked-mint drains: the protocol first trusted the wrong integration endpoint, then let that counterfeit endpoint print claims the honest redemption side still paid out.
- This was not just
- Representative transactions and addresses:
- Vulnerable vVISR contract:
0xc9f27a50f82571c1c8423a42970613b8dbda14ef - Attacker EOA:
0x8efab89b497b887cdaa2fb08ff71e4b3827774b2 - Attacker helper contract used as fake
from:0x10c509aa9ab291c76c45414e7cdbd375e1d5ace8 - Tornado Cash funding tx:
0x660b97542ade1e533bd5e098dbe53da26b63d53a3c2c4dd46b481a91bd075ead - Helper-contract ownership transfer tx:
0x27f2210536553392cf180c0b37055b3dc92094a5d585d7d2a51f790c9145e47c - Main exploit tx:
0x69272d8c84d67d1da2f6425b339192fa472898dce936f24818fda415c1c1ff3f - Burn / withdrawal tx:
0x6eabef1bf310a1361041d97897c192581cd9870f6a39040cd24d7de2335b4546 - Example Tornado Cash laundering tx:
0x0e16210218ecc487a35b9ff48fe3d9f3e9b0f50330f9e7805e38135732b85270
- Vulnerable vVISR contract:
- Response / mitigation:
- Visor published a post-mortem and proposed a token migration based on a pre-exploit snapshot.
- The incident accelerated a migration away from the compromised staking setup and fed into the protocol’s broader restructuring around Gamma / successor products.
- The exploit also became a cited example in later academic work on address-verification failures, because the vulnerable logic looked like an authorization check but still let attacker-deployed contracts masquerade as trusted payment endpoints.
Pattern tags
- account-confusion-root-of-trust-substitution
- issuance-redemption-solvency-asymmetry
- The exploit only needed one counterfeit trust anchor: once Visor let an attacker-chosen
fromcontract stand in for both payer identity and payment execution, the honestvVISR -> VISRredemption leg converted that local authentication error into reserve loss. - This is a clean
account confusionincident rather than a generic callback bug. The narrow callback question only matters after the larger mistake of letting caller-authored contract behavior define who the payer was and whether payment happened at all.
Why the exploit worked
- Visor collapsed three different questions into one helper-shaped check:
what exact entity is the canonical payer?,what exact rail proves VISR moved?, anddoes the resulting share mint deserve reserve-backed redemption authority?The attacker only had to satisfy the first with their own contract for the other two to be granted implicitly. - The
deposit()path treated interface shape plusowner() == msg.senderas if it were economic proof. That authenticated the attacker’s helper as a legitimate payer endpoint even though the protocol had not pinned that helper to any protocol-owned payer registry or measured a canonicalVISRbalance delta. - Once the counterfeit mint succeeded, the exit side stayed honest.
vVISRburns still redeemed against real staking reserves, so an issuance-side receipt failure immediately became a drain rather than merely a bad accounting artifact. - That is why the exploit is a strong corpus exemplar: the protocol did not need an oracle error, weak withdrawal auth, or exotic market manipulation. It only needed one attacker-authored contract to be promoted from
callable helperintopayer truth, and then frompayer truthintoreserve-backed share truth.
Three payer-and-receipt promotions this incident collapsed
Some helper-shaped contract exists at the caller-supplied from addressis not the same thing asthat contract is the canonical payer or delegated payment endpoint this mint path meant to trust.That helper passed owner() / delegatedTransferERC20(...)is not the same thing ascanonical VISR receipt actually arrived and now counts as payment truth for this issuance action.The protocol minted vVISR against that accepted payment storyis not the same thing asthose shares still deserve immediate reserve-backed redemption authority against real VISR inventory right now.- Visor collapsed those promotions into one helper-authenticity assumption: once an attacker-controlled contract looked like an authorized delegated payer, the protocol let that same contract stand in for receipt proof and then honored the resulting shares through an honest reserve exit.
Detection and audit pivots
- Enumerate every mint, stake, or deposit rail that accepts a user-supplied
from, helper, adapter, or payer contract. Ask which of those surfaces are protocol-pinned and which are merely interface-compatible. - Trace where receipt is actually measured. If a helper call can run successfully without a canonical token balance delta or single-use escrow debit being consumed, the mint path is still helper-authored rather than receipt-authored.
- Fuzz delegated-payer flows with hostile contracts that satisfy
owner()or similar auth checks, return success, no-op internally, partially transfer, or recurse. The question is not just whether the helper can re-enter, but whether it can impersonate payment truth at all. - Compare issuance invariants against redemption invariants. If burning shares still pays out honest reserves after a mint path accepted weaker proof than
real assets arrived, the protocol has already built a drain rail. - Alert on
share supply up / reserve assets unchangedstates and on large reserve withdrawals immediately after a fresh mint from an adapter or delegated-payer path.
Why this incident matters
- It preserves a clean
counterfeit payer endpoint -> unbacked claim mint -> honest reserve redemptionchain. - It shows that verifying
owner()on a caller-supplied contract is not the same thing as verifying canonical payment authority. - It is also a good reminder that issuance-side payment proof is a security boundary of its own: once minting trusts attacker-authored transfer semantics, the redemption side becomes the actual drain rail.
Failure-prevention checklist
- Pin delegated or third-party payer contracts to protocol-owned config, an explicit allowlist, or an immutable adapter registry. Do not let callers choose arbitrary contracts to answer
who is paying?for mint or staking flows. - Separate payer authentication from receipt measurement. Passing an authorization check on a helper contract should still be insufficient until the protocol proves canonical
VISRactually arrived on the intended rail. - Treat helper callbacks like untrusted integration surfaces. If a delegated payer is necessary, its interface should expose narrow, typed semantics and post-conditions the helper cannot self-author into existence.
- Require one protocol-owned receipt object per mint: measured token balance delta, escrow debit, or similarly bounded payment proof that can only be consumed once.
- Make
deposit()-style issuance paths fail closed if the asset movement step is skipped, returns ambiguous success, or is delegated through arbitrary user code. - Keep issuance and redemption tightly coupled under the same solvency invariant: if the mint side can accept weaker payment proof than the burn side requires for payout, treat that asymmetry as an immediate treasury-risk bug.
- Add supply-side circuit breakers for unexplained share issuance, sudden share-supply jumps, and large reserve drawdowns immediately following fresh mint events.
- Fuzz delegated-payer paths with attacker-authored contracts that self-report ownership, no-op transfers, partial transfers, fake callbacks, and nested calls into the same mint path.
- Keep emergency response able to pause both new issuance and reserve-side redemption quickly, because counterfeit-share incidents monetize through the honest exit leg.
Audit prompts
- What exact protocol-owned object proves that this
fromaddress is an admitted payer rather than just a contract the caller chose? - If the payer is a contract, what prevents it from self-certifying both payer identity and payment success through methods like
owner()anddelegatedTransferERC20(...)? - Where is actual receipt measured, and can minting continue if the helper call ran but no canonical token balance delta entered the staking contract?
- Could a malicious helper satisfy every interface and authorization check while doing nothing economically meaningful on the payment rail?
- Does the mint path bind one measured receipt to one issuance action, or can helper-controlled execution, nested calls, or stale assumptions amplify the resulting share mint?
- If the protocol supports delegated payment for UX reasons, what exact asset, amount, recipient, and route limits keep that helper from becoming a generic counterfeit-mint endpoint?
- Once new shares exist, what onchain control limits immediate redemption or reserve withdrawal before operators can pause the system?
- If the protocol’s answer is
we verified the helper owns the payer role, where exactly did it separately verify that backing assets moved and cannot be socially constructed by the helper itself?
Lessons that should already be standard
- A caller-supplied contract is not a trust anchor just because it implements the expected methods. Interface shape and canonical payment authority are different properties.
Who is allowed to pay?anddid payment actually happen?are separate security questions. Visor blurred them together and let the same attacker-owned contract answer both.- Delegated payment helpers are part of the mint authority surface. If they are generic, user-selectable, or self-certifying, they are counterfeit-share printers waiting for a reserve-backed exit leg.
- Honest redemption logic does not save a system whose issuance side already accepted fake payment proof. It becomes the monetization rail.
- Looking like an authorization check is not enough.
owner() == msg.senderon attacker-authored code is still attacker-authored code. - If a protocol cannot point to one narrow receipt object that funded a mint, then the mint path is already relying on folklore rather than accounting.
- Reserve-backed staking or vault systems should assume attackers will search for the weakest receipt-proof boundary, not just the obvious withdrawal function.
Internal linkages
- Sharper recurring pattern: account-confusion-root-of-trust-substitution.
- Positive-design companion: canonical-payer-identity-and-receipt-binding-discipline — the healthier question is what exact payer identity, payment rail, and one-shot receipt object
deposit()should have required before mintingvVISRcould proceed at all. - Economic cash-out pattern: issuance-redemption-solvency-asymmetry.
- Adjacent execution-side lens: reentrancy — useful only for the narrower
delegatedTransferERC20()callback amplification story; the exploit already paid once Visor let a counterfeit payer contract stand in for real payment proof.
Control surface
-
The leverage sat in Visor’s payer-authentication boundary: who was allowed to act as
from, how thatfromcontract proved authenticity, whetherdelegatedTransferERC20(...)was allowed to define payment truth, and whethervVISRminting independently checked that backingVISRhad actually arrived. -
The protocol did not mainly fail because the attacker found a clever dump venue. It failed because the staking contract let an attacker-authored contract certify and simulate the very transfer that was supposed to back new shares.
-
Sources:
- https://rekt.news/visor-finance-rekt
- https://x.com/storming0x/status/1473321779250802693
- https://medium.com/visorfinance/post-mortem-for-vvisr-staking-contract-exploit-and-upcoming-migration-7920e1dee55a
- https://www.usenix.org/system/files/usenixsecurity24-sun-tianle.pdf
- https://hackernoon.com/a-$82m-hack-and-the-blind-spot-in-ethereum-smart-contract-verification
-
Last reviewed: 2026-07-31 UTC