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: 57/132
Findings: 4
Award: $137.45
π Selected for report: 1
π Solo Findings: 0
π Selected for report: alexweb3
Also found by: D_Auditor, DedOhWale, DelerRH, LuchoLeonel1, Musaka, Neon2835, Silvermist, Timenov, TorpedoPistolIXC41, adeolu, cartlex_, hals, josephdara, koo, lanrebayode77, mahyar, mladenov, mrudenko, pep7siup, zaevlad, zaggle
18.4208 USDC - $18.42
https://github.com/code-423n4/2023-06-lybra/blob/26915a826c90eeb829863ec3851c3c785800594b/contracts/lybra/configuration/LybraConfigurator.sol#L85-L88 https://github.com/code-423n4/2023-06-lybra/blob/26915a826c90eeb829863ec3851c3c785800594b/contracts/lybra/configuration/LybraConfigurator.sol#L90-L92
Configurator
contract has external functions with onlyRole
and checkRole
modifiers, in both cases the modifier implementation is flawed as there isnβt any check for a require or revert, the comparison will silently return false and let the execution continue:
Any account without any role can change protocol parameters in the Configurator
contract.
contract LybraConfiguratorTest is Test { address deployer; address attacker; address[] proposers; address[] executors; GovernanceTimelock governance; mockCurve curvePool; Configurator configurator; function setUp() public { deployer = makeAddr("deployer"); attacker = makeAddr("attacker"); vm.startPrank(deployer); proposers.push(deployer); executors.push(deployer); governance = new GovernanceTimelock(2, proposers, executors, deployer); curvePool = new mockCurve(); configurator = new Configurator(address(governance), address(0)); vm.stopPrank(); } function test_revertIfUserHasNotAnyRole() public { vm.startPrank(attacker); vm.expectRevert(); // Include all Configurator contract external calls with only onlyRole or checkRole modifier configurator.setPremiumTradingEnabled(true); vm.stopPrank(); } }
Modifiers should require that the caller has a role or not:
modifier onlyRole(bytes32 role) { require(GovernanceTimelock.checkOnlyRole(role, msg.sender), "Forbidden"); _; }
modifier checkRole(bytes32 role) { require(GovernanceTimelock.checkRole(role, msg.sender), "Forbidden"); _; }
Access Control
#0 - c4-pre-sort
2023-07-08T23:34:16Z
JeffCX marked the issue as duplicate of #704
#1 - c4-judge
2023-07-28T17:18:58Z
0xean marked the issue as satisfactory
π Selected for report: georgypetrov
Also found by: CrypticShepherd, DelerRH, Kenshin, LuchoLeonel1, SpicyMeatball, bart1e, ktg, pep7siup
53.1445 USDC - $53.14
https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/configuration/LybraConfigurator.sol#L28-L30 https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/configuration/LybraConfigurator.sol#L198-L206
Vault doesn't implement the vaultType()
external function, however, the setSafeCollateralRatio
function in the Configurator
contract checks the private state of the pool by vaultType()
instead of getVaultType()
and reverts.
Timelock can't change safe collateral ratios.
// SPDX-License-Identifier: MIT pragma solidity ^0.8.17; import {Test, console} from "forge-std/Test.sol"; import {GovernanceTimelock} from "contracts/lybra/governance/GovernanceTimelock.sol"; import {mockCurve} from "contracts/mocks/mockCurve.sol"; import {Configurator} from "contracts/lybra/configuration/LybraConfigurator.sol"; import {LybraWBETHVault} from "contracts/lybra/pools/LybraWbETHVault.sol"; import {PeUSDMainnet} from "contracts/lybra/token/PeUSDMainnetStableVision.sol"; contract LybraConfiguratorTest is Test { address goerliEndPoint = 0xbfD2135BFfbb0B5378b56643c2Df8a87552Bfa23; address wbETH = 0xbfD2135BFfbb0B5378b56643c2Df8a87552Bfa23; address deployer; address attacker; address[] proposers; address[] executors; GovernanceTimelock governance; mockCurve curvePool; Configurator configurator; PeUSDMainnet peUsdMainnet; LybraWBETHVault wbETHVault; function setUp() public { deployer = makeAddr("deployer"); attacker = makeAddr("attacker"); vm.startPrank(deployer); proposers.push(deployer); executors.push(deployer); governance = new GovernanceTimelock(2, proposers, executors, deployer); curvePool = new mockCurve(); configurator = new Configurator(address(governance), address(0)); peUsdMainnet = new PeUSDMainnet( address(configurator), 8, goerliEndPoint ); // Ether oracle has no impact on this test wbETHVault = new LybraWBETHVault(address(peUsdMainnet), makeAddr("NonImportantMockForEtherOracle"), wbETH, address(configurator)); vm.stopPrank(); } function test_canChangeSafeCollateralRatio() public { vm.startPrank(deployer); // Set safe collateral ratio - 160% configurator.setSafeCollateralRatio(address(wbETHVault), 160 * 1e18); // ... Reverted assertEq(configurator.getSafeCollateralRatio(address(wbETHVault)), 160 * 1e18); vm.stopPrank(); } }
1- Implements IVault correctly:
interface IVault { function getVaultType() external pure returns (uint8); }
2- Use getVaultType()
instead of vaultType()
function setSafeCollateralRatio(address pool, uint256 newRatio) external checkRole(TIMELOCK) { if (IVault(pool).getVaultType() == 0) { // EUSD vault base require(newRatio >= 160 * 1e18, "eUSD vault safe collateralRatio should more than 160%"); } else { // peUSD vault base require( newRatio >= vaultBadCollateralRatio[pool] + 1e19, "PeUSD vault safe collateralRatio should more than bad collateralRatio" ); } vaultSafeCollateralRatio[pool] = newRatio; emit SafeCollateralRatioChanged(pool, newRatio); }
DoS
#0 - c4-pre-sort
2023-07-10T14:00:37Z
JeffCX marked the issue as duplicate of #494
#1 - c4-pre-sort
2023-07-11T21:29:22Z
JeffCX marked the issue as not a duplicate
#2 - c4-pre-sort
2023-07-11T21:29:40Z
JeffCX marked the issue as duplicate of #882
#3 - c4-judge
2023-07-28T15:36:26Z
0xean marked the issue as satisfactory
#4 - c4-judge
2023-07-28T19:43:25Z
0xean changed the severity to 2 (Med Risk)
55.9611 USDC - $55.96
The notifyRewardAmount
external function calculates decimals rewardPerTokenStored
incorrectly when the reward token type equals 1 and the rewardPerTokenStored
value increase unreal.
This bug can break the protocol rewards distribution.
// SPDX-License-Identifier: MIT pragma solidity ^0.8.17; import {Test, console} from "forge-std/Test.sol"; import {GovernanceTimelock} from "contracts/lybra/governance/GovernanceTimelock.sol"; import {mockCurve} from "contracts/mocks/mockCurve.sol"; import {Configurator} from "contracts/lybra/configuration/LybraConfigurator.sol"; import {LybraWBETHVault} from "contracts/lybra/pools/LybraWbETHVault.sol"; import {PeUSDMainnet} from "contracts/lybra/token/PeUSDMainnetStableVision.sol"; import {ProtocolRewardsPool} from "contracts/lybra/miner/ProtocolRewardsPool.sol"; import {EUSDMock} from "contracts/mocks/MockEUSD.sol"; import {LBR} from "contracts/lybra/token/LBR.sol"; import {esLBR} from "contracts/lybra/token/esLBR.sol"; import {esLBRBoost} from "contracts/lybra/miner/esLBRBoost.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; // 6 decimal USDC mock contract mockUSDC is ERC20 { constructor() ERC20("USDC", "USDC") { _mint(msg.sender, 1000000 * 1e6); } function claim() external returns (uint256) { _mint(msg.sender, 10000 * 1e6); return 10000 * 1e6; } function decimals() public view virtual override returns (uint8) { return 6; } } contract ProtocolRewardsPoolTest is Test { address goerliEndPoint = 0xbfD2135BFfbb0B5378b56643c2Df8a87552Bfa23; address wbETH = 0xbfD2135BFfbb0B5378b56643c2Df8a87552Bfa23; address deployer; address attacker; address alice; address[] proposers; address[] executors; address[] minerContracts; bool[] minerContractsBools; GovernanceTimelock governance; mockCurve curvePool; Configurator configurator; PeUSDMainnet peUsdMainnet; EUSDMock eUSD; mockUSDC usdc; LBR lbr; esLBR eslbr; esLBRBoost boost; LybraWBETHVault wbETHVault; ProtocolRewardsPool rewardsPool; function setUp() public { deployer = makeAddr("deployer"); attacker = makeAddr("attacker"); alice = makeAddr("alice"); vm.startPrank(deployer); proposers.push(deployer); executors.push(deployer); governance = new GovernanceTimelock(2, proposers, executors, deployer); curvePool = new mockCurve(); configurator = new Configurator(address(governance), address(curvePool)); peUsdMainnet = new PeUSDMainnet( address(configurator), 8, goerliEndPoint ); eUSD = new EUSDMock(address(configurator)); // 6 decimal USDC token usdc = new mockUSDC(); lbr = new LBR(address(configurator), 8, goerliEndPoint); eslbr = new esLBR(address(configurator)); boost = new esLBRBoost(); rewardsPool = new ProtocolRewardsPool(address(configurator)); rewardsPool.setTokenAddress(address(eslbr), address(lbr), address(boost)); // Ether oracle has no impact on this test wbETHVault = new LybraWBETHVault(address(peUsdMainnet), makeAddr("NonImportantMockForEtherOracle"), wbETH, address(configurator)); configurator.setMintVault(deployer, true); configurator.initToken(address(eUSD), address(peUsdMainnet)); configurator.setProtocolRewardsPool(address(rewardsPool)); configurator.setProtocolRewardsToken(address(usdc)); curvePool.setToken(address(eUSD), address(usdc)); // Set minters minerContracts.push(address(deployer)); minerContracts.push(address(rewardsPool)); minerContractsBools.push(true); minerContractsBools.push(true); configurator.setTokenMiner(minerContracts, minerContractsBools); // Fund curve pool eusd/usdc eUSD.mint(address(curvePool), 10000 * 1e18); usdc.transfer(address(curvePool), 10000 * 1e6); // Fund ALice lbr.mint(address(alice), 100 * 1e18); vm.stopPrank(); } function test_notifyRewardAmountRewardCalculation() public { // Alice stake LBR vm.startPrank(alice); rewardsPool.stake(100 * 1e18); assertEq(eslbr.balanceOf(alice), 100 * 1e18); vm.stopPrank(); // Notify reward amount vm.startPrank(deployer); eUSD.mint(address(configurator), 3000 * 1e18); configurator.setPremiumTradingEnabled(true); uint256 eusdPreBalance = eUSD.balanceOf(address(configurator)); configurator.distributeRewards(); curvePool.setPrice(1010000); uint256 price = curvePool.get_dy_underlying(0, 2, 1e18); uint256 outUSDC = eusdPreBalance * price * 998 / 1e21; assertEq(eUSD.sharesOf(address(rewardsPool)), 0); assertEq(usdc.balanceOf(address(rewardsPool)), outUSDC); configurator.distributeRewards(); vm.stopPrank(); uint256 newRewardPerTokenstored = outUSDC * 1e36 / (10 ** ERC20(configurator.stableToken()).decimals()) / eslbr.totalSupply(); assertEq(rewardsPool.rewardPerTokenStored(), newRewardPerTokenstored); assertEq(rewardsPool.earned(alice), eslbr.balanceOf(alice) * (newRewardPerTokenstored - 0) / 1e18); } }
Convert decimals correctly when tokenType
equals 1:
function notifyRewardAmount(uint amount, uint tokenType) external { require(msg.sender == address(configurator)); if (totalStaked() == 0) return; require(amount > 0, "amount = 0"); if(tokenType == 0) { uint256 share = IEUSD(configurator.getEUSDAddress()).getSharesByMintedEUSD(amount); rewardPerTokenStored = rewardPerTokenStored + (share * 1e18) / totalStaked(); } else if(tokenType == 1) { ERC20 token = ERC20(configurator.stableToken()); rewardPerTokenStored = rewardPerTokenStored + (amount * 1e36 / (10 ** token.decimals()) / totalStaked(); } else { rewardPerTokenStored = rewardPerTokenStored + (amount * 1e18) / totalStaked(); } }
Decimal
#0 - c4-pre-sort
2023-07-10T01:37:42Z
JeffCX marked the issue as primary issue
#1 - c4-pre-sort
2023-07-10T01:37:49Z
JeffCX marked the issue as high quality report
#2 - c4-pre-sort
2023-07-13T13:44:15Z
JeffCX marked the issue as duplicate of #501
#3 - c4-judge
2023-07-28T15:40:19Z
0xean marked the issue as satisfactory
#4 - c4-judge
2023-07-28T19:46:50Z
0xean changed the severity to 2 (Med Risk)
55.9611 USDC - $55.96
https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/miner/ProtocolRewardsPool.sol#L190-L218 https://github.com/code-423n4/2023-06-lybra/blob/7b73ef2fbb542b569e182d9abf79be643ca883ee/contracts/lybra/miner/ProtocolRewardsPool.sol#L209
The getReward
external function can't calculate and distribute rewards correctly for an account because of the reasons below:
Users can't get rewards and rewards freezes.
// SPDX-License-Identifier: MIT pragma solidity ^0.8.17; import {Test, console} from "forge-std/Test.sol"; import {GovernanceTimelock} from "contracts/lybra/governance/GovernanceTimelock.sol"; import {mockCurve} from "contracts/mocks/mockCurve.sol"; import {Configurator} from "contracts/lybra/configuration/LybraConfigurator.sol"; import {LybraWBETHVault} from "contracts/lybra/pools/LybraWbETHVault.sol"; import {PeUSDMainnet} from "contracts/lybra/token/PeUSDMainnetStableVision.sol"; import {ProtocolRewardsPool} from "contracts/lybra/miner/ProtocolRewardsPool.sol"; import {EUSDMock} from "contracts/mocks/MockEUSD.sol"; import {LBR} from "contracts/lybra/token/LBR.sol"; import {esLBR} from "contracts/lybra/token/esLBR.sol"; import {esLBRBoost} from "contracts/lybra/miner/esLBRBoost.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; // 6 decimal USDC mock contract mockUSDC is ERC20 { constructor() ERC20("USDC", "USDC") { _mint(msg.sender, 1000000 * 1e6); } function claim() external returns (uint256) { _mint(msg.sender, 10000 * 1e6); return 10000 * 1e6; } function decimals() public view virtual override returns (uint8) { return 6; } } contract ProtocolRewardsPoolTest is Test { address goerliEndPoint = 0xbfD2135BFfbb0B5378b56643c2Df8a87552Bfa23; address wbETH = 0xbfD2135BFfbb0B5378b56643c2Df8a87552Bfa23; address deployer; address attacker; address alice; address[] proposers; address[] executors; address[] minerContracts; bool[] minerContractsBools; GovernanceTimelock governance; mockCurve curvePool; Configurator configurator; PeUSDMainnet peUsdMainnet; EUSDMock eUSD; mockUSDC usdc; LBR lbr; esLBR eslbr; esLBRBoost boost; LybraWBETHVault wbETHVault; ProtocolRewardsPool rewardsPool; function setUp() public { deployer = makeAddr("deployer"); attacker = makeAddr("attacker"); alice = makeAddr("alice"); vm.startPrank(deployer); proposers.push(deployer); executors.push(deployer); governance = new GovernanceTimelock(2, proposers, executors, deployer); curvePool = new mockCurve(); configurator = new Configurator(address(governance), address(curvePool)); peUsdMainnet = new PeUSDMainnet( address(configurator), 8, goerliEndPoint ); eUSD = new EUSDMock(address(configurator)); // 6 decimal USDC token usdc = new mockUSDC(); lbr = new LBR(address(configurator), 8, goerliEndPoint); eslbr = new esLBR(address(configurator)); boost = new esLBRBoost(); rewardsPool = new ProtocolRewardsPool(address(configurator)); rewardsPool.setTokenAddress(address(eslbr), address(lbr), address(boost)); // Ether oracle has no impact on this test wbETHVault = new LybraWBETHVault(address(peUsdMainnet), makeAddr("NonImportantMockForEtherOracle"), wbETH, address(configurator)); configurator.setMintVault(deployer, true); configurator.initToken(address(eUSD), address(peUsdMainnet)); configurator.setProtocolRewardsPool(address(rewardsPool)); configurator.setProtocolRewardsToken(address(usdc)); curvePool.setToken(address(eUSD), address(usdc)); // Set minters minerContracts.push(address(deployer)); minerContracts.push(address(rewardsPool)); minerContractsBools.push(true); minerContractsBools.push(true); configurator.setTokenMiner(minerContracts, minerContractsBools); // Fund curve pool eusd/usdc eUSD.mint(address(curvePool), 10000 * 1e18); usdc.transfer(address(curvePool), 10000 * 1e6); // Fund ALice lbr.mint(address(alice), 100 * 1e18); vm.stopPrank(); } function test_canGetReward() public { // Alice stake LBR vm.startPrank(alice); rewardsPool.stake(100 * 1e18); assertEq(eslbr.balanceOf(alice), 100 * 1e18); vm.stopPrank(); // Notify reward amount vm.startPrank(deployer); eUSD.mint(address(configurator), 3000 * 1e18); configurator.setPremiumTradingEnabled(true); uint256 eusdPreBalance = eUSD.balanceOf(address(configurator)); configurator.distributeRewards(); curvePool.setPrice(1010000); uint256 price = curvePool.get_dy_underlying(0, 2, 1e18); uint256 outUSDC = eusdPreBalance * price * 998 / 1e21; assertEq(eUSD.sharesOf(address(rewardsPool)), 0); assertEq(usdc.balanceOf(address(rewardsPool)), outUSDC); configurator.distributeRewards(); vm.stopPrank(); uint256 newRewardPerTokenstored = outUSDC * 1e36 / (10 ** ERC20(configurator.stableToken()).decimals()) / eslbr.totalSupply(); assertEq(rewardsPool.rewardPerTokenStored(), newRewardPerTokenstored); assertEq(rewardsPool.earned(alice), eslbr.balanceOf(alice) * (newRewardPerTokenstored - 0) / 1e18); uint256 earnedUSDC = rewardsPool.earned(alice); vm.startPrank(alice); rewardsPool.getReward(); // earnedUSDC / 1e12 -> Because rewardsPool.earned output has 18 decimal and USDC has 6 decimals assertEq(usdc.balanceOf(alice), earnedUSDC / 1e12); vm.stopPrank(); } }
Math
#0 - c4-pre-sort
2023-07-10T01:48:47Z
JeffCX marked the issue as duplicate of #501
#1 - c4-judge
2023-07-28T15:40:18Z
0xean marked the issue as satisfactory
#2 - c4-judge
2023-07-28T19:44:12Z
0xean changed the severity to 2 (Med Risk)
#3 - c4-judge
2023-07-28T20:39:20Z
0xean marked the issue as selected for report
#4 - c4-sponsor
2023-07-29T09:01:33Z
LybraFinance marked the issue as sponsor confirmed
π 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
Configurator
contract has external functions to set vaultBadCollateralRatio
and vaultSafeCollateralRatio
of pools, In the peUSD pool type vaultSafeCollateralRatio
of the pool checks incorrectly.
DAO can set vaultSafeCollateralRatio
of peUSD base pools lower than vaultBadCollateralRatio
.
// SPDX-License-Identifier: MIT pragma solidity ^0.8.17; import {Test, console} from "forge-std/Test.sol"; import {GovernanceTimelock} from "contracts/lybra/governance/GovernanceTimelock.sol"; import {mockCurve} from "contracts/mocks/mockCurve.sol"; import {Configurator} from "contracts/lybra/configuration/LybraConfigurator.sol"; import {LybraWBETHVault} from "contracts/lybra/pools/LybraWbETHVault.sol"; import {PeUSDMainnet} from "contracts/lybra/token/PeUSDMainnetStableVision.sol"; contract LybraConfiguratorTest is Test { address goerliEndPoint = 0xbfD2135BFfbb0B5378b56643c2Df8a87552Bfa23; address wbETH = 0xbfD2135BFfbb0B5378b56643c2Df8a87552Bfa23; address deployer; address attacker; address[] proposers; address[] executors; GovernanceTimelock governance; mockCurve curvePool; Configurator configurator; PeUSDMainnet peUsdMainnet; LybraWBETHVault wbETHVault; function setUp() public { deployer = makeAddr("deployer"); attacker = makeAddr("attacker"); vm.startPrank(deployer); proposers.push(deployer); executors.push(deployer); governance = new GovernanceTimelock(2, proposers, executors, deployer); curvePool = new mockCurve(); configurator = new Configurator(address(governance), address(0)); peUsdMainnet = new PeUSDMainnet( address(configurator), 8, goerliEndPoint ); // Ether oracle has no impact on this test wbETHVault = new LybraWBETHVault(address(peUsdMainnet), makeAddr("NonImportantMockForEtherOracle"), wbETH, address(configurator)); vm.stopPrank(); } function test_revertIfSafeDifferenceIsLowerThan10Percent() public { // Deployer had DAO role vm.startPrank(deployer); // Set safe collateral ratio - 120% configurator.setSafeCollateralRatio(address(wbETHVault), 120 * 1e18); assertEq(configurator.getSafeCollateralRatio(address(wbETHVault)), 120 * 1e18); // Set bad collateral ratio - 130% vm.expectRevert(); configurator.setBadCollateralRatio(address(wbETHVault), 130 * 1e18); vm.stopPrank(); } }
Should validate vaultSafeCollateralRatio[pool]
correctly:
function setBadCollateralRatio(address pool, uint256 newRatio) external onlyRole(DAO) { require( newRatio >= 130 * 1e18 && newRatio <= 150 * 1e18 && newRatio <= vaultSafeCollateralRatio[pool] - 1e19, "LNA" ); vaultBadCollateralRatio[pool] = newRatio; emit SafeCollateralRatioChanged(pool, newRatio); }
Invalid Validation
#0 - c4-pre-sort
2023-07-10T21:41:29Z
JeffCX marked the issue as duplicate of #108
#1 - c4-judge
2023-07-28T15:44:36Z
0xean marked the issue as satisfactory
#2 - c4-judge
2023-07-28T20:47:21Z
0xean changed the severity to QA (Quality Assurance)
#3 - c4-judge
2023-07-28T20:48:39Z
0xean marked the issue as grade-b