Platform: Code4rena
Start Date: 16/02/2023
Pot Size: $144,750 USDC
Total HM: 17
Participants: 154
Period: 19 days
Judge: Trust
Total Solo HM: 5
Id: 216
League: ETH
Rank: 125/154
Findings: 1
Award: $61.26
π Selected for report: 0
π Solo Findings: 0
π Selected for report: GalloDaSballo
Also found by: 0x3b, 0xAgro, 0xSmartContract, 0xTheC0der, 0xackermann, 0xnev, 0xsomeone, ABA, BRONZEDISC, Bjorn_bug, Bnke0x0, Breeje, Co0nan, CodeFoxInc, CodingNameKiki, DadeKuma, DeFiHackLabs, IceBear, Josiah, Kaysoft, Lavishq, MohammedRizwan, PaludoX0, PawelK, Phantasmagoria, Raiders, RaymondFam, Rickard, Rolezn, Sathish9098, SleepingBugs, SuperRayss, UdarTeam, Udsen, Viktor_Cortess, arialblack14, ast3ros, bin2chen, brgltd, btk, catellatech, ch0bu, chaduke, chrisdior4, codeislight, cryptonue, delfin454000, descharre, dontonka, emmac002, fs0c, hacker-dom, hansfriese, imare, lukris02, luxartvinsec, martin, matrix_0wl, peanuts, rbserver, shark, tnevler, trustindistrust, tsvetanovv, vagrant, yongskiws, zzzitron
61.2601 USDC - $61.26
CollateralConfig
contractThe CollateralConfig
contract contains a constant variable MIN_ALLOWED_CCR
with the value 1.5 ether. The value of ether is assumed to be 10^18, but if ether were to be assigned a different value, there could be an integer overflow during the execution of the contract. An attacker could exploit this vulnerability to cause unexpected behavior in the contract.
An integer overflow caused by a change in the value of ether could result in a breach of the security of the contract. Depending on the severity of the overflow, the attacker could be able to steal funds or cause a denial-of-service attack. In the worst case, the contract could be rendered completely unusable.
It is recommended to replace the constant variables with the explicit values of 110% and 150%, respectively, to prevent potential integer overflow issues in the future. Additionally, it is recommended to use SafeMath
to perform arithmetic operations in the contract, which would prevent integer overflow attacks.
The MIN_ALLOWED_CCR
and MIN_ALLOWED_MCR
variables could be replaced with the following code snippet:
uint256 constant public MIN_ALLOWED_MCR = 110e16; // 110% uint256 constant public MIN_ALLOWED_CCR = 150e16; // 150%
The SafeMath
library could be imported and used for arithmetic operations, as shown below:
import "./Dependencies/SafeMath.sol"; // ... uint256 constant public MIN_ALLOWED_MCR = 110e16; // 110% uint256 constant public MIN_ALLOWED_CCR = 150e16; // 150% using SafeMath for uint256; // ... require(_MCRs[i].safeMul(1e18) >= MIN_ALLOWED_MCR, "MCR below allowed minimum"); config.MCR = _MCRs[i]; require(_CCRs[i].safeMul(1e18) >= MIN_ALLOWED_CCR, "CCR below allowed minimum"); config.CCR = _CCRs[i];
CollateralConfig
ContractThe CollateralConfig
contract has a vulnerability that may allow an attacker to exploit the re-entrancy attack. The issue exists in the updateCollateralRatios
function, which can change the MCR
and CCR
ratios of a specific collateral. An attacker can execute a re-entrancy attack on this function by calling an external contract with a fallback function that calls the transferFrom
function of a vulnerable ERC20 token, such as a malicious token.
An attacker can exploit this vulnerability to withdraw more funds than allowed as collateral, causing the contract to lose all of its assets. This vulnerability can lead to the loss of user funds, which can have a severe impact on the entire system.
To mitigate this vulnerability, I recommend using the nonReentrant
modifier in the updateCollateralRatios
function to prevent re-entrancy attacks. Also, it is recommended to use the SafeERC20
library in the updateCollateralRatios
function and avoid transferring funds by calling external contracts.
import "./Dependencies/ReentrancyGuard.sol"; contract CollateralConfig is ICollateralConfig, CheckContract, Ownable, ReentrancyGuard { using SafeERC20 for IERC20; ... function updateCollateralRatios( address _collateral, uint256 _MCR, uint256 _CCR ) external onlyOwner checkCollateral(_collateral) nonReentrant { Config storage config = collateralConfig[_collateral]; require(_MCR <= config.MCR, "Can only walk down the MCR"); require(_CCR <= config.CCR, "Can only walk down the CCR"); require(_MCR >= MIN_ALLOWED_MCR, "MCR below allowed minimum"); config.MCR = _MCR; require(_CCR >= MIN_ALLOWED_CCR, "CCR below allowed minimum"); config.CCR = _CCR; emit CollateralRatiosUpdated(_collateral, _MCR, _CCR); } }
Another recommendation you can use like this :
bool private locked = false; function updateCollateralRatios( address _collateral, uint256 _MCR, uint256 _CCR ) external onlyOwner checkCollateral(_collateral) { require(!locked, "Function is locked"); locked = true; Config storage config = collateralConfig[_collateral]; require(_MCR <= config.MCR, "Can only walk down the MCR"); require(_CCR <= config.CCR, "Can only walk down the CCR"); require(_MCR >= MIN_ALLOWED_MCR, "MCR below allowed minimum"); config.MCR = _MCR; require(_CCR >= MIN_ALLOWED_CCR, "CCR below allowed minimum"); config.CCR = _CCR; emit CollateralRatiosUpdated(_collateral, _MCR, _CCR); locked = false; }
The CommunityIssuance
contract may be vulnerable to re-entrancy attacks, which can allow an attacker to repeatedly execute a function to drain the contract's funds or cause other malicious behavior.
An attacker can use a re-entrancy attack to repeatedly call the issueOath()
or sendOath()
functions, which can lead to unexpected results. For example, an attacker could drain the contract's funds by repeatedly calling the issueOath()
function before the contract has a chance to update its state variables. Additionally, an attacker could execute malicious code repeatedly by exploiting the re-entrancy vulnerability.
To mitigate the risk of re-entrancy attacks, the CommunityIssuance
contract should use the "checks-effects-interactions" pattern. Specifically, any external call should be made after all state changes have been completed. In this case, the OathToken.transfer()
function should be called after the totalOATHIssued
variable has been updated, and the lastIssuanceTimestamp
variable has been set.
// @dev issues a set amount of Oath to the stability pool function issueOath() external override returns (uint issuance) { _requireCallerIsStabilityPool(); if (lastIssuanceTimestamp < lastDistributionTime) { uint256 endTimestamp = block.timestamp > lastDistributionTime ? lastDistributionTime : block.timestamp; uint256 timePassed = endTimestamp.sub(lastIssuanceTimestamp); issuance = timePassed.mul(rewardPerSecond); totalOATHIssued = totalOATHIssued.add(issuance); } lastIssuanceTimestamp = block.timestamp; emit TotalOATHIssuedUpdated(totalOATHIssued); // Move the external call to the end of the function if (issuance > 0) { OathToken.transfer(stabilityPoolAddress, issuance); } }
CommunityIssuance
contractThe CommunityIssuance
contract has a potential integer underflow vulnerability when calculating the issuance of OATH tokens in the function issueOath()
. An attacker could exploit this vulnerability to cause unexpected behavior, such as issuing more tokens than intended or causing the contract to become stuck.
An attacker could exploit this vulnerability to cause the contract to behave unexpectedly, which could result in financial loss for the contract owner or users of the contract.
I recommend adding a check to ensure that the rewardPerSecond
variable is greater than or equal to zero before calculating the issuance amount in the issueOath()
function.
function issueOath() external override returns (uint issuance) { _requireCallerIsStabilityPool(); if (lastIssuanceTimestamp < lastDistributionTime) { uint256 endTimestamp = block.timestamp > lastDistributionTime ? lastDistributionTime : block.timestamp; uint256 timePassed = endTimestamp.sub(lastIssuanceTimestamp); if (rewardPerSecond > 0) { issuance = timePassed.mul(rewardPerSecond); totalOATHIssued = totalOATHIssued.add(issuance); } } lastIssuanceTimestamp = block.timestamp; emit TotalOATHIssuedUpdated(totalOATHIssued); }
LQTYStaking
contract allows users to stake LQTY tokens but does not check if the contract is pausedThe LQTYStaking
contract allows users to stake LQTY tokens, but the contract does not check if it is paused. Therefore, if the contract is paused, users can still stake LQTY tokens, leading to possible losses.
If the contract is paused, users can still stake LQTY tokens, leading to a potential loss of funds.
It is recommended to add a modifier
to check if the contract is not paused before allowing users to stake tokens. Additionally, the contract owner should have the ability to pause
and unpause
the contract.
// --- Modifiers --- modifier notPaused() { require(!paused, "Contract is paused"); _; } // --- Functions --- function stake(uint _LQTYamount) external override notPaused { // function body } function pause() external onlyOwner { paused = true; } function unpause() external onlyOwner { paused = false; }
stake
function does not update snapshots before sending accumulated gainsThe stake
function in the LQTYStaking
contract allows users to stake LQTY tokens, and also withdraw any accumulated collateral and LUSD gains from their current stake. However, the function does not update the user's snapshots before sending the gains, which can lead to incorrect calculations of the collateral and LUSD gains.
If the stake
function is called by a user who has accumulated collateral or LUSD gains, but their snapshots have not been updated, the function will send incorrect gains. This could lead to incorrect accounting of the collateral and LUSD gains, and potentially cause financial losses for the staker.
The stake
function should update the user's snapshots before sending any accumulated gains. The following lines of code should be moved to the beginning of the function, before the check for existing stake:
_updateUserSnapshots(msg.sender); address[] memory collGainAssets; uint[] memory collGainAmounts; uint LUSDGain; // Grab any accumulated collateral and LUSD gains from the current stake if (currentStake != 0) { (collGainAssets, collGainAmounts) = _getPendingCollateralGain(msg.sender); LUSDGain = _getPendingLUSDGain(msg.sender); }
This will ensure that the user's snapshots are up-to-date before calculating the accumulated gains. As a best practice, the updated function should also include a require
statement to ensure that the caller has approved the transfer of the LQTY tokens to the staking contract. For example:
function stake(uint _LQTYamount) external override { require(lqtyToken.allowance(msg.sender, address(this)) >= _LQTYamount, "LQTY allowance not set"); _requireNonZeroAmount(_LQTYamount); _updateUserSnapshots(msg.sender); uint currentStake = stakes[msg.sender]; address[] memory collGainAssets; uint[] memory collGainAmounts; uint LUSDGain; // Grab any accumulated collateral and LUSD gains from the current stake if (currentStake != 0) { (collGainAssets, collGainAmounts) = _getPendingCollateralGain(msg.sender); LUSDGain = _getPendingLUSDGain(msg.sender); } uint newStake = currentStake.add(_LQTYamount); // Increase userβs stake and total LQTY staked stakes[msg.sender] = newStake; totalLQTYStaked = totalLQTYStaked.add(_LQTYamount); emit TotalLQTYStakedUpdated(totalLQTYStaked); // Transfer LQTY from caller to this contract lqtyToken.safeTransferFrom(msg.sender, address(this), _LQTYamount); emit StakeChanged(msg.sender, newStake); emit StakingGainsWithdrawn(msg.sender, LUSDGain, collGainAssets, collGainAmounts); }
If the Alice has a stake of 100 LQTY tokens, and has accumulated 10 LUSD in gains, but has not updated their snapshots, and then calls the stake
function to add 50 LQTY tokens to their stake, the function will send 10 LUSD to Alice, but her snapshots will not be updated. As a result, when Alice tries to withdraw her accumulated collateral gains, the function will return an incorrect value, which can lead to financial losses for Alice.
ReaperVaultERC4626
contractThe ReaperVaultERC4626
contract allows users to deposit
and withdraw
assets to/from the contract, but it lacks proper input validation. Specifically, the contract doesn't check if the receiver address is valid, which could lead to funds being sent to an unintended recipient or lost forever.
This vulnerability can have severe consequences as users could unintentionally lose their funds. If the receiver address is not a valid address, the funds will be lost forever.
To mitigate this vulnerability, input validation should be added to the deposit
function. Specifically, the contract should check that the receiver address is a valid Ethereum address.
For example, in the deposit
function, the following line of code can be added to check the validity of the receiver address:
require(receiver != address(0), "Invalid receiver address");
ReaperVaultERC4626
The contract ReaperVaultERC4626
has an integer overflow vulnerability. The convertToShares()
and convertToAssets()
functions can potentially overflow if the values of the totalSupply()
and _freeFunds()
variables are very large. As a result, attackers can mint an unlimited number of shares, leading to a loss of funds for the contract.
An attacker can exploit this vulnerability to mint an unlimited number of shares and potentially drain the contract of all its funds. If successful, this attack could cause significant financial losses to the contract and its users.
To fix the vulnerability, the contract can be modified by using the SafeMath
library to perform arithmetic operations on variables. The SafeMath
library prevents integer overflow and underflow by reverting the transaction if an operation results in an overflow or underflow.
import "./ReaperVaultV2.sol"; import "./interfaces/IERC4626Functions.sol"; import "@openzeppelin/contracts/utils/math/SafeMath.sol"; contract ReaperVaultERC4626 is ReaperVaultV2, IERC4626Functions { using SafeERC20 for IERC20Metadata; using SafeMath for uint256; // See comments on ReaperVaultV2's constructor constructor( address _token, string memory _name, string memory _symbol, uint256 _tvlCap, address _treasury, address[] memory _strategists, address[] memory _multisigRoles ) ReaperVaultV2(_token, _name, _symbol, _tvlCap, _treasury, _strategists, _multisigRoles) {} function convertToShares(uint256 assets) public view override returns (uint256 shares) { if (totalSupply() == 0 || _freeFunds() == 0) return assets; return assets.mul(totalSupply()).div(_freeFunds()); } function convertToAssets(uint256 shares) public view override returns (uint256 assets) { if (totalSupply() == 0) return shares; return shares.mul(_freeFunds()).div(totalSupply()); } function previewMint(uint256 shares) public view override returns (uint256 assets) { if (totalSupply() == 0) return shares; assets = roundUpDiv(shares.mul(_freeFunds()), totalSupply()); } function previewWithdraw(uint256 assets) public view override returns (uint256 shares) { if (totalSupply() == 0 || _freeFunds() == 0) return 0; shares = roundUpDiv(assets.mul(totalSupply()), _freeFunds()); } function roundUpDiv(uint256 x, uint256 y) internal pure returns (uint256) { require(y != 0, "Division by 0"); uint256 z = x.add(y.sub(1)); return z.div(y); } }
withdrawal
function can be called by unauthorized partiesThe withdraw()
function of the ReaperBaseStrategyv4
contract can be called by any address, not only the vault. This can lead to unauthorized withdrawals and theft of funds.
An attacker can steal funds from the contract by calling the withdraw()
function with an arbitrary amount. They can do this by simply creating a transaction from any address, even if it's not authorized to withdraw funds. This can result in the loss of user funds and reputation damage to the contract.
The contract should be updated to include access control on the withdraw()
function. This can be achieved using OpenZeppelin's AccessControl
library, which is already being used in the contract. Only the vault address should be allowed to call the function, as it's responsible for managing the contract's funds.
function withdraw(uint256 _amount) external override onlyRole(VAULT_ROLE) returns (uint256 loss) { require(_amount != 0, "Amount cannot be zero"); require(_amount <= balanceOf(), "Amount must be less than balance"); uint256 amountFreed = 0; (amountFreed, loss) = _liquidatePosition(_amount); IERC20Upgradeable(want).safeTransfer(msg.sender, amountFreed); }
In this example, the onlyRole
modifier restricts access to the VAULT_ROLE
. This ensures that only the vault can withdraw funds from the contract. Also, the msg.sender
is used instead of the vault address to transfer funds back, which is safer as it ensures that the funds are transferred to the address that called the function.
ReaperStrategyGranarySupplyOnly
smart contractReaperStrategyGranarySupplyOnly
is a smart contract that deposits tokens on Granary to maximize yield. The vulnerability allows an attacker to steal funds from the contract by using a flash loan attack.
An attacker can steal funds from the contract by using a flash loan attack. The vulnerability allows an attacker to perform multiple transactions in a single atomic transaction by taking advantage of the fact that some of the contract's logic is not atomic. The attacker can take out a flash loan, deposit the borrowed funds into the contract, and then withdraw the contract's funds to a different address before returning the borrowed funds. This would result in the contract losing its funds and the attacker gaining the stolen funds.
The vulnerable function _harvestCore
should be updated to ensure that it is atomic. One way to do this would be to use a reentrancy guard to prevent the function from being called multiple times within the same transaction. Additionally, it is recommended to use the nonReentrant
modifier from the OpenZeppelin ReentrancyGuard
library to prevent reentrancy attacks.
import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; contract ReaperStrategyGranarySupplyOnly is ReaperBaseStrategyv4, VeloSolidMixin, ReentrancyGuard { ... function _harvestCore(uint256 _debt) internal override nonReentrant returns (int256 roi, uint256 repayment) { _claimRewards(); uint256 numSteps = steps.length; for (uint256 i = 0; i < numSteps; i = i.uncheckedInc()) { address[2] storage step = steps[i]; IERC20Upgradeable startToken = IERC20Upgradeable(step[0]); uint256 amount = startToken.balanceOf(address(this)); if (amount == 0) { continue; } _swapVelo(step[0], step[1], amount, VELO_ROUTER); } uint256 allocated = IVault(vault).strategies(address(this)).allocated; ...
ReaperStrategyGranarySupplyOnly
smart contractThe ReaperStrategyGranarySupplyOnly
smart contract is vulnerable to a potential reentrancy attack due to not updating the total debt when the position is adjusted.
An attacker could potentially withdraw the entire want balance from the contract by exploiting the reentrancy vulnerability. The impact would depend on the amount of funds in the contract at the time of the attack.
To prevent this reentrancy attack, it is recommended to update the total debt value before calling any external functions that could trigger further contract interactions. For example, in the _adjustPosition
function, the wantBalance
should be checked before updating the totalDebt
value. The updated function should look like:
function _adjustPosition(uint256 _debt) internal override { if (emergencyExit) { return; } uint256 wantBalance = balanceOfWant(); if (wantBalance > _debt) { uint256 toReinvest = wantBalance - _debt; _deposit(toReinvest); } totalDebt = ILendingPool(ADDRESSES_PROVIDER.getLendingPool()).getUserDebtCurrent(address(this), want); }
This update will ensure that the totalDebt
value is accurate before any external function calls are made.
ReaperStrategyGranarySupplyOnly
contractThe contract ReaperStrategyGranarySupplyOnly
is vulnerable to an arithmetic overflow attack because it uses unchecked arithmetic. An attacker could exploit this vulnerability to steal funds from the contract.
An attacker could exploit the vulnerability in the contract to steal funds from the contract. They could use a smart contract that exploits the vulnerability to drain the contract of its funds, leading to financial loss for the users of the contract.
The use of unchecked arithmetic should be avoided as it can lead to unpredictable results. The Solidity language provides built-in functions for safe arithmetic operations such as SafeMath
, which should be used instead. In the contract, the unchecked increment operator is used in the for loop at lines 117-165, which can be replaced with the safe MathUpgradeable.add()
function.
for (uint256 i = 0; i < numSteps; i = i.uncheckedInc()) {
should be replaced with
for (uint256 i = 0; i < numSteps; i = MathUpgradeable.add(i, 1)) {
import
in TroveManager.sol
Recommendation : delete //
this wrong symbol for import Ownable.sol
Also all contracts have old Solidity version. Please upgrade latest Solidity version : 0.8.19
#0 - c4-judge
2023-03-09T15:32:07Z
trust1995 marked the issue as grade-b
#1 - c4-sponsor
2023-03-28T20:11:23Z
0xBebis marked the issue as sponsor acknowledged