We do not have a reliable USD price for the recorded assets yet.
0xc9f27a50f82571c1c8423a42970613b8dbda14efEthereumAn unprivileged Ethereum mainnet EOA 0x8efab89b497b887cdaa2fb08ff71e4b3827774b2 deployed and configured a self-owned IVisor-compatible orchestrator contract 0x10c509aa9ab291c76c45414e7cdbd375e1d5ace8 that targets the VISR RewardsHypervisor staking pool 0xc9f27a50f82571c1c8423a42970613b8dbda14ef. In transaction 0x69272d8c84d67d1da2f6425b339192fa472898dce936f24818fda415c1c1ff3f (block 13849007) the EOA calls the orchestrator, which triggers two nested calls to RewardsHypervisor.deposit(1e26, 0x10c5..ace8, 0x8efa..74b2).
Because RewardsHypervisor.deposit trusts the external IVisor.delegatedTransferERC20 callback, computes vVISR shares from an attacker-chosen visrDeposit and existing pool balances, and neither enforces that visr.balanceOf(this) increases nor guards against re-entrancy, the orchestrator re-enters deposit() instead of transferring VISR. Across the two calls, the hypervisor mints 195249950963631432273419474 vVISR to the EOA while its VISR balance remains unchanged. In transaction 0x6eabef1bf310a1361041d97897c192581cd9870f6a39040cd24d7de2335b4546 (block 13849051) the EOA calls withdraw() with exactly these shares, deterministically receiving VISR from the pool, and then swaps the withdrawn VISR for ETH on DEXes.
8812958138426966035592617The VISR RewardsHypervisor 0xc9f2..14ef is a single-sided staking contract that pools VISR token 0xf938424f7210f31df2aee3011291b658f872e91e and issues vVISR ERC20 share tokens 0x3a84ad5d16adbe566baa6b3dafe39db3d5e261e5. Users are expected to call deposit(visrDeposit, from, to) to move VISR into the hypervisor in exchange for vVISR shares, and later call withdraw(shares, to, from) to redeem VISR based on the pool’s VISR balance and total vVISR supply.
The verified RewardsHypervisor implementation contains the following core logic:
function deposit(
uint256 visrDeposit,
address payable from,
address to
) external returns (uint256 shares) {
require(visrDeposit > 0, "deposits must be nonzero");
require(to != address(0) && to != address(this), "to");
require(from != address(0) && from != address(this), "from");
shares = visrDeposit;
if (vvisr.totalSupply() != 0) {
uint256 visrBalance = visr.balanceOf(address(this));
shares = shares.mul(vvisr.totalSupply()).div(visrBalance);
}
if(isContract(from)) {
require(IVisor(from).owner() == msg.sender);
IVisor(from).delegatedTransferERC20(address(visr), address(this), visrDeposit);
}
else {
visr.safeTransferFrom(from, address(this), visrDeposit);
}
vvisr.mint(to, shares);
}
function withdraw(
uint256 shares,
address to,
address payable from
) external returns (uint256 rewards) {
require(shares > 0, "shares");
require(to != address(0), "to");
require(from != address(0), "from");
rewards = visr.balanceOf(address(this)).mul(shares).div(vvisr.totalSupply());
visr.safeTransfer(to, rewards);
require(from == msg.sender || IVisor(from).owner() == msg.sender, "Sender must own the tokens");
vvisr.burn(from, shares);
}
The vVISR token 0x3a84..61e5 is an ERC20Permit + ERC20Snapshot share token whose mint and burn functions are restricted to an owner (initially the deployer, later transferred to the RewardsHypervisor). Users never call vVISR directly; they interact exclusively via deposit() and withdraw(), so any bug in the hypervisor’s share-minting or redemption logic directly translates into unbacked vVISR supply or mispriced withdrawals.
To support integrations, deposit() accepts a from address that may be a contract. When from is a contract, it is treated as an IVisor helper and the hypervisor performs an external callback:
if (isContract(from)) {
require(IVisor(from).owner() == msg.sender);
IVisor(from).delegatedTransferERC20(address(visr), address(this), visrDeposit);
} else {
visr.safeTransferFrom(from, address(this), visrDeposit);
}
The assumption is that delegatedTransferERC20 will move visrDeposit VISR from the integration contract to the hypervisor before vvisr.mint(to, shares) executes. There is no re-entrancy guard and no post-condition tying minted shares to an observed increase in visr.balanceOf(address(this)).
The adversary’s orchestrator contract 0x10c5..ace8 is an IVisor-compatible helper configured with pool = 0xc9f2..14ef and self-owned via an internal ownership variable. The decompiled delegatedTransferERC20 and the public 0x4a0b0c38 entrypoint both call back into the pool rather than transferring VISR:
/// @custom:selector 0x2e88fb97
/// delegatedTransferERC20(address arg0, address arg1, uint256 arg2)
function delegatedTransferERC20(address arg0, address arg1, uint256 arg2) public payable {
count = 0x01 + count;
require(!count < 0x02);
uint256 amount = 0x52b7d2dcc80cd2e4000000; // 1e26
require(address(pool).code.length != 0);
// calls pool with a function selector that resolves to RewardsHypervisor.deposit
(bool success, bytes memory ret0) = address(pool).call(abi.encodeWithSelector(0x2e2d2984, amount));
require(ret0.length >= 0x20);
}
/// @custom:selector 0x4a0b0c38
function exploitEntry() public payable {
uint256 amount = 0x52b7d2dcc80cd2e4000000; // 1e26
require(address(pool).code.length != 0);
(bool success, bytes memory ret0) = address(pool).call(abi.encodeWithSelector(0x2e2d2984, amount));
require(ret0.length >= 0x20);
}
This design makes the hypervisor vulnerable to a re-entrant deposit when from is the orchestrator and msg.sender is its owner.
The vulnerability is a re-entrancy and invariant-enforcement bug in the VISR RewardsHypervisor’s deposit() function. When from is a contract, deposit() calls an untrusted external callback IVisor(from).delegatedTransferERC20 before minting vVISR shares, but it computes the share amount as
shares = visrDeposit * vVISR.totalSupply() / visr.balanceOf(address(this))
using only the caller-supplied visrDeposit and the current pool balances, without enforcing that the VISR balance actually increases by visrDeposit. There is no re-entrancy guard around this external call and no post-condition check tying shares to any observed asset inflow.
A malicious IVisor contract can exploit this by having delegatedTransferERC20 re-enter RewardsHypervisor.deposit() instead of transferring VISR. In the incident, the orchestrator calls deposit(1e26, orchestrator, EOA) twice within a single transaction, each time computing shares from essentially the same pre-attack VISR balance and vVISR totalSupply, and then minting vVISR to the attacker’s EOA without any VISR transfers. This breaks the collateralization invariant that vVISR totalSupply must be fully backed by VISR held by the hypervisor and sets up a deterministic VISR theft via withdraw().
Let V denote visr.balanceOf(RewardsHypervisor) and S denote vVISR.totalSupply() immediately before the exploit transaction. From the VISR/vVISR balance window for the hypervisor address, the pool’s VISR balance across blocks 13849002–13849012 (including the seed block 13849007) is constant:
{
"hypervisor": "0xc9f27a50f82571c1c8423a42970613b8dbda14ef",
"tokens": {
"0xf938424f7210f31df2aee3011291b658f872e91e": {
"symbol": "VISR",
"balances": [
{ "block": 13849006, "balance": "9219200268612237484049971" },
{ "block": 13849007, "balance": "9219200268612237484049971" }
]
}
},
"seed_block": 13849007
}
Thus the pool holds
V = 9219200268612237484049971 VISR
before and after the seed transaction. From the seed trace and vVISR source, the pre-attack vVISR total supply is
S = 9000242001852185487035933 vVISR
The intended invariant for a healthy state is:
deposit() either increases V by visrDeposit and mints proportional shares, or reverts.vVISR.mint is only invoked when corresponding VISR has been transferred into the pool.Formally, for any account the total redeemable VISR for all its vVISR shares should not exceed V, and the aggregate vVISR supply should correspond to VISR actually held by the pool.
In the seed transaction 0x6927..1ff3f the adversary EOA calls the orchestrator’s 0x4a0b0c38 exploit entrypoint. The trace shows the orchestrator calling RewardsHypervisor.deposit and then re-entering it via delegatedTransferERC20:
├─ RewardsHypervisor::deposit(100000000000000000000000000 [1e26], 0x10C5..ACE8, 0x8EFA..74B2)
│ ├─ ERC20::balanceOf(RewardsHypervisor)
│ ├─ 0x10C5..ACE8::delegatedTransferERC20(VISR, RewardsHypervisor, 1e26)
│ │ ├─ RewardsHypervisor::deposit(100000000000000000000000000 [1e26], 0x10C5..ACE8, 0x8EFA..74B2)
│ │ │ ├─ ERC20::balanceOf(RewardsHypervisor)
│ │ │ ├─ 0x10C5..ACE8::delegatedTransferERC20(VISR, RewardsHypervisor, 1e26)
│ │ │ │ ├─ emit Transfer(0x0000000000000000000000000000000000000000, 0x8EFA..74B2, 97624975481815716136709737)
│ │ ├─ emit Transfer(0x0000000000000000000000000000000000000000, 0x8EFA..74B2, 97624975481815716136709737)
Key observations from this trace and the accompanying balance data:
Both deposit(1e26, 0x10c5..ace8, 0x8efa..74b2) calls execute with the same pre-attack V and S, because visr.balanceOf(this) is only read (staticcall) and the orchestrator never transfers VISR.
Each deposit() call computes
shares_each = 1e26 * S / V ≈ 97624975481815716136709737 vVISR
and then calls vvisr.mint(to, shares_each).
The nested structure causes vvisr.mint to execute twice with to = 0x8efa..74b2, producing a total of
shares_total = 2 * shares_each
= 195249950963631432273419474 vVISR
as confirmed by the two Transfer events from the vVISR token contract to the EOA in the trace.
Throughout the seed trace, there are no Transfer events for VISR involving the hypervisor, and the VISR balance window confirms that V remains at 9219200268612237484049971 across the window containing block 13849007.
As a result, the adversary’s EOA ends the seed transaction holding 195249950963631432273419474 newly minted vVISR shares that are completely unbacked by additional VISR in the pool, violating the collateralization invariant.
After the seed transaction, the adversary retains control of the unbacked vVISR. In transaction 0x6eab..b4546 (block 13849051), the EOA calls withdraw() directly on the hypervisor with
shares = 0xa181c90cf59ed4f8c6dcd2
= 195249950963631432273419474 vVISR
matching the total unbacked vVISR minted in the exploit transaction. At this point:
The hypervisor’s VISR balance is still V = 9219200268612237484049971 (no intermediate VISR movements to or from the pool between the exploit and withdraw in the hypervisor’s tx history).
The vVISR total supply has increased to
S' = S + shares_total
= 9000242001852185487035933
+ 195249950963631432273419474
= 204250192965483617760455406.914376… (before rounding)
The withdraw() function computes
rewards = V * shares_total / S'
≈ 8812958138426966035592617 VISR
which matches the VISR amount recorded in the root cause analysis as the deterministic payout from the pool. This calculation uses only on-chain quantities (V, S, and shares_total) and the exact ERC20 amounts observed in the trace and source; no off-chain prices or assumptions are required.
The exploit’s concrete breakpoint can therefore be summarized as:
deposit() mints vVISR based solely on visrDeposit, V, and S, while the orchestrator re-enters deposit() through delegatedTransferERC20 without performing a VISR transfer, causing V to remain constant while S and the attacker’s vVISR balance increase.withdraw() uses the inflated vVISR supply and the attacker’s unbacked shares to compute rewards = V * shares / S', transferring 8812958138426966035592617 VISR from the pool to the attacker.These two operations together permanently violate the pool’s collateralization invariant and drain most of the VISR backing honest vVISR holders.
The adversary lifecycle is fully captured by a small cluster of transactions executed by EOA 0x8efa..74b2 on Ethereum mainnet:
Adversary contract deployment and configuration
0xbe65cb0dd9f4619939cfeb56b3ef3a996e2b028b93fd66443abfa06d6df8e58d (block 13848982): The EOA deploys the orchestrator contract 0x10c5..ace8 with constructor arguments pointing pool to RewardsHypervisor 0xc9f2..14ef.0x27f2210536553392cf180c0b37055b3dc92094a5d585d7d2a51f790c9145e47c (block 13848983): The EOA calls transferOwnership(0x10c5..ace8) on the orchestrator, making the contract self-owned. This guarantees that the check IVisor(from).owner() == msg.sender in RewardsHypervisor.deposit passes when msg.sender is the EOA and from is the orchestrator.Re-entrant deposit and unbacked vVISR minting
0x69272d8c84d67d1da2f6425b339192fa472898dce936f24818fda415c1c1ff3f (block 13849007): The EOA calls orchestrator function 0x4a0b0c38. The orchestrator calls RewardsHypervisor.deposit(1e26, 0x10c5..ace8, 0x8efa..74b2) and, via delegatedTransferERC20, re-enters deposit() with the same arguments. In both calls, shares is computed from the same V and S, and vvisr.mint(to, shares) executes twice, minting a total of 195249950963631432273419474 vVISR to the EOA. The trace and VISR balance window confirm no VISR transfers into the hypervisor during this transaction.VISR withdrawal and profit realization
0x6eabef1bf310a1361041d97897c192581cd9870f6a39040cd24d7de2335b4546 (block 13849051): The EOA calls RewardsHypervisor.withdraw directly with shares = 195249950963631432273419474 and to = from = 0x8efa..74b2. Using the on-chain V and S', withdraw() transfers 8812958138426966035592617 VISR from the pool to the EOA.0x2dfd114bb28f6625f08663c8782c0a72bef89d6c0d4457542928a3f2416dd964 (block 13849059): The EOA approves VISR spending for a DEX router.0x86d2689eeb9b1dd233e6a9ab62ffa16ecdedff55ea5f6f10571432cf9830d907 (block 13849099) and Tx 0xbd6e12468cb498d35fc9f0f9679c2e74a70de09d1c79b42ed54fefbd7ff7131a (block 13849105): The EOA executes swapExactTokensForETH calls on Uniswap-style routers, converting the withdrawn VISR into ETH under standard ERC20 and DEX semantics.Throughout this lifecycle, the adversary operates entirely through unprivileged accounts and contracts they control. The orchestrator’s exploit entrypoint 0x4a0b0c38 is a publicly callable function that simply requires the caller to be its owner (which any unprivileged EOA can arrange for its own copy). The exploitation strategy is therefore an anyone-can-take (ACT) opportunity: any unprivileged searcher could reproduce the same sequence by deploying and configuring an equivalent orchestrator against a hypervisor in the same vulnerable state.
From the perspective of the VISR RewardsHypervisor pool, the exploit has a single dominant on-chain impact: it drains 8812958138426966035592617 VISR from the pool to the adversary cluster without any legitimate VISR deposit.
9219200268612237484049971 VISR.withdraw() transfers 8812958138426966035592617 VISR to the attacker’s EOA, corresponding to approximately 95.6% of the pre-attack pool balance.In metadata terms, the quantifiable loss is:
Token: VISR (0xf938424f7210f31df2aee3011291b658f872e91e)
Amount: 8812958138426966035592617 (18 decimals)
Chain: Ethereum mainnet (chainid 1)
Subsequent swaps by the attacker convert this VISR into ETH, but the economic harm to the protocol is fully captured by the deterministic VISR transfer from the hypervisor to the adversary cluster.
[1] RewardsHypervisor.sol verified source
artifacts/root_cause/data_collector/iter_1/contract/1/0xc9f27a50f82571c1c8423a42970613b8dbda14ef/source/src/RewardsHypervisor.sol
[2] vVISR.sol verified source
artifacts/root_cause/data_collector/iter_1/contract/1/0xc9f27a50f82571c1c8423a42970613b8dbda14ef/source/src/vVISR.sol
[3] VISR ERC20 verified source
artifacts/root_cause/data_collector/iter_1/contract/1/0xf938424f7210f31df2aee3011291b658f872e91e/source/src/Contract.sol
[4] Orchestrator 0x10c5..ace8 decompiled source
artifacts/root_cause/data_collector/iter_2/contract/1/0x10c509aa9ab291c76c45414e7cdbd375e1d5ace8/decompile/0x10c509aa9ab291c76c45414e7cdbd375e1d5ace8-decompiled.sol
[5] Seed transaction trace (re-entrant deposit)
artifacts/root_cause/data_collector/iter_1/tx/1/0x69272d8c84d67d1da2f6425b339192fa472898dce936f24818fda415c1c1ff3f/trace.cast.log
[6] RewardsHypervisor VISR/vVISR balance window
artifacts/root_cause/data_collector/iter_2/address/1/0xc9f27a50f82571c1c8423a42970613b8dbda14ef/visr_vvisr_balance_window.json
[7] Adversary EOA 0x8efa..74b2 full tx history
artifacts/root_cause/data_collector/iter_1/address/1/0x8efab89b497b887cdaa2fb08ff71e4b3827774b2/normal_txlist.json