BriVault

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

Incomplete Function Override Allows ERC4626 Withdraw to Bypass Competition Lock-In Restrictions

Incomplete Function Override Allows ERC4626 Withdraw to Bypass Competition Lock-In Restrictions

Description

  • The briVault.sol contract locks user funds during the competition event. Users can only withdraw after the event ends and a winner is set, and only if they selected the winning country. This ensures participants remain committed until the competition concludes.

  • The contract inherits from ERC4626.sol, which exposes a public withdraw(uint256 assets, address receiver, address owner) function. Since briVault.sol implements a custom withdraw() function with a different signature (no parameters), it does not override the ERC4626 function. Users can call the ERC4626 withdraw function directly during the event, bypassing all restrictions (winner check, event end check, winner validation) and allowing them to exit the competition and withdraw funds at any time, including just before the event ends.

abstract contract ERC4626 is ERC20, IERC4626 {
// ...rest of code...
// @inheritdoc IERC4626
@> function withdraw(uint256 assets, address receiver, address owner) public virtual returns (uint256) {
uint256 maxAssets = maxWithdraw(owner);
if (assets > maxAssets) {
revert ERC4626ExceededMaxWithdraw(owner, assets, maxAssets);
}
uint256 shares = previewWithdraw(assets);
_withdraw(_msgSender(), receiver, owner, assets, shares);
return shares;
}
// ...rest of code...
}

Risk

Likelihood:

  • The ERC4626::withdraw(uint256, address, address) function remains accessible because briVault.sol inherits from ERC4626.sol and does not override it.

  • Users can call this function at any time during the event period, including just before the event ends making it extremely likely.

Impact:

  • Participants can exit the competition during the event and withdraw their funds, avoiding losses when they anticipate their team will lose. This defeats the intended lock-in mechanism, allowing users to game the system and withdraw funds that should remain locked until the winner is determined.

Proof of Concept

Add test_withdraw_funds_during_event_using_ERC4626_withdraw() to the briVault.t.sol test file.

function test_withdraw_funds_during_event_using_ERC4626_withdraw() public {
vm.startPrank(owner);
briVault.setCountry(countries);
vm.stopPrank();
vm.startPrank(user1);
mockToken.approve(address(briVault), 5 ether);
briVault.deposit(5 ether, user1);
briVault.joinEvent(10);
uint256 balanceBefore = mockToken.balanceOf(user1);
vm.stopPrank();
vm.startPrank(user2);
mockToken.approve(address(briVault), 5 ether);
briVault.deposit(5 ether, user2);
briVault.joinEvent(20);
vm.stopPrank();
// Warp to just before the event ends (e.g., 1 day before eventEndDate)
// This simulates a user realizing their team is losing and wanting to exit
vm.warp(eventEndDate - 1 days);
vm.startPrank(user1);
// Withdraw all available funds to save them from potential loss
uint256 maxWithdrawable = briVault.maxWithdraw(user1);
uint256 assetsToWithdraw = maxWithdrawable;
// Call ERC4626 withdraw function directly: withdraw(uint256 assets, address receiver, address owner)
// This bypasses the custom withdraw() function with all its checks
briVault.withdraw(assetsToWithdraw, user1, user1);
uint256 balanceAfter = mockToken.balanceOf(user1);
uint256 balanceAfterShares = briVault.balanceOf(user1);
// Verify that the withdrawal succeeded - user1 saved their funds by exiting during the event
assertGt(balanceAfter, balanceBefore, "User should have received tokens back");
assertEq(balanceAfterShares, 0, "User should have withdrawn all shares");
assertEq(balanceAfter, balanceBefore + assetsToWithdraw, "User should have received their assets back");
vm.stopPrank();
}

Recommended Mitigation

Overriding the ERC4626::maxWithdraw and ERC4626::maxRedeem would mitigate the vulnerability. This prevents the user from using the ERC4626::withdraw to bypass the intent of the protocol.

/**
* @dev Override maxWithdraw to enforce competition restrictions
* prevents withdrawals at the limit-check level
*/
+ function maxWithdraw(address owner) public view override returns (uint256) {
// Block withdrawals during event period
+ if (block.timestamp >= eventStartDate && block.timestamp < eventEndDate) {
+ return 0;
+ }
// Block withdrawals if winner not set
+ if (!_setWinner) {
+ return 0;
+ }
// Block withdrawals if user didn't win
+ if (keccak256(abi.encodePacked(userToCountry[owner])) !=
+ keccak256(abi.encodePacked(winner))) {
+ return 0;
+ }
// For winners, calculate their proportional share of finalized vault
+ uint256 ownerShares = balanceOf(owner);
+ if (ownerShares == 0 || totalWinnerShares == 0) {
+ return 0;
+ }
+ uint256 proportionalAssets = Math.mulDiv(ownerShares, finalizedVaultAsset, totalWinnerShares);
+ return proportionalAssets;
+ }
+ function maxRedeem(address owner) public view override returns (uint256) {
// Same restrictions as maxWithdraw
+ if (block.timestamp >= eventStartDate && block.timestamp < eventEndDate) {
+ return 0;
+ }
+ if (!_setWinner) {
+ return 0;
+ }
+ if (keccak256(abi.encodePacked(userToCountry[owner])) !=
+ keccak256(abi.encodePacked(winner))) {
+ return 0;
+ }
+ return balanceOf(owner);
+ }
Updates

Appeal created

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