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: 7/132
Findings: 6
Award: $1,674.57
π Selected for report: 1
π Solo Findings: 1
80.4648 USDC - $80.46
Detailed description of the impact of this finding.
Even though, the votes are calculated correctly, the proposals
view function returns wrong voting results returning forVotes
results as againstVotes
amount.
This would negatively impact the users understanding of the proposals voting results and lead to confusions.
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.
The Governance COUNTING_MODE()
is set Bravo as follows:
function COUNTING_MODE() public override view virtual returns (string memory){ return "support=bravo&quorum=for,abstain"; }
This refers to the vote options 0 = Against, 1 = For, 2 = Abstain
, as in GovernorBravo.
Openzepping#Governor,
The total amount of votes are stored in proposalData[proposalId].supportVotes[X]
, where X, as abovementioned, can be 0 = Against, 1 = For, 2 = Abstain
:
struct ProposalExtraData { mapping(address => Receipt) receipts; mapping(uint8 => uint256) supportVotes; uint256 totalVotes; } mapping (uint256 => ProposalExtraData) public proposalData;
Finally, this is the proposals
function which returns forVotes
as a total amount of votes against, which corresponds to proposalData[proposalId].supportVotes[0]
:
function proposals(uint256 proposalId) external view returns (uint256 id, address proposer, uint256 eta, uint256 startBlock, uint256 endBlock, uint256 forVotes, uint256 againstVotes, uint256 abstainVotes, bool canceled, bool executed) { id = proposalId; //... forVotes = proposalData[proposalId].supportVotes[0]; againstVotes = proposalData[proposalId].supportVotes[1]; abstainVotes = proposalData[proposalId].supportVotes[2]; //... }
Manual Review
In LybraGovernance.sol#L120-L122 replace
forVotes = proposalData[proposalId].supportVotes[0]; againstVotes = proposalData[proposalId].supportVotes[1];
with
againstVotes = proposalData[proposalId].supportVotes[0]; forVotes = proposalData[proposalId].supportVotes[1];
Other
#0 - c4-pre-sort
2023-07-04T13:39:25Z
JeffCX marked the issue as duplicate of #15
#1 - c4-judge
2023-07-28T15:33:01Z
0xean marked the issue as satisfactory
π Selected for report: devival
1444.4545 USDC - $1,444.45
The proposal can be created with only 100_000 esLBR delegated instead of 10_000_000.
According to LybraV2Docs, a proposal can only be created if the sender has at least 10 million esLBR tokens delegated to his address to meet the proposal threshold.
In LybraGovernance.sol#L172-L174 the proposal threshold is set to only 1e23
which equals to 100_000
as esLBR has 18 decimals.
function proposalThreshold() public pure override returns (uint256) { return 1e23; }
Manual Review
In LybraGovernance.sol#L173 replace 1e23
with 1e25
Alternatively, the team can update the documentation stating that it is only required 100_000 esLBR tokens (0.1% of the total LBR supply) delegated to meet the proposal threshold.
Math
#0 - c4-pre-sort
2023-07-08T13:34:16Z
JeffCX marked the issue as primary issue
#1 - c4-sponsor
2023-07-18T03:41:42Z
LybraFinance marked the issue as sponsor confirmed
#2 - c4-judge
2023-07-26T12:39:26Z
0xean marked the issue as satisfactory
#3 - c4-judge
2023-07-28T19:54:36Z
0xean marked the issue as selected for report
π Selected for report: Musaka
Also found by: 0xhacksmithh, 0xnev, CrypticShepherd, LuchoLeonel1, T1MOH, bytes032, cccz, devival, josephdara, ktg, squeaky_cactus
29.0567 USDC - $29.06
LybraGovernance
contract only allows to vote during the first 3 blocks after the snapshot is taken. Assuming it takes roughly 12 seconds per 1 block on Ethereum, it is only 36 seconds to decide and vote for a proposal.
This would be too fast for a regular human being to evaluate the proposal and vote.
In LybraGovernance.sol
, the clock()
implements block number as a unit of time:
function clock() public override view returns (uint48){ return SafeCast.toUint48(block.number); }
LybraGovernance.sol#L161-L163 Openzeppelin#votingPeriod
According to the Lybra V2 Docs: "When a proposal is created, the community can cast their votes during a 7 day voting period. "
However, instead of 7 days, the current voting period has hardcoded 3 blocks:
function votingPeriod() public pure override returns (uint256){ return 3; }
LybraGovernance.sol#L147-L149 not allowing users enough time to evaluate the proposal and vote making the whole Governor contract useless.
Manual Review
Set the votingPeriod()
to return 50400
blocks (604800 seconds in a day divided by 12 seconds (Approx. Ethereum Average Block Time)
LybraGovernance.sol#L147-L149
Alternatively, change the unit of time to block.timestamp
and set votingPeriod to 7 days
, if it is intended to be used in different blockchain, due to the different time it takes for each block to be mined.
Timing
#0 - c4-pre-sort
2023-07-04T14:09:55Z
JeffCX marked the issue as duplicate of #268
#1 - c4-judge
2023-07-28T15:43:50Z
0xean marked the issue as satisfactory
#2 - c4-judge
2023-07-28T19:46:32Z
0xean changed the severity to 2 (Med Risk)
π Selected for report: Musaka
Also found by: 0xhacksmithh, 0xnev, CrypticShepherd, LuchoLeonel1, T1MOH, bytes032, cccz, devival, josephdara, ktg, squeaky_cactus
29.0567 USDC - $29.06
Detailed description of the impact of this finding.
With a voting delay set to 1 block, users would not have enough time to buy tokens, or delegate their votes.
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.
In LybraGovernance.sol
, the clock()
implements block number as a unit of time:
function clock() public override view returns (uint48){ return SafeCast.toUint48(block.number); }
This means, if votingDelay
function returns 1
, the delay since the proposal is submitted until voting power is fixed and voting starts equals 1 block. Openzeppelin#votingDelay
Manual Review
Set votingDelay
to at least 7200 blocks (~1 day) Ethereum Average Block Time)
Timing
#0 - c4-pre-sort
2023-07-04T14:10:32Z
JeffCX marked the issue as duplicate of #268
#1 - c4-judge
2023-07-28T15:43:49Z
0xean marked the issue as satisfactory
109.3508 USDC - $109.35
https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/configuration/LybraConfigurator.sol#L339 https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/pools/base/LybraPeUSDVaultBase.sol#L128
The liquidation
function in all contracts inheriting LybraPeUSDVaultBase.sol
would revert until both vaultSafeCollateralRatio[pool]
and vaultBadCollateralRatio[pool]
are set. Even though, according to the documentation, the default safe and bad collateral ratios should be 160 and 150 respectively.
Contracts affected by LybraPeUSDVaultBase
abstract: LybraWbETHVault.sol
, LybraWstETHVault.sol
, LybraRETHVault.sol
.
In getBadCollateralRatio
function in LybraConfigurator.sol
, if vaultBadCollateralRatio[pool]
equals 0
, the function would return vaultSafeCollateralRatio[pool] - 1e19
.
function getBadCollateralRatio(address pool) external view returns(uint256) { if(vaultBadCollateralRatio[pool] == 0) return vaultSafeCollateralRatio[pool] - 1e19; return vaultBadCollateralRatio[pool]; }
However, if vaultSafeCollateralRatio[pool]
is not manually set through setSafeCollateralRatio
function, it will equal 0
, which will make vaultSafeCollateralRatio[pool] - 1e19
cause a revert due to the possible underflow.
As a consequence, liquidation
function in all contracts inheriting LybraPeUSDVaultBase.sol
would revert after calling getBadCollateralRatio
.
Manual Review
Replace vaultSafeCollateralRatio[pool] - 1e19
with 150 * 1e18
in getBadCollateralRatio
function of LybraConfigurator.sol
.
LybraConfigurator.sol#L339
Under/Overflow
#0 - c4-pre-sort
2023-07-04T15:25:14Z
JeffCX marked the issue as low quality report
#1 - JeffCX
2023-07-04T15:25:33Z
If both vaultSafeCollateralRatio[pool] and vaultBadCollateralRatio[pool] in LybraConfigurator.sol are not set, getBadCollateralRatio(pool) would revert
this is expected, admin need to set the pool address
#2 - c4-pre-sort
2023-07-04T15:25:59Z
JeffCX marked the issue as high quality report
#3 - c4-pre-sort
2023-07-04T15:26:04Z
JeffCX marked the issue as primary issue
#4 - c4-sponsor
2023-07-18T06:25:26Z
LybraFinance marked the issue as sponsor confirmed
#5 - c4-judge
2023-07-26T12:40:06Z
0xean marked the issue as satisfactory
#6 - c4-judge
2023-07-28T19:56:04Z
0xean marked issue #44 as primary and marked this issue as a duplicate of 44
π 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
Detailed description of the impact of this finding.
Executing getAssetPrice()
will always fail which makes all functions calling it fail as well. This includes depositEtherToMint
which will not allow to deposit any funds to the contract.
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.
In depositEtherToMint
of LybraWBETHVault.sol
and LybraRETHVault.sol
there is the line of code calling getAssetPrice()
:
_mintPeUSD(msg.sender, msg.sender, mintAmount, getAssetPrice());
LybraWbETHVault.sol#L28
LybraRETHVault.sol#L35
getAssetPrice()
function calls nonexistent function exchangeRatio()
in WBETH contract to get the exchange rate of WBETH. This will make the call to always fail.
function getAssetPrice() public override returns (uint256) { return (_etherPrice() * IWBETH(address(collateralAsset)).exchangeRatio()) / 1e18; }
Manual Review
Replace exchangeRatio()
with exchangeRate()
in the following instances:
IWBETH
interface LybraWbETHVault.sol#L10
getAssetPrice()
function LybraWbETHVault.sol#L35
IRETH
interface LybraRETHVault.sol#L10
getAssetPrice()
function in LybraRETHVault.sol#L47
Error
#0 - c4-pre-sort
2023-07-04T02:31:49Z
JeffCX marked the issue as duplicate of #129
#1 - c4-pre-sort
2023-07-04T02:31:52Z
JeffCX marked the issue as low quality report
#2 - c4-pre-sort
2023-07-04T02:33:28Z
JeffCX marked the issue as high quality report
#3 - c4-pre-sort
2023-07-04T13:29:34Z
JeffCX marked the issue as duplicate of #27
#4 - c4-judge
2023-07-28T17:14:13Z
0xean changed the severity to 2 (Med Risk)
#5 - c4-judge
2023-07-28T17:15:07Z
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
9.931 USDC - $9.93
According to the developerβs comment in LybraWbETHVault.sol
, the WBETH contract address is intended to be 0xae78736Cd615f374D3085123A210448E74Fc6393
, which is rETH Token address.
In case rETH Token address is passed in the LybraWbETHVault.sol
constructor the contract will not be functional.
IWBETH interface has a deposit function
function deposit(address referral) external payable;
While there is no deposit function in rETH.
Manual Review
Use the correct WBETH Token address 0xa2E3356610840701BDf5611a53974510Ae27E2e1
Other
#0 - c4-pre-sort
2023-07-08T14:19:57Z
JeffCX marked the issue as duplicate of #79
#1 - c4-judge
2023-07-25T19:36:27Z
0xean changed the severity to QA (Quality Assurance)