Platform: Code4rena
Start Date: 23/06/2023
Pot Size: $60,500 USDC
Total HM: 31
Participants: 132
Period: 10 days
Judge: 0xean
Total Solo HM: 10
Id: 254
League: ETH
Rank: 1/132
Findings: 6
Award: $5,144.97
🌟 Selected for report: 1
🚀 Solo Findings: 1
143.4901 USDC - $143.49
There is no control on borrowing flashloan, so anyone can borrow an unwanted flashloan on behalf of a receiver, and the receiver must burn share to pay the fee (in case it has nonzero eUSD balance).
Suppose there is an innocent contract (called FlashloanBorrower.sol
) which is deployed to borrow flashloan from LybraFinance protocol. This contract must have the interface FlashBorrower
implemented to be used during the flashloan callback:
interface FlashBorrower { /// @notice Flash loan callback /// @param amount The amount of tokens received /// @param data Forwarded data from the flash loan request /// @dev Called after receiving the requested flash loan, should return tokens + any fees before the end of the transaction function onFlashLoan(uint256 amount, bytes calldata data) external; }
During executing flashloan, the requested amount is transferred to the receiver: https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/token/PeUSDMainnetStableVision.sol#L131 Then, the callback function is called on the receiver address: https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/token/PeUSDMainnetStableVision.sol#L132 Then, the same lent out fund will be transferred from the receiver to the protocol: https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/token/PeUSDMainnetStableVision.sol#L133 Finally, the fee of the flashloan is burned from the receiver. https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/token/PeUSDMainnetStableVision.sol#L137
The problem is that anyone can call this function to borrow flashloan on behalf of FlashloanBorrower.sol
, and finally the fee will be deducted from FlashloanBorrower.sol
.
Let's say Bob (a malicious user) calls executeFlashloan(FlashBorrower(address(FlashloanBorrower)), 1_000_000e18, "")
. In this case, 1_000_000e18
will be transferred to FlashloanBorrower
contract, then the callback is called, then it is transferred from FlashloanBorrower
to the protocol, and finally the fee is deducted from FlashloanBorrower
(assuming FlashloanBorrower
has enough eUSD to pay the fee). As a result, FlashloanBorrower
loses the fee of the unwanted flashloan.
There are two recommendations as other protocol's flashloan is working:
msg.sender
, not the receiver
. In other words, burning the shares of msg.sender
as fee.EUSD.transferFrom(...)
, it is better to track the balanceBefore
and balanceAfter
, so that balanceAfter - balanceBefore >= getFee(shareAmount)
.Context
#0 - c4-pre-sort
2023-07-09T15:01:09Z
JeffCX marked the issue as duplicate of #280
#1 - c4-judge
2023-07-28T15:30:34Z
0xean marked the issue as satisfactory
#2 - c4-judge
2023-07-28T19:53:20Z
0xean changed the severity to 3 (High Risk)
🌟 Selected for report: HE1M
4814.8482 USDC - $4,814.85
If the project is just started, a malicious user can make the _totalSupply
and _totalShares
imbalance significantly by providing fake income. By doing so, later, when innocent users deposit and mint, the malicious user can steal protocol's stETH without burning any share. Moreover, the protocol's income can be stolen as well.
Suppose nothing is deposited in the protocol (it is day 0).
Bob (a malicious user) deposits $1000 worth of ether (equal to 1 ETH, assuming ETH price is $1000) to mint 200e18 + 1
eUSD. The state will be:
shares[Bob] = 200e18 + 1
_totalShares = 200e18 + 1
_totalSupply = 200e18 + 1
borrowed[Bob] = 200e18 + 1
poolTotalEUSDCirculation = 200e18 + 1
depositAsset[Bob] = 1e18
totalDepositedAsset = 1e18
stETH.balanceOf(protocol) = 1e18
https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/pools/LybraStETHVault.sol#L37Then, Bob transfers directly 0.2stETH
(worth $200) to the protocol. By doing so, Bob is providing a fake excess income in the protocol. So, the state will be:
shares[Bob] = 200e18 + 1
_totalShares = 200e18 + 1
_totalSupply = 200e18 + 1
borrowed[Bob] = 200e18 + 1
poolTotalEUSDCirculation = 200e18 + 1
depositAsset[Bob] = 1e18
totalDepositedAsset = 1e18
stETH.balanceOf(protocol) = 1e18 + 2e17
Then, Bob calls excessIncomeDistribution
to buy this excess income. As you see in line 63, the excessIncome
is equal to the difference of stETH.balanceOf(protocol)
and totalDepositedAsset
. So, the excessAmount = 2e17
.
https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/pools/LybraStETHVault.sol#L63
Then, in line 66, this amount 2e17
is converted to eUSD amount based on the price of stETH. Since, we assumed ETH is $1000, we have:
uint256 payAmount = (((realAmount * getAssetPrice()) / 1e18) * getDutchAuctionDiscountPrice()) / 10000 = 2e17 * 1000e18 / 1e18 = 200e18
Since the protocol is just started, there is no feeStored
, so the income is equal to zero.
https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/pools/LybraStETHVault.sol#L68
In line 75, we have:
uint256 sharesAmount = _EUSDAmount.mul(_totalShares).div(totalMintedEUSD) = 200e18 * (200e18 + 1) / (200e18 + 1) = 200e18
In line 81, this amount of sharesAmount
will be burned from Bob, and then in line 93, 2e17
stETH will be transferred to Bob. So, the state will be:
shares[Bob] = 200e18 + 1 - 200e18 = 1
_totalShares = 200e18 + 1 - 200e18 = 1
_totalSupply = 200e18 + 1
borrowed[Bob] = 200e18 + 1
poolTotalEUSDCirculation = 200e18 + 1
depositAsset[Bob] = 1e18
totalDepositedAsset = 1e18
stETH.balanceOf(protocol) = 1e18 + 2e17 - 2e17 = 1e18
https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/pools/LybraStETHVault.sol#L81
https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/pools/LybraStETHVault.sol#L93Please note that currently we have _totalSupply = 200e18 + 1
and _totalShares = 1
.
Suppose, Alice (an innocent user) deposits 10ETH, and mints 4000e18 eUSD. So, the amount of shares minted to Alice will be:
sharesAmount = _EUSDAmount.mul(_totalShares).div(totalMintedEUSD) = 4000e18 * 1 / (200e18 + 1) = 19
So, the state will be:
shares[Bob] = 1
_totalShares = 1 + 19 = 20
_totalSupply = 200e18 + 1 + 4000e18 = 4200e18 + 1
borrowed[Bob] = 200e18 + 1
poolTotalEUSDCirculation = 200e18 + 1 + 4000e18 = 4200e18 + 1
depositAsset[Bob] = 1e18
totalDepositedAsset = 1e18 + 10e18 = 11e18
stETH.balanceOf(protocol) = 1e18 + 10e18 = 11e18
shares[Alice] = 19
borrowed[Alice] = 4000e18
depositAsset[Alice] = 10e18
Now, different issues can happen leading to loss/steal of funds:
First Scenario: If Alice is a provider, Bob can redeem eUSD multiple of times to receive stETH without burning any share by calling rigidRedemption
. To be more accurate, Bob should call this function with eusdAmount
as parameter equal to _totalSupply / _totalShares
. For example:
first call: rigidRedemption (Alice, 210e18)
, so we will have:
shares[Bob] = 1
_totalShares = 20
_totalSupply = 4200e18 + 1 - 210e18 = 3990e18 + 1
borrowed[Bob] = 200e18 + 1
poolTotalEUSDCirculation = 4200e18 + 1 - 210e18 = 3990e18 + 1
depositAsset[Bob] = 1e18
totalDepositedAsset = 11e18 - 21e16
stETH.balanceOf(protocol) = 11e18 - 21e16
shares[Alice] = 19
borrowed[Alice] = 4000e18 - 210e18 = 3790e18
depositAsset[Alice] = 10e18 - 21e16
Please note that no shares are burned from Bob, because getSharesByMintedEUSD
returns zero as 210e18 * 20 / (4200e18 + 1) = 0
. It means, Bob receives 0.21 stETH by burning no shares.second call: rigidRedemption (Alice, 199e18)
, so we will have:
shares[Bob] = 1
_totalShares = 20
_totalSupply = 3990e18 + 1 - 199e18 = 3791e18 + 1
borrowed[Bob] = 200e18 + 1
poolTotalEUSDCirculation = 3990e18 + 1 - 199e18 = 3791e18 + 1
depositAsset[Bob] = 1e18
totalDepositedAsset = 11e18 - 210e15 - 199e15 = 11e18 - 409e15
stETH.balanceOf(protocol) = 11e18 - 210e15 - 199e15 = 11e18 - 409e15
shares[Alice] = 19
borrowed[Alice] = 3790e18 - 199e18 = 3591e18
depositAsset[Alice] = 10e18 - 210e15 - 199e15 = 10e18 - 409e15
Please note that no shares are burned from Bob, because getSharesByMintedEUSD
returns zero as 199e18 * 20 / (3990e18 + 1) = 0
. It means, Bob receives 0.199 stETH by burning no shares.third call: rigidRedemption (Alice, 189e18)
, so we will have:
shares[Bob] = 1
_totalShares = 20
_totalSupply = 3791e18 + 1 - 189e18 = 3602e18 + 1
borrowed[Bob] = 200e18 + 1
poolTotalEUSDCirculation = 3791e18 + 1 - 189e18 = 3602e18 + 1
depositAsset[Bob] = 1e18
totalDepositedAsset = 11e18 - 409e15 - 189e15 = 11e18 - 598e15
stETH.balanceOf(protocol) = 11e18 - 409e15 - 189e15 = 11e18 - 598e15
shares[Alice] = 19
borrowed[Alice] = 3591e18 - 189e18 = 3402e18
depositAsset[Alice] = 10e18 - 409e15 - 189e15 = 10e18 - 598e15
Please note that no shares are burned from Bob, because getSharesByMintedEUSD
returns zero as 189e18 * 20 / (3791e18 + 1) = 0
. It means, Bob receives 0.189 stETH by burning no shares.So far, by just calling the function rigidRedemption
three times, Bob received 0.21 + 0.199 + 0.189 = 0.598
stETH (worths $598). If, bob continues calling this function, his gain will increase more and more to the point that _totalSupply
and _totalShares
become closer to each other.
A simple calculation shows that if Bob calls this function 60 times (for sure each time the input parameter should be adjusted based on the _totalSupply
and _totalShares
), the state will be:
shares[Bob] = 1
_totalShares = 20
_totalSupply = 203.7e18
borrowed[Bob] = 200e18 + 1
poolTotalEUSDCirculation = 203.7e18
depositAsset[Bob] = 1e18
totalDepositedAsset = 7e18
stETH.balanceOf(protocol) = 7e18
shares[Alice] = 19
borrowed[Alice] = 3.7e18
depositAsset[Alice] = 6e18
It shows that almost the gain of Bob is 4 stETH (worth $4000).The following code simply shows that how this repetitive calling of rigidRedemption
works:
// SPDX-License-Identifier: MIT pragma solidity 0.8.18; contract LybraPoC { mapping(address => uint256) public borrowed; mapping(address => uint256) public shares; address public Alice = address(1); address public Bob = address(2); uint256 public bobGain; uint256 public num; function redeem() public { uint256 toBeRedeemed; uint256 requiredShares; uint256 _totalSupply = 4200e18 + 1; uint256 _totalShares = 20; shares[Bob] = 1; shares[Alice] = 19; borrowed[Bob] = 200e18 + 1; borrowed[Alice] = 4000e18; while (true) { num++; toBeRedeemed = (_totalSupply % _totalShares == 0) ? (_totalSupply / _totalShares) - 1 : (_totalSupply / _totalShares); requiredShares = (toBeRedeemed * _totalShares) / _totalSupply; if (toBeRedeemed > borrowed[Alice]) { break; } borrowed[Alice] -= toBeRedeemed; _totalSupply -= toBeRedeemed; _totalShares -= requiredShares; shares[Bob] -= requiredShares; bobGain += toBeRedeemed; } } }
Please note that, Bob does not have enough share to repay his borrow and release all his collateral. So, assuming safe collateral rate is 160%, Bob at most can withdraw 1 ETH - 1.6 * (200e18 + 1) = $680
. He also gained $4000 by redeeming Alice 60 times, so Bob's balance now is: $680 + $4000 = $4680
which means $3680 is his total gain that is stolen from the protocol. In other words, protocol has minted some shares without enough stETH backed.
Please note that Bob can now start to repay his borrow to reduce borrowed[Bob]
step by step, without burning any share. For example, first repays 10e18 eUSD, second repays 9e18 eUSD. But, for simplicity, I ignored this calculation, and just focused on redeeming Alice to steal big fund. By repaying multiple of times, _totalSupply
and _totalShares
become closer to each other. Then again it is possible to make it imbalance by providing fake income and attack the next users. So, this attack can be applied multiple of times without any restriction.
Please note that Alice is just an example of all the providers in the protocol. If there are other non-provider users also, this scenario is still valid.
Second Scenario: If Alice is liquidated, Bob can liquidate her without burning share again similar to the mechanism described during redeeming.
Third Scenario: Please note that if another innocent user (Eve) is also involved in our example, she will lose fund as well. So, let's say that Eve deposited 20 ETH, and also minted 10000e18 eUSD. So, the state will be:
shares[Bob] = 1
_totalShares = 20 + 47 = 67
_totalSupply = 4200e18 + 1 + 10000e18 = 14200e18 + 1
borrowed[Bob] = 200e18 + 1
poolTotalEUSDCirculation = 4200e18 + 1 + 10000e18 = 14200e18 + 1
depositAsset[Bob] = 1e18
totalDepositedAsset = 11e18 + 20e18 = 31e18
stETH.balanceOf(protocol) = 11e18 + 20e18 = 31e18
shares[Alice] = 19
borrowed[Alice] = 4000e18
depositAsset[Alice] = 10e18
shares[Eve] = 47
borrowed[Eve] = 10000e18
depositAsset[Eve] = 20e18
Now, suppose only Alice is provider, and Eve is not. So, we can redeem Alice by using the same mechanism we describe in the first scenario. Using the same piece of code for repetitive redemption, we have:// SPDX-License-Identifier: MIT pragma solidity 0.8.18; contract LybraPoC { mapping(address => uint256) public borrowed; mapping(address => uint256) public shares; address public Alice = address(1); address public Bob = address(2); address public Eve = address(3); uint256 public bobGain; uint256 public num; uint256 public _totalSupply; uint256 public _totalShares; function redeem() public { uint256 toBeRedeemed; uint256 requiredShares; _totalSupply = 14200e18 + 1; _totalShares = 67; shares[Bob] = 1; shares[Alice] = 19; shares[Eve] = 47; borrowed[Bob] = 200e18 + 1; borrowed[Alice] = 4000e18; borrowed[Eve] = 10000e18; while (true) { num++; toBeRedeemed = (_totalSupply % _totalShares == 0) ? (_totalSupply / _totalShares) - 1 : (_totalSupply / _totalShares); requiredShares = (toBeRedeemed * _totalShares) / _totalSupply; if (toBeRedeemed > borrowed[Alice]) { break; } borrowed[Alice] -= toBeRedeemed; _totalSupply -= toBeRedeemed; _totalShares -= requiredShares; shares[Bob] -= requiredShares; bobGain += toBeRedeemed; } } }
After redeeming Alice 23 times, the state will be:
shares[Bob] = 1
_totalShares = 20 + 47 = 67
_totalSupply = 10200.2e18
borrowed[Bob] = 200e18 + 1
poolTotalEUSDCirculation = 10200.2e18 + 1
depositAsset[Bob] = 1e18
totalDepositedAsset = 27e18
stETH.balanceOf(protocol) = 27e18
shares[Alice] = 19
borrowed[Alice] = 2.1e17
depositAsset[Alice] = 6e18
shares[Eve] = 47
borrowed[Eve] = 10000e18
depositAsset[Eve] = 20e18
Now if Eve wants to repay her whole borrowed amount, she should burn almost 65 shares: 10000e18 * 67 / 10200e18
, but she has only 47 shares. So, she can only repay at most 7155e18 of her borrow. It means that Eve's fund is stolen by Bob. In other words, the collateralized ETH are taken by Bob without burning any shares, so the left shares do not have enough ETH backed.
This scenario shows that Bob made _totalSupply
and _totalShares
imbalance, then two innocent users deposited in the protocol and borrowed some eUSD. Since the difference between these two _totalSupply
and _totalShares
is large, small amount of shares are minted. Then, Bob redeemed some amount through the user who was provider. By doing so, the values of _totalSupply
and _totalShares
become closer to each other. Now if the second user intends to repay her borrow, she should burn more shares that she owns (because the difference of the values _totalSupply
and _totalShares
is now smaller).
Fourth Scenario: Alice can not transfer less than 210e18 eUSD. Because, in the function _trasnfer
, _sharesToTransfer = 209e18 * 20 / (4200e18 + 1) = 0
https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/token/EUSD.sol#L153
https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/token/EUSD.sol#L349
Fifth Scenario: If protocol stETH balance increases by 0.1stETH through LSD after some time. Bob can buy this income without burning any share, in other words Bob steals the income of the protocol. The flow is as follows:
excessIncomeDistribution(1e17)
.
https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/pools/LybraStETHVault.sol#L62C14-L62C38payAmount
will be 100e18
.
https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/pools/LybraStETHVault.sol#L66income >= payAmount
, then payAmount
should be transferred from Bob to the configurator address.
https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/pools/LybraStETHVault.sol#L85_transfer
, 100e18
will be converted to shares: _sharesToTransfer = 100e18 * 20 / (4200e18 + 1) = 0
. So, 0 shares will be deducted from Bob, but 0.1 stETH will be transferred to him.
https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/token/EUSD.sol#L348Please note that for sake of simplicity the fees related to the redemption/liquidation are ignored. So, considering those into our calculation does not make the scenarios invalid.
In Summary:
Bob makes _totalSupply
and _totalShares
imbalance significantly, by just providing fake income in the protocol at day 0. Now that it is imbalanced, he can redeem, or liquidate users without burning any shares. He can also steal protocol's income fund without burning any shares.
First Fix:
During the _repay
, it should return the amount of burned shares.
https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/pools/base/LybraEUSDVaultBase.sol#L279
So that in the functions liquidation
, superLiquidation
, and rigidRedemption
, again the burned shares should be converted to eUSD, and this amount should be used for the rest of calculations.
function rigidRedemption(address provider, uint256 eusdAmount) external virtual { // ... uint256 brnedShares = _repay(msg.sender, provider, eusdAmount); eusdAmount = getMintedEUSDByShares(brnedShares); //... }
Second Fix:
In the excessIncomeDistribution
, the same check
uint256 sharesAmount = EUSD.getSharesByMintedEUSD(payAmount - income); if (sharesAmount == 0) { //EUSD totalSupply is 0: assume that shares correspond to EUSD 1-to-1 sharesAmount = (payAmount - income); }
should be included in the else body as well. https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/pools/LybraStETHVault.sol#L75-L79
Context
#0 - c4-pre-sort
2023-07-11T20:00:09Z
JeffCX marked the issue as primary issue
#1 - c4-sponsor
2023-07-14T09:41:24Z
LybraFinance marked the issue as sponsor acknowledged
#2 - c4-judge
2023-07-26T00:05:24Z
0xean marked the issue as satisfactory
#3 - c4-judge
2023-07-28T20:49:37Z
0xean marked the issue as selected for report
43.047 USDC - $43.05
https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/miner/ProtocolRewardsPool.sol#L227 https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/miner/ProtocolRewardsPool.sol#L190
Miscalculation in token.decimals()
results in wrong calculation of reward, and causing users to receive much lower reward than expected.
There are two issues regarding the token.decimals()
:
First: When the stable tokens are sent by the configurator contract to record the rewards accumulation per esLBR held, based on the type of tokens, rewardPerTokenStored
is set.
https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/miner/ProtocolRewardsPool.sol#L227
If token type is 0 or 2, rewardPerTokenStored = rewardPerTokenStored + (share * 1e18) / totalStaked();
or rewardPerTokenStored = rewardPerTokenStored + (amount * 1e18) / totalStaked();
, respectively.
https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/miner/ProtocolRewardsPool.sol#L233
https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/miner/ProtocolRewardsPool.sol#L238C13-L238C91
But if the token type is 1, rewardPerTokenStored = rewardPerTokenStored + (amount * 1e36 / token.decimals()) / totalStaked();
. It shows that amount * 1e36 / token.decimals()
. But, token.decimals()
will return a value like 18, 6, or etc. It does not include the multiplier 1e18
. So, it means that rewardPerTokenStored
calculated for token type 2 is wrong with factor of 1e18. In other words, it should be: rewardPerTokenStored = rewardPerTokenStored + (amount * 1e36 / (10**token.decimals())) / totalStaked();
or rewardPerTokenStored = rewardPerTokenStored + (amount * 1e18 * 10**(18 - token.decimals)) / totalStaked();
https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/miner/ProtocolRewardsPool.sol#L236C13-L236C110
Second:
During claiming protocol rewards earnings, if the reward
is higher than sum of eUSD and peUSD balance, the remaining reward amount will be covered by the stableToken
.
https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/miner/ProtocolRewardsPool.sol#L204-L212
During calculating the tokenAmount
, it is wrongly multiplied by token.decimals()
instead of 10**token.decimals()
.
https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/miner/ProtocolRewardsPool.sol#L209
So, it means that the amount of tokenAmount
to be transferred will be much lower than expected. Let's say reward = 100e18
, eUSDShare = 20e18
, and peUSDBalance = 50e18
and token.decimals() = 6
, so it is expected that tokenAmount = 30e6
be transferred to msg.sender
. But, due to this bug, it will transfer only: 30e18 * 6 / 1e18 = 180
. So, msg.sender
will receive almost 30e6
of stableToken
lower than expected as reward.
In both bugs, it is recommended to replace token.decimals()
with 10**token.decimals()
.
Decimal
#0 - c4-pre-sort
2023-07-09T14:27:20Z
JeffCX marked the issue as primary issue
#1 - c4-sponsor
2023-07-18T08:52:37Z
LybraFinance marked the issue as sponsor confirmed
#2 - c4-judge
2023-07-26T13:15:32Z
0xean marked the issue as satisfactory
#3 - c4-judge
2023-07-28T20:39:17Z
0xean marked issue #828 as primary and marked this issue as a duplicate of 828
84.3563 USDC - $84.36
Miscalculation in claiming the protocol rewards earnings causes the users not be able to receive their reward properly.
During claiming the protocol rewards earnings, there is a miscalculation in
uint256 eUSDShare = balance >= reward ? reward : reward - balance;
Let's say reward = rewards[msg.sender] = 100e18
and balance = EUSD.sharesOf(address(this)) = 20e18
. So, eUSDShare = balance >= reward ? reward : reward - balance
will be equal to 80e18
. So, it is supposed that 80e18
be transferred to the msg.sender
, while this amount is not available in ProtocolRewardsPool
.
https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/miner/ProtocolRewardsPool.sol#L197
Let's say reward = rewards[msg.sender] = 100e18
and balance = EUSD.sharesOf(address(this)) = 80e18
. So, eUSDShare = balance >= reward ? reward : reward - balance
will be equal to 20e18
. So, 20e18 eUSD
will be transferred to the msg.sender
, while based on the protocol comment: eUSD will be prioritized for distribution. Then, the remaining 80e18
will be transferred by peUSD or the stable token.
https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/miner/ProtocolRewardsPool.sol#L196
The following line should be modified.
uint256 eUSDShare = balance >= reward ? reward : reward - balance;
Recommended modification:
uint256 eUSDShare = balance >= reward ? reward : balance;
Math
#0 - c4-pre-sort
2023-07-10T13:50:33Z
JeffCX marked the issue as duplicate of #161
#1 - c4-judge
2023-07-28T15:44:21Z
0xean marked the issue as satisfactory
🌟 Selected for report: bytes032
Also found by: 0xMAKEOUTHILL, 0xgrbr, 0xkazim, 0xnacho, Arz, Co0nan, CrypticShepherd, Cryptor, HE1M, Iurii3, LaScaloneta, LokiThe5th, LuchoLeonel1, MrPotatoMagic, Musaka, Qeew, RedTiger, SovaSlava, Toshii, Vagner, a3yip6, azhar, bart1e, devival, hl_, jnrlouis, kutugu, peanuts, pep7siup, qpzm, smaul
1.3247 USDC - $1.32
Unmatched function for getting the exchange rate can lead to being unable to mint PeUSD when depositing ETH into Rocket Pool.
The interface used in LybraRETHVault.sol
for getting the exchange rate does not match the target contract RETH.
In line 10, the interface is getExchangeRatio
:
https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/pools/LybraRETHVault.sol#L10
while the contract RocketTokenRETH.sol
implements getExchangeRate
.
https://github.com/rocket-pool/rocketpool/blob/6a9dbfd85772900bb192aabeb0c9b8d9f6e019d1/contracts/contract/token/RocketTokenRETH.sol#L66
which is deployed in the following address:
https://etherscan.io/address/0xae78736Cd615f374D3085123A210448E74Fc6393#readContract#F6
This can cause the protocol not to be able to mint PeUSD when depositing ETH into Rocket Pool, because it will revert. The flow is: LybraRETHVault::depositEtherToMint >> LybraRETHVault::getAssetPrice >> IRETH(address(collateralAsset)).getExchangeRatio() >> Revert!!!
https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/pools/LybraRETHVault.sol#L27 https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/pools/LybraRETHVault.sol#L35 https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/pools/LybraRETHVault.sol#L47C33-L47C83
The same issue is also available in LybraWbETHVault.sol
.
https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/pools/LybraWbETHVault.sol#L10
https://etherscan.io/token/0xa2E3356610840701BDf5611a53974510Ae27E2e1#readProxyContract#F13
Match the interfaces together.
Context
#0 - c4-pre-sort
2023-07-04T02:31:16Z
JeffCX marked the issue as duplicate of #129
#1 - c4-pre-sort
2023-07-04T02:33:56Z
JeffCX marked the issue as high quality report
#2 - c4-pre-sort
2023-07-04T13:29:35Z
JeffCX marked the issue as duplicate of #27
#3 - c4-judge
2023-07-28T17:15:05Z
0xean marked the issue as satisfactory
🌟 Selected for report: 0xnev
Also found by: 0xRobocop, 0xbrett8571, 0xkazim, 0xnacho, 3agle, 8olidity, ABAIKUNANBAEV, Bauchibred, Co0nan, CrypticShepherd, D_Auditor, DelerRH, HE1M, Iurii3, Kaysoft, MrPotatoMagic, RedOneN, RedTiger, Rolezn, SanketKogekar, Sathish9098, Timenov, Toshii, Vagner, bart1e, bytes032, codetilda, devival, halden, hals, kutugu, m_Rassska, naman1778, nonseodion, seth_lawson, solsaver, squeaky_cactus, totomanov, y51r, yudan, zaevlad
57.9031 USDC - $57.90
The comment states that:
amount Must be higher than 0. Individual mint amount shouldn't surpass 10% when the circulation reaches 10_000_000
https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/pools/base/LybraEUSDVaultBase.sol#L124C10-L124C126
But it is not implemented same as what the comment suggests.
This amount is limited to mintVaultMaxSupply
set in the LybraConfigurator
:
https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/pools/base/LybraEUSDVaultBase.sol#L260
https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/configuration/LybraConfigurator.sol#L119
Whether remove this comment or implement such functionality in mint(...)
:
if ( (borrowed[msg.sender] * 100) / poolTotalEUSDCirculation > 10 && poolTotalEUSDCirculation > 10_000_000 * 1e18 ) revert("Mint Amount cannot be more than 10% of total circulation");
Wrong error message in require-statement. EUSD
should be changed to PeUSD
.
https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/pools/base/LybraPeUSDVaultBase.sol#L131
#0 - c4-pre-sort
2023-07-27T19:34:50Z
JeffCX marked the issue as high quality report
#1 - c4-judge
2023-07-27T23:58:20Z
0xean marked the issue as grade-b
#2 - c4-judge
2023-07-28T20:47:47Z
0xean marked the issue as grade-a
#3 - 0xean
2023-07-28T20:48:07Z
upgrading to A score based on multiple QA reports combined for this warden
#4 - c4-sponsor
2023-07-29T11:22:19Z
LybraFinance marked the issue as sponsor confirmed