BriVault

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

Bypass of Event Locks via Inherited ERC4626 Functions

Root + Impact

Description

  • The BriVault contract implements a time-locked betting mechanism where users should only be able to withdraw funds after the event concludes and only if they backed the winning outcome.

  • However, by inheriting from ERC4626 without overriding the standard withdrawal functions, users can completely bypass all event restrictions and withdraw their funds at any time.

contract BriVault is ERC4626, Ownable {
// Custom withdraw function with proper restrictions
function withdraw() external winnerSet {
if (block.timestamp < eventEndDate) {
revert eventNotEnded();
}
if (userToCountry[msg.sender] != winner) {
revert didNotWin();
}
// ... custom withdrawal logic
}
// VULNERABILITY: Inherits ERC4626's standard functions without restrictions:
// - withdraw(uint256 assets, address receiver, address owner)
// - redeem(uint256 shares, address receiver, address owner)
// - maxWithdraw(address owner)
// - maxRedeem(address owner)
}

Risk

Likelihood: High

  • The inherited functions are publicly visible in the ABI and follow standard ERC4626 patterns

  • Users seeking early liquidity or those who backed losing outcomes have clear incentive to use these functions

  • The vulnerability is easily discoverable through standard blockchain explorers or interface generators

Impact: High

  • Complete circumvention of the vault's core betting mechanics

  • Early withdrawals drain the prize pool before the event concludes

  • Losing bettors can recover funds, eliminating the risk/reward model

  • The vault becomes economically non-viable

Proof of Concept

  • Add testEarlyWithdraw to briVault.t.sol

  • Run forge test --mt testEarlyWithdraw

function testEarlyWithdraw() public {
//user deposit
vm.prank(user1);
mockToken.approve(address(briVault), 5 ether);
vm.prank(user1);
briVault.deposit(5 ether, user1);
//user1 balance before withdraw
uint256 user1BalanceBeforeWithdraw = mockToken.balanceOf(user1);
// staked user1 assets
// withdraw
vm.prank(user1);
briVault.redeem(1 ether, user1, user1);
assertEq( mockToken.balanceOf(user1),user1BalanceBeforeWithdraw + 1 ether);
// user2 deposit
vm.prank(user2);
mockToken.approve(address(briVault), 5 ether);
vm.prank(user2);
briVault.deposit(5 ether, user2);
// user joint event
vm.prank(user2);
briVault.joinEvent(0);
//event end
vm.warp(eventEndDate + 1);
// owner set winner
vm.prank(owner);
briVault.setWinner(1);
// when user2 knows that he does not win
// he withdraw his token
//user1 balance before withdraw
uint256 user2BalanceBeforeWithdraw = mockToken.balanceOf(user2);
vm.prank(user2);
briVault.redeem(1 ether, user2, user2);
assertEq( mockToken.balanceOf(user2),user2BalanceBeforeWithdraw + 1 ether);
}

Recommended Mitigation

  • Override all ERC4626 withdrawal functions to enforce vault business logic

unction withdraw(uint256 assets, address receiver, address owner)
public
override
winnerSet
returns (uint256)
{
if (block.timestamp < eventEndDate) {
revert eventNotEnded();
}
if (
keccak256(abi.encodePacked(userToCountry[owner])) !=
keccak256(abi.encodePacked(winner))
) {
revert didNotWin();
}
return super.withdraw(assets, receiver, owner);
}
function redeem(uint256 shares, address receiver, address owner)
public
override
winnerSet
returns (uint256)
{
if (block.timestamp < eventEndDate) {
revert eventNotEnded();
}
if (
keccak256(abi.encodePacked(userToCountry[owner])) !=
keccak256(abi.encodePacked(winner))
) {
revert didNotWin();
}
return super.redeem(shares, receiver, owner);
}
// Also override view functions to reflect restrictions
function maxWithdraw(address owner) public view override returns (uint256) {
if (block.timestamp < eventEndDate || !_setWinner) {
return 0;
}
if (
keccak256(abi.encodePacked(userToCountry[owner])) !=
keccak256(abi.encodePacked(winner))
) {
return 0;
}
return super.maxWithdraw(owner);
}
function maxRedeem(address owner) public view override returns (uint256) {
if (block.timestamp < eventEndDate || !_setWinner) {
return 0;
}
if (
keccak256(abi.encodePacked(userToCountry[owner])) !=
keccak256(abi.encodePacked(winner))
) {
return 0;
}
return super.maxRedeem(owner);
}
Updates

Appeal created

bube Lead Judge 19 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!