The VotiumStrategy contract is susceptible to the Inflation Attack, in which the first depositor can be front-runned by an attacker to steal their deposit.
Both AfEth and VotiumStrategy acts as vaults: accounts deposit some tokens and get back another token (share) that represents their participation in the vault.
These types of contracts are potentially vulnerable to the inflation attack: an attacker can front-run the initial deposit to the vault to inflate the value of a share and render the front-runned deposit worthless.
In AfEth, this is successfully mitigated by the slippage control. Any attack that inflates the value of a share to decrease the number of minted shares is rejected due to the validation of minimum output:
https://github.com/code-423n4/2023-09-asymmetry/blob/main/contracts/AfEth.sol#L166-L167
166: uint256 amountToMint = totalValue / priceBeforeDeposit;
167: if (amountToMint < _minout) revert BelowMinOut();
However, this is not the case of VotiumStrategy. In this contract, no validation is done in the number of minted tokens. This means that an attacker can execute the attack by front-running the initial deposit, which may be from AfEth or from any other account that interacts with the contract. See Proof of Concept for a detailed walkthrough of the issue.
39: function deposit() public payable override returns (uint256 mintAmount) {
40: uint256 priceBefore = cvxPerVotium();
41: uint256 cvxAmount = buyCvx(msg.value);
42: IERC20(CVX_ADDRESS).approve(VLCVX_ADDRESS, cvxAmount);
43: ILockedCvx(VLCVX_ADDRESS).lock(address(this), cvxAmount, 0);
44: mintAmount = ((cvxAmount * 1e18) / priceBefore);
45: _mint(msg.sender, mintAmount);
46: }
Let's say a user wants to deposit in VotiumStrategy and calls deposit()
sending an ETH amount such as it is expected to buy X
tokens of CVX. Attacker will front-run the transaction and execute the following:
- Initial state is empty contract,
assets = 0
andsupply = 0
. - Attacker calls deposit with an amount of ETH such as to buy
1e18
CVX tokens, this makesassets = 1e18
andsupply = 1e18
. - Attacker calls
requestWithdraw(1e18 - 1)
so thatsupply = 1
, assume also1e18 - 1
CVX tokens are withdrawn so thatcvxUnlockObligations = 1e18 - 1
. - Attacker transfers (donates) X amount of CVX to VotiumStrategy contract.
- At this point,
priceBefore = cvxPerVotium() = (totalCvx - cvxUnlockObligations) * 1e18 / supply = (X + 1e18 - (1e18 - 1)) * 1e18 / 1 = (X + 1) * 1e18
- User transaction gets through and
deposit()
buys X amount of CVX. Minted tokens will bemintAmount = X * 1e18 / priceBefore = X * 1e18 / (X + 1) * 1e18 = X / (X + 1) = 0
. - User is then minted zero VotiumStrategy tokens.
- Attacker calls
requestWithdraw()
again to queue withdrawal to remove all CVX balance from the contract, including the tokens deposited by the user.
There are multiple ways of solving the issue:
- Similar to AfEth, add a minimum output check to ensure the amount of minted shares.
- Track asset balances internally so an attacker cannot donate assets to inflate shares.
- Mint an initial number of "dead shares", similar to how UniswapV2 does.
A very good discussion of these can be found here.