Platform: Code4rena
Start Date: 29/03/2022
Pot Size: $30,000 USDC
Total HM: 6
Participants: 24
Period: 3 days
Judge: HardlyDifficult
Total Solo HM: 4
Id: 101
League: ETH
Rank: 2/24
Findings: 3
Award: $5,134.83
π Selected for report: 1
π Solo Findings: 1
/** * @notice Function invoked when pooled credit line is terminated by admin * @dev only pooledCreditLineContract can invoke * @param _id identifier for the pooled credit line * @param _to address to which all the borrow tokens are transfered */ function terminate(uint256 _id, address _to) external override onlyPooledCreditLine nonReentrant { address _strategy = pooledCLConstants[_id].borrowAssetStrategy; address _borrowAsset = pooledCLConstants[_id].borrowAsset; uint256 _borrowedTokens = pooledCLConstants[_id].borrowLimit; uint256 _notBorrowed = _borrowedTokens.sub(POOLED_CREDIT_LINE.getPrincipal(_id)); uint256 _notBorrowedInShares = IYield(_strategy).getSharesForTokens(_notBorrowed, _borrowAsset); uint256 _sharesHeld = pooledCLVariables[_id].sharesHeld; uint256 _totalInterestInShares = _sharesHeld.sub(_notBorrowedInShares); uint256 _principalWithdrawable = _notBorrowed.mul(totalSupply[_id]).div(_borrowedTokens); SAVINGS_ACCOUNT.withdrawShares(_borrowAsset, _strategy, _to, _principalWithdrawable.add(_totalInterestInShares), false); delete pooledCLConstants[_id]; delete pooledCLVariables[_id]; }
LenderPool.sol#terminate()
will be called when a pooled credit line is terminated by admin.
However, in the current implementation, a wrong value is used as the shares
parameter in SAVINGS_ACCOUNT.withdrawShares()
.
Instead of using the amount of shares in terms of the _strategy
's shares, it's actually using _principalWithdrawable
which is in terms of _borrowAsset
.
As a result, when the pricePerShare of _strategy
's shares is higher than 1, terminate()
a pooled credit line will withdraw more funds from SAVINGS_ACCOUNT
than expected and all the benificatories of the SAVINGS_ACCOUNT
will suffer a fund loss.
Change to:
/** * @notice Function invoked when pooled credit line is terminated by admin * @dev only pooledCreditLineContract can invoke * @param _id identifier for the pooled credit line * @param _to address to which all the borrow tokens are transfered */ function terminate(uint256 _id, address _to) external override onlyPooledCreditLine nonReentrant { address _strategy = pooledCLConstants[_id].borrowAssetStrategy; address _borrowAsset = pooledCLConstants[_id].borrowAsset; uint256 _borrowedTokens = pooledCLConstants[_id].borrowLimit; uint256 _notBorrowed = _borrowedTokens.sub(POOLED_CREDIT_LINE.getPrincipal(_id)); uint256 _notBorrowedInShares = IYield(_strategy).getSharesForTokens(_notBorrowed, _borrowAsset); uint256 _sharesHeld = pooledCLVariables[_id].sharesHeld; uint256 _totalInterestInShares = _sharesHeld.sub(_notBorrowedInShares); uint256 _principalWithdrawableInShares = _notBorrowedInShares.mul(totalSupply[_id]).div(_borrowedTokens); SAVINGS_ACCOUNT.withdrawShares(_borrowAsset, _strategy, _to, _principalWithdrawableInShares.add(_totalInterestInShares), false); delete pooledCLConstants[_id]; delete pooledCLVariables[_id]; }
#0 - ritik99
2022-04-13T09:10:58Z
Duplicate of #21
π Selected for report: WatchPug
2590.5858 USDC - $2,590.59
function withdrawInterest(uint256 _id, address _lender) external nonReentrant { _withdrawInterest(_id, _lender); } function _withdrawInterest(uint256 _id, address _lender) internal { address _strategy = pooledCLConstants[_id].borrowAssetStrategy; address _borrowAsset = pooledCLConstants[_id].borrowAsset; (uint256 _interestToWithdraw, uint256 _interestSharesToWithdraw) = _calculateInterestToWithdraw( _id, _lender, _strategy, _borrowAsset ); pooledCLVariables[_id].sharesHeld = pooledCLVariables[_id].sharesHeld.sub(_interestSharesToWithdraw); if (_interestToWithdraw != 0) { SAVINGS_ACCOUNT.withdraw(_borrowAsset, _strategy, _lender, _interestToWithdraw, false); } emit InterestWithdrawn(_id, _lender, _interestSharesToWithdraw); }
withdrawInterest()
at a certain time may not be in the best interest of the specific lender
.
It's unconventional and can potentially cause leak of value for the lender
. For example, the lender may still want to accrued more interest from the strategy.
Change to:
function withdrawInterest(uint256 _id, address _lender) external nonReentrant { require(msg.sender == _lender); _withdrawInterest(_id, _lender); }
#0 - ritik99
2022-04-12T13:00:03Z
This is a valid suggestion. This allowed more flexibility from a composability/complexity perspective (for eg, an abstraction can be built that regularly withdraws interests for all lenders), hence the check was not put in place. We will add a check as suggested
Since assets are not at risk (any withdrawn amount is still transferred to the correct lender), we would suggest lowering the severity to (1) low-risk
#1 - HardlyDifficult
2022-04-26T15:40:04Z
Agree with medium risk as this seems to potentially leak some value for the lender.
212.706 USDC - $212.71
LenderPool.sol#lend()
Wrong event emittedfunction lend(uint256 _id, uint256 _amount) external nonReentrant { require(VERIFICATION.isUser(msg.sender, pooledCLConstants[_id].lenderVerifier), 'L1'); require(block.timestamp < pooledCLConstants[_id].startTime, 'L2'); uint256 _totalLent = totalSupply[_id]; uint256 _maxLent = pooledCLConstants[_id].borrowLimit; require(_maxLent > _totalLent, 'L3'); uint256 _amountToLend = _amount; if (_totalLent.add(_amount) > _maxLent) { _amountToLend = _maxLent.sub(_totalLent); } address _borrowAsset = pooledCLConstants[_id].borrowAsset; IERC20(_borrowAsset).safeTransferFrom(msg.sender, address(this), _amountToLend); _mint(msg.sender, _id, _amountToLend, ''); emit Lend(_id, msg.sender, _amount); // If borrowLimit reached, accept CL if (_totalLent.add(_amountToLend) == _maxLent) { _accept(_id, _maxLent); } }
_amount
should be changed to : _amountToLend
.
LimitsUpdated
event parameterevent LimitsUpdated(string indexed limitType, uint256 max, uint256 min);
function updateBorrowLimitLimits(uint256 _min, uint256 _max) external onlyOwner { require(_min < _max || _min.mul(_max) == 0, 'UBLL1'); require(!(borrowLimitLimits.min == _min && borrowLimitLimits.max == _max), 'UBLL2'); borrowLimitLimits = Limits(_min, _max); emit LimitsUpdated('borrowLimit', _min, _max); }
Change to:
emit LimitsUpdated('borrowLimit', _max, _min);
It's a best practice to use the latest compiler version.
The specified minimum compiler version is quite old. Older compilers might be susceptible to some bugs. We recommend changing the solidity version pragma to the latest version to enforce the use of an up to date compiler.
List of known compiler bugs and their severity can be found here: https://etherscan.io/solcbuginfo
PooledCreditLine.sol
LenderPool.sol
should use the Upgradeable variant of OpenZeppelin ContractsAs per the OpenZeppelin docs:
If your contract is going to be deployed with upgradeability, such as using the OpenZeppelin Upgrades Plugins, you will need to use the Upgradeable variant of OpenZeppelin Contracts.
import '@openzeppelin/contracts/utils/ReentrancyGuard.sol';
import '@openzeppelin/contracts/utils/ReentrancyGuard.sol';
Change to:
import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
div
before mul
_maxPossible = _totalCollateralToken.mul(_ratioOfPrices).div(_collateralRatio).mul(SCALING_FACTOR).div(10**_decimals);
Can be changed to:
_maxPossible = _totalCollateralToken.mul(_ratioOfPrices).mul(SCALING_FACTOR).div(_collateralRatio).div(10**_decimals);
#0 - ritik99
2022-04-12T19:54:25Z
All suggestions except second last ("[L] Precision loss due to div before mul") are valid. The operations have been defined in that sequence to prevent overflows.
The report is of high quality