BriVault

First Flight #52
Beginner FriendlySolidity
100 EXP
View results
Submission Details
Impact: low
Likelihood: low
Invalid

Reentrancy Risk in deposit function

Root + Impact

The deposit function violates the Checks-Effects-Interactions (CEI) security pattern. While this specific instance is not exploitable due to the code's structure, it represents a deviation from best practices and could lead to vulnerabilities during future code maintenance.

Description

  • Normal Behavior: A user calls deposit, the contract calculates shares, pulls the user's tokens, and mints the shares to the user.

  • The Problem: The deposit function executes two external safeTransferFrom calls (Interactions) before it updates the vault's internal state by calling _mint (Effect). This is a clear violation of the CEI pattern and at first glance appears to open a reentrancy attack vector.

// Root cause in src/briVault.sol
// The participantShares is calculated first
uint256 participantShares = _convertToShares(stakeAsset);
// Then, Interactions happen
@> IERC20(asset()).safeTransferFrom(msg.sender, participationFeeAddress, fee);
@> IERC20(asset()).safeTransferFrom(msg.sender, address(this), stakeAsset);
// Finally, the Effect happens last
@> _mint(msg.sender, participantShares);

Risk

Likelihood: Low

An exploit is not possible.

  • Reason 1: The critical value, participantShares, is calculated and stored in a local variable before the external calls.

  • Reason 2: A re-entrant call while possible, cannot modify the original function's local variables. The original call will resume and mint the correct, pre-calculated number of shares, preventing state corruption.

Impact: Low

  • Impact 1: A successful reentrancy attack could manipulate the share calculation, allowing the attacker to mint an excessive number of shares

  • Impact 2: Breaking of the vault's accounting and could lead to a direct loss of funds for other users.

Proof of Concept

The following demonstrates that re-entrancy is possible but has no negative effect. It uses a single test contract that also acts as the malicious ERC20 token. The test triggers a re-entrant deposit call but asserts that the final state totalSupply and balanceOf(vault) is correct, proving the state was not corrupted.

A test contract is set up that also inherits from MockERC20, allowing the test itself to act as the malicious asset token.
The transferFrom function is overridden within this test contract to serve as the re-entrancy hook.
This hook is programmed to detect when the briVault contract calls it to pull the stakeAsset.
When triggered, the hook immediately calls briVault.deposit() a second time before the first call has finished.
After the re-entrant call and the original call complete, the test asserts that the vault's final totalSupply and balanceOf are both correct (e.g., 19.7 ether of assets and 19.7 ether of shares).
This successful assertion proves that while re-entrancy is possible, the contract's state is not corrupted, and the vulnerability is possible.

Recommended Mitigation

The function should be refactored to strictly follow the Checks-Effects-Interactions pattern. This makes the code safer, easier to read, and aligns with security best practices, even though no exploit exists here.

// src/briVault.sol
function deposit(uint256 assets, address receiver) public override returns (uint256) {
// ... (Checks and calculations)
uint256 fee = _getParticipationFee(assets);
if (minimumAmount + fee > assets) {
revert lowFeeAndAmount();
}
uint256 stakeAsset = assets - fee;
stakedAsset[receiver] = stakeAsset;
uint256 participantShares = _convertToShares(stakeAsset);
- IERC20(asset()).safeTransferFrom(msg.sender, participationFeeAddress, fee);
- IERC20(asset()).safeTransferFrom(msg.sender, address(this), stakeAsset);
- _mint(msg.sender, participantShares);
+ // 1. EFFECT (Mint shares first)
+ _mint(msg.sender, participantShares);
+
+ // 2. INTERACTIONS (Transfer tokens last)
+ IERC20(asset()).safeTransferFrom(msg.sender, participationFeeAddress, fee);
+ IERC20(asset()).safeTransferFrom(msg.sender, address(this), stakeAsset);
emit deposited (receiver, stakeAsset);
return participantShares;
}
Updates

Appeal created

bube Lead Judge 20 days ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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

Give us feedback!