All audits
FailedEthereumIndependent

Visor Finance (2021)

Smart contract security audit by TierZero · 2023-03-15

Independent research. This is an unsolicited security analysis published by TierZero — not a commissioned audit, and no certificate is issued. It reflects our own review of public code and on-chain events.

Findings summary

1
Critical
1
High
2
Medium
1
Low
1
Informational

Independent research — not a commissioned audit. This is a post-mortem analysis of a publicly documented incident, compiled from public reports and on-chain data.

What the protocol did

Visor Finance built concentrated-liquidity management vaults ("Hypervisors") on top of Uniswap V3. Users deposited paired tokens into a Hypervisor contract, which handled range-setting, rebalancing, and fee compounding on their behalf, and in return received vVISR (or vault-specific LP) shares representing their claim on the underlying position. A periphery layer coordinated deposits, approvals, and reward accounting so the average LP didn't have to manage V3 ticks manually.

In December 2021, an attacker drained a large share of the protocol's Uniswap V3 positions and associated user funds — publicly reported at the time in the range of several million dollars — by abusing how the Hypervisor's deposit path assigned ownership of newly minted shares. VISR's market price also fell sharply once the exploit became public, compounding the loss for token holders beyond the directly stolen funds.

The vulnerability

The root cause was a classic trusted-input flaw: the deposit function accepted a caller-supplied address as the recipient/owner of the newly minted vault shares, without independently verifying that the address invoking the deposit — or the address whose tokens were being pulled — actually controlled or authorized that share allocation. Any address that already held a token allowance against the Hypervisor (set for legitimate reasons, such as auto-compounding or a periphery integration) could be treated as the source of funds for a deposit initiated by someone else, while the shares themselves were minted to whatever address the caller specified.

Illustrative pattern of the flaw (not the literal source):

function deposit(
    uint256 amount0,
    uint256 amount1,
    address to // caller-controlled recipient, unchecked
) external returns (uint256 shares) {
    // pulls from an address with an existing allowance,
    // but never confirms msg.sender == source of funds
    token0.transferFrom(from, address(this), amount0);
    token1.transferFrom(from, address(this), amount1);

    shares = _calcShares(amount0, amount1);
    _mint(to, shares); // trusts caller-supplied owner
}

The bug class is "unvalidated ownership on a value-minting entry point": a function that both moves funds and mints a claim on those funds needs to tie the two together cryptographically or via msg.sender, not accept the beneficiary as free-form input. This is the same family of issue we flag repeatedly in EVM vault code, and it rhymes with the approval-trust problems we cover when reviewing gasless-approval flows — see our notes on how a poorly scoped signature or allowance can be replayed against unrelated flows in Solidity Permit2 gasless approvals and their EVM sniping-bot exposure.

How the exploit worked

  1. The attacker first obtained a valid token allowance from a victim contract or user — publicly reported as the result of a social-engineering step, where a party was induced to interact with a malicious contract or sign a transaction that granted the Hypervisor periphery an allowance over their VISR/paired tokens.
  2. With that allowance in place, the attacker called the Hypervisor's deposit function directly, bypassing the periphery's intended checks, and supplied the victim's approved address as the source of the deposited tokens.
  3. The deposit logic pulled the tokens using the pre-existing allowance and minted the corresponding vVISR/vault shares to an address the attacker controlled, rather than to the token owner.
  4. The attacker then redeemed or transferred the minted shares, extracting value that belonged to the original approvers, and repeated the pattern across available allowances until the position was drained.

The exploit therefore combined an off-chain trust failure (getting an allowance signed under false pretenses) with an on-chain validation failure (the contract's willingness to mint shares to an arbitrary address once any valid allowance existed). Neither half alone was sufficient; the contract-level gap turned a single phishing success into a scalable drain.

How an audit catches this

This is exactly the class of issue a structured smart contract audit is built to surface, because it isn't found by unit tests written against the happy path — it requires adversarial thinking about who can call a function and with whose funds.

  • Invariant check: for any function that mints a claim token, assert that the beneficiary of the mint is always msg.sender, a value derived from a signed message the recipient authorized, or an address explicitly and immutably tied to the funds source — never a bare caller-supplied parameter.
  • Allowance-source tracing: for every transferFrom, enumerate who could plausibly hold an allowance against the contract (periphery integrations, auto-compounders, prior approvals from unrelated flows) and verify the contract cannot be tricked into treating a stranger's allowance as its own deposit.
  • Differential test: write a test where address A holds an allowance and address B calls deposit specifying A as source and B as recipient; the transaction should revert.
  • Entry-point enumeration: confirm that privileged or fund-moving functions cannot be reached by skipping the periphery/router that was assumed to be the only caller — a pattern we check systematically as part of code review and audit engagements, since assumptions about "the only caller will be our own frontend" break the moment a contract is composable.

The formal-verification mindset applies directly here too: encoding "minted shares always correspond to the depositor of record" as a provable property, rather than a code-review heuristic, is the kind of invariant work we discuss in the context of formal verification for trading program logic — the underlying principle (state-changing functions must be bound to their true authorizer) is chain-agnostic.

Remediation

The fix pattern is narrow and well understood: bind minted shares to msg.sender by default, and if a third-party recipient is genuinely needed (e.g., depositing on behalf of another account), require that recipient's explicit, single-use authorization — a signed permit scoped to the specific deposit amount and nonce, not a standing allowance that any caller can invoke against. Separately, protocols relying on user-granted allowances to a periphery contract should treat that periphery as the sole authorized caller of the underlying vault's fund-moving functions, enforced on-chain via an access-control check, not by convention. Finally, any flow that asks users to sign a transaction or grant an approval outside the primary application UI warrants its own scrutiny during development — this is the kind of design question worth raising with an auditor before shipping, not after, and it's the reason we fold access-control and approval-flow review into every smart contract development engagement rather than treating it as a bolt-on.

If your vault or LP-management contracts mint claims against user-supplied recipients or rely on standing allowances, it's worth having them checked — get a smart contract audit scoped to exactly this class of bug.

Need your contracts audited?

Manual review + tooling across TON, Solana and EVM — certificate and full report included.

Get an audit