BriVault

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

Incomplete ERC-4626 interface restrictions - bad UX

Root + Impact

Description

The contract implements a tournament-style ERC-4626 vault, where deposits are only allowed before the event starts and withdrawals only after the winner is finalized.
However, not all ERC-4626 interface functions enforce these timing restrictions:

  • mint(uint256, address) (inherited) is still callable and can mint shares but you cannot join event with these shares.

  • maxWithdraw(address) and maxRedeem(address) do not return 0 before the event ends.

  • maxDeposit(address) and maxMint(address) do not return 0 after the event starts.

While no direct loss of funds is possible, these inconsistencies can confuse wallets, explorers, and DeFi protocols that rely on ERC-4626 introspection (e.g., for displaying available deposit/withdraw limits), leading to a poor or misleading user experience.

Risk

Likelihood: Medium

  • Integration with wallets, explorers, and DeFi protocols that rely on ERC-4626

Impact: Low

  • No direct loss of funds is possible

  • Inconsistent UI behavior or misleading results

Recommended Mitigation

To align behavior with the intended tournament rules and ERC-4626 best practices, implement the following overrides:

/**
* @notice Override ERC4626 mint to prevent bypassing deposit logic
* @dev Users must use custom deposit() function
*/
function mint(uint256 shares, address receiver) public pure override returns (uint256) {
revert("Use deposit(uint256 assets, address receiver) instead");
}
/**
* @notice Override maxWithdraw to return 0 until event ends
* @dev Prevents wallet UIs from showing withdraw button
*/
function maxWithdraw(address owner) public view override returns (uint256) {
// Can't withdraw until winner is set and event ended
if (!_setWinner || block.timestamp < eventEndDate) {
return 0;
}
// Check if user won
if (
keccak256(abi.encodePacked(userToCountry[owner])) !=
keccak256(abi.encodePacked(winner))
) {
return 0;
}
// Calculate winner's share
uint256 shares = balanceOf(owner);
if (shares == 0 || totalWinnerShares == 0) {
return 0;
}
uint256 vaultAsset = finalizedVaultAsset;
return Math.mulDiv(shares, vaultAsset, totalWinnerShares);
}
/**
* @notice Override maxRedeem to return 0 until event ends
* @dev Prevents wallet UIs from showing redeem button
*/
function maxRedeem(address owner) public view override returns (uint256) {
// Can't redeem until winner is set and event ended
if (!_setWinner || block.timestamp < eventEndDate) {
return 0;
}
// Check if user won
if (
keccak256(abi.encodePacked(userToCountry[owner])) !=
keccak256(abi.encodePacked(winner))
) {
return 0;
}
// Return user's shares if they won
return balanceOf(owner);
}
/**
* @notice Override maxDeposit to prevent deposits after event starts
* @dev Prevents bypassing deposit time restrictions
*/
function maxDeposit(address) public view override returns (uint256) {
if (block.timestamp >= eventStartDate) {
return 0;
}
// Otherwise allow unlimited deposits
return type(uint256).max;
}
/**
* @notice Override maxMint to prevent minting after event starts
* @dev Prevents bypassing deposit time restrictions
*/
function maxMint(address) public view override returns (uint256) {
if (block.timestamp >= eventStartDate) {
return 0;
}
return type(uint256).max;
}
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!