Omniscia Steadefi Audit
LendingPool Manual Review Findings
LendingPool Manual Review Findings
LPL-01M: Improper Execution Flows
Type | Severity | Location |
---|---|---|
Language Specific | LendingPool.sol:L248, L296 |
Description:
The LendingPool::borrow
and LendingPool::repay
functions do not implement correct execution flows and thus contain incorrect system states when they perform outward / inward transfer of funds.
Impact:
While the contract is adequately protected against re-entrancy attacks, other system modules that rely on LendingPool
data points may be prone to cross-contract re-entrancy attacks as a result of the incorrect system state that LendingPool
has during these transfers.
Example:
236/**237 * Borrow asset from lending pool, adding debt238 * @param _borrowAmount Amount of tokens to borrow in token decimals239*/240function borrow(uint256 _borrowAmount) external nonReentrant whenNotPaused onlyBorrower {241 require(_borrowAmount > 0, "Amount must be > 0");242 require(_borrowAmount <= totalAvailableSupply(), "Not enough lending liquidity to borrow");243
244 // Update pool with accrued interest and latest timestamp245 _updatePoolWithInterestsAndTimestamp(0);246
247 // Transfer borrowed token from pool to manager248 IERC20(asset).safeTransfer(msg.sender, _borrowAmount);249
250 // Calculate debt amount251 uint256 debt = totalBorrows == 0 ? _borrowAmount : _borrowAmount * totalBorrowDebt / totalBorrows;252
253 // Update pool state254 totalBorrows = totalBorrows + _borrowAmount;255 totalBorrowDebt = totalBorrowDebt + debt;256
257 // Update borrower state258 borrowers[msg.sender].debt = borrowers[msg.sender].debt + debt;259 borrowers[msg.sender].lastUpdatedAt = block.timestamp;260
261 emit Borrow(msg.sender, debt, _borrowAmount);262}
Recommendation:
We advise the LendingPool::borrow
function to transfer the _borrowAmount
funds outward at the end of its function execution and the LendingPool::repay
function to transfer the _repayAmount
inward at the start of the function after the proper _repayAmount
has been assessed, ensuring that the LendingPool
contract contains a correct state when the asset
transfer is being performed.
Alleviation (4325253d6de0ea91c1e9fb9e01d2e7e98f3d83a9):
The recommended course of action for the two referenced instances has been applied to the letter, ensuring that the LendingPool
storage state during inward and outward token transfers is correct.
LPL-02M: Improper Enforcement of Access Control
Type | Severity | Location |
---|---|---|
Logical Fault | LendingPool.sol:L268 |
Description:
The LendingPool::repay
function improperly mandates that the caller is a borrower via the onlyBorrower
modifier.
Impact:
If a borrower is removed from the system while their debt is still active, they will be unable to repay it thus causing bad debt to be created in the system undesirably as the ex-borrower may wish to actually repay it.
Example:
236/**237 * Borrow asset from lending pool, adding debt238 * @param _borrowAmount Amount of tokens to borrow in token decimals239*/240function borrow(uint256 _borrowAmount) external nonReentrant whenNotPaused onlyBorrower {241 require(_borrowAmount > 0, "Amount must be > 0");242 require(_borrowAmount <= totalAvailableSupply(), "Not enough lending liquidity to borrow");243
244 // Update pool with accrued interest and latest timestamp245 _updatePoolWithInterestsAndTimestamp(0);246
247 // Transfer borrowed token from pool to manager248 IERC20(asset).safeTransfer(msg.sender, _borrowAmount);249
250 // Calculate debt amount251 uint256 debt = totalBorrows == 0 ? _borrowAmount : _borrowAmount * totalBorrowDebt / totalBorrows;252
253 // Update pool state254 totalBorrows = totalBorrows + _borrowAmount;255 totalBorrowDebt = totalBorrowDebt + debt;256
257 // Update borrower state258 borrowers[msg.sender].debt = borrowers[msg.sender].debt + debt;259 borrowers[msg.sender].lastUpdatedAt = block.timestamp;260
261 emit Borrow(msg.sender, debt, _borrowAmount);262}263
264/**265 * Repay asset to lending pool, reducing debt266 * @param _repayAmount Amount of debt to repay in token decimals267*/268function repay(uint256 _repayAmount) external nonReentrant whenNotPaused onlyBorrower {269 require(_repayAmount > 0, "Amount must be > 0");270 require(maxRepay(msg.sender) > 0, "Repay amount must be > 0");271
272 if (_repayAmount > maxRepay(msg.sender)) {273 _repayAmount = maxRepay(msg.sender);274 }275
276 // Update pool with accrued interest and latest timestamp277 _updatePoolWithInterestsAndTimestamp(0);278
279 uint256 borrowerTotalRepayAmount = maxRepay(msg.sender);280
281 // Calculate debt to reduce based on repay amount282 uint256 debt = _repayAmount * SAFE_MULTIPLIER283 / borrowerTotalRepayAmount284 * borrowers[msg.sender].debt285 / SAFE_MULTIPLIER;286
287 // Update pool state288 totalBorrows = totalBorrows - _repayAmount;289 totalBorrowDebt = totalBorrowDebt - debt;290
291 // Update borrower state292 borrowers[msg.sender].debt = borrowers[msg.sender].debt - debt;293 borrowers[msg.sender].lastUpdatedAt = block.timestamp;294
295 // Transfer repay tokens to the pool296 IERC20(asset).safeTransferFrom(msg.sender, address(this), _repayAmount);297
298 emit Repay(msg.sender, debt, _repayAmount);299}
Recommendation:
We advise the LendingPool::repay
function to no longer apply the modifier
as a non-zero LendingPool::maxRepay
value indicates that a LendingPool::borrow
transaction was performed that has applied the borrower check. Additionally, if a borrower is removed from the system they should be able to repay their debt regardless.
Alleviation (4325253d6de0ea91c1e9fb9e01d2e7e98f3d83a9):
The LendingPool::repay
function no longer mandates that its caller is a borrower, preventing the behaviour outlined in the exhibit and thus alleviating it.
LPL-03M: Improper Native Deposit / Withdrawal Handling System
Type | Severity | Location |
---|---|---|
Logical Fault | LendingPool.sol:L187-L196, L211-L218 |
Description:
The LendingPool::deposit
and LendingPool::withdraw
functions will improperly handle incoming native payments and redundantly contain an _isNative
variable. In detail, if a user submits a LendingPool::deposit
call with a non-zero msg.value
but a false
value for _isNative
, their funds will be locked in the contract forever.
Impact:
It is presently possible to permanently lock native funds within the contract, rendering them irrecoverable.
Example:
181/**182 * Deposits asset into lending pool and mint ibToken to user183 * @param _assetAmount Amount of asset tokens to deposit in token decimals184 * @param _isNative Boolean for whether asset is a native token or not185*/186function deposit(uint256 _assetAmount, bool _isNative) external payable nonReentrant whenNotPaused {187 if (_isNative) {188 require(isNativeAsset == true, "Lending pool asset not native token");189 require(msg.value > 0, "Quantity must be > 0");190 require(_assetAmount == msg.value, "Amount != msg.value");191
192 IWAVAX(asset).deposit{ value: msg.value }();193 } else {194 require(_assetAmount > 0, "Deposited amount must be > 0");195 IERC20(asset).safeTransferFrom(msg.sender, address(this), _assetAmount);196 }197
198 // Update pool with accrued interest and latest timestamp199 _updatePoolWithInterestsAndTimestamp(_assetAmount);200
201 uint256 sharesAmount = _mintShares(_assetAmount);202
203 emit Deposit(msg.sender, sharesAmount, _assetAmount);204}
Recommendation:
We advise the code to remove the _isNative
variable altogether, instead evaluating whether msg.value
is non-zero in LendingPool::deposit
and evaluating isNativeAsset
in LendingPool::withdraw
.
In the LendingPool::deposit
function, the isNativeAsset
value should be mandated as true
and the msg.value
to be equivalent to _assetAmount
when it is non-zero. Alternatively (i.e. else
branch), the isNativeAsset
value should be mandated as false
and the _assetAmount
as non-zero.
The withdraw
function does not require the _isNative
variable as the isNativeAsset
variable is sufficient to assess which logic branches of it should be executed.
These changes will minimize the gas cost involved in all functions and will resolve the present vulnerability outlined in the description of this issue.
Alleviation (4325253d6de0ea91c1e9fb9e01d2e7e98f3d83a9):
The LendingPool::deposit
and LendingPool::withdraw
flows were altered per our recommendation, making use of the isNativeAsset
contract-level variable and preventing the lock of native funds in the contract due to misuse of these functions.
LPL-04M: Inexistent Slippage Protections
Type | Severity | Location |
---|---|---|
Logical Fault | LendingPool.sol:L186, L210 |
Description:
The LendingPool::deposit
and LendingPool::withdraw
functions do not apply any form of slippage protection, rendering all depositors and withdrawers prone to slippage related attacks (such as sandwich attacks).
Impact:
The current mechanisms of the contract render the users prone to arbitrage attacks, causing them to receive less shares and less funds than they expect from their deposits and withdrawals.
Example:
181/**182 * Deposits asset into lending pool and mint ibToken to user183 * @param _assetAmount Amount of asset tokens to deposit in token decimals184 * @param _isNative Boolean for whether asset is a native token or not185*/186function deposit(uint256 _assetAmount, bool _isNative) external payable nonReentrant whenNotPaused {187 if (_isNative) {188 require(isNativeAsset == true, "Lending pool asset not native token");189 require(msg.value > 0, "Quantity must be > 0");190 require(_assetAmount == msg.value, "Amount != msg.value");191
192 IWAVAX(asset).deposit{ value: msg.value }();193 } else {194 require(_assetAmount > 0, "Deposited amount must be > 0");195 IERC20(asset).safeTransferFrom(msg.sender, address(this), _assetAmount);196 }197
198 // Update pool with accrued interest and latest timestamp199 _updatePoolWithInterestsAndTimestamp(_assetAmount);200
201 uint256 sharesAmount = _mintShares(_assetAmount);202
203 emit Deposit(msg.sender, sharesAmount, _assetAmount);204}205
206/**207 * Withdraws asset from lending pool, burns ibToken from user208 * @param _ibTokenAmount Amount of ibTokens to burn in 1e18209*/210function withdraw(uint256 _ibTokenAmount, bool _isNative) external nonReentrant whenNotPaused {211 if (_isNative) {212 require(isNativeAsset == true, "Lending pool asset not native token");213 require(_ibTokenAmount > 0, "Amount must be > 0");214 require(_ibTokenAmount <= balanceOf(msg.sender), "Withdraw amount exceeds balance");215 } else {216 require(_ibTokenAmount > 0, "Amount must be > 0");217 require(_ibTokenAmount <= balanceOf(msg.sender), "Withdraw amount exceeds balance");218 }219
220 // Update pool with accrued interest and latest timestamp221 _updatePoolWithInterestsAndTimestamp(0);222
223 uint256 withdrawAmount = _burnShares(_ibTokenAmount);224
225 if (_isNative) {226 IWAVAX(asset).withdraw(withdrawAmount);227 (bool success, ) = msg.sender.call{value: withdrawAmount}("");228 require(success, "Transfer failed.");229 } else {230 IERC20(asset).safeTransfer(msg.sender, withdrawAmount);231 }232
233 emit Withdraw(msg.sender, _ibTokenAmount, withdrawAmount);234}
Recommendation:
We advise both functions to introduce a new variable representing the minimum sharesAmount
and minimum withdrawAmount
respectively that they wish to accept as a result of their LendingPool::deposit
/ LendingPool::withdraw
transaction.
Alleviation (4325253d6de0ea91c1e9fb9e01d2e7e98f3d83a9):
The LendingPool::deposit
and LendingPool::withdraw
functions were updated to accept an additional argument that represents the minimum shares / withdrawal amount the user is willing to accept, enabling them to control the slippage they expose themselves to.
LPL-05M: Total Fund Loss of First Deposit
Type | Severity | Location |
---|---|---|
Logical Fault | LendingPool.sol:L310, L311 |
Description:
The LendingPool::_mintShares
function follows an unconventional paradigm whereby the previousTotalValue
of the contract is evaluated as an indicator of existing shares rather than the actual ERC20::totalSupply
. As a result, it is possible to race the first LendingPool::deposit
transaction and transmit asset
funds directly to the contract. This will cause the previousTotalValue
to evaluate to a non-zero value due to how LendingPool::totalValue
-> LendingPool::totalAvailableSupply
evaluates the balanceOf
the contract itself. As such, the LendingPool::_mintShares
function will calculate _assetAmount * totalSupply() / previousTotalValue
with a zero totalSupply
, causing the transaction to mint zero shares.
Impact:
The present calculation enables a malicious party to transmit dust to the LendingPool
implementation of a particular asset and cause total fund loss for the depositor.
Example:
303/**304 * Calculate amount of ibTokens owed to depositor and mints them305 * @param _assetAmount Amount of tokens to deposit in token decimals306 * @return shares Amount of ibTokens minted in 1e18307*/308function _mintShares(uint256 _assetAmount) internal returns (uint256) {309 // Calculate liquidity share amount310 uint256 previousTotalValue = totalValue() - _assetAmount;311 uint256 shares = previousTotalValue == 0 ? _assetAmount * _to18ConversionFactor(asset) : _assetAmount * totalSupply() / previousTotalValue;312
313 // Mint ibToken to user equal to liquidity share amount314 _mint(msg.sender, shares);315
316 return shares;317}
Recommendation:
We advise the contract to utilize the totalSupply
function as an indicator of whether shares exist in the system instead of a previousTotalValue
evaluation.
Alleviation (4325253d6de0ea91c1e9fb9e01d2e7e98f3d83a9):
The share calculation mechanism was adjusted to utilize the ERC20::totalSupply
per our recommendation, preventing the first deposit from being susceptible to attacks.