Summary
LendingPool::_rebalanceLiquidity()
emits LiquidityRebalanced
with the wrong asset balance/buffer.
Vulnerability Details
The LendingPool::_rebalanceLiquidity()
internal function is responsible for rebalancing liquidity by meeting the desired buffer. This function is called upon LendingPool::deposit()
, LendingPool::withdraw()
, and LendingPool::borrow()
and only rebalances assuming curveVault
is not address 0 and there is a difference between buffers:
function _rebalanceLiquidity() internal {
if (address(curveVault) == address(0)) {
return;
}
uint256 totalDeposits = reserve.totalLiquidity;
uint256 desiredBuffer = totalDeposits.percentMul(liquidityBufferRatio);
uint256 currentBuffer = IERC20(reserve.reserveAssetAddress).balanceOf(reserve.reserveRTokenAddress);
if (currentBuffer > desiredBuffer) {
uint256 excess = currentBuffer - desiredBuffer;
_depositIntoVault(excess);
} else if (currentBuffer < desiredBuffer) {
uint256 shortage = desiredBuffer - currentBuffer;
_withdrawFromVault(shortage);
}
emit LiquidityRebalanced(currentBuffer, totalVaultDeposits);
}
The function determines the desiredBuffer
based on the total liquidity and buffer ratio, while getting the currentBuffer
based on the current balance of the reserve asset in the contract:
uint256 totalDeposits = reserve.totalLiquidity;
uint256 desiredBuffer = totalDeposits.percentMul(liquidityBufferRatio);
uint256 currentBuffer = IERC20(reserve.reserveAssetAddress).balanceOf(reserve.reserveRTokenAddress);
It is then determined that if the currentBuffer
is higher than the desiredBuffer
, deposit into Curve, else, withdraw from Curve.
if (currentBuffer > desiredBuffer) {
uint256 excess = currentBuffer - desiredBuffer;
_depositIntoVault(excess);
} else if (currentBuffer < desiredBuffer) {
uint256 shortage = desiredBuffer - currentBuffer;
_withdrawFromVault(shortage);
}
Both _depositIntoVault()
& _withdrawFromVault()
functions update the current balance of the reserve asset (essentially meeting the expectation of making the currentBuffer
the desiredBuffer
) and the totalVaultDeposits
state:
function _depositIntoVault(uint256 amount) internal {
IERC20(reserve.reserveAssetAddress).approve(address(curveVault), amount);
curveVault.deposit(amount, address(this));
totalVaultDeposits += amount;
}
function _withdrawFromVault(uint256 amount) internal {
curveVault.withdraw(amount, address(this), msg.sender, 0, new address[](0));
totalVaultDeposits -= amount;
}
Afterwards, execution returns back to _rebalanceLiquidity()
where the following event is emitted:
emit LiquidityRebalanced(currentBuffer, totalVaultDeposits);
As is, the event emits currentBuffer
for the 1st argument, which is the local value set before the deposit or withdraw. This results in an incorrect value as the balance of the reserve asset has changed. The currentBuffer
is no longer the amount of liquidity in the buffer.
Furthermore, as per the defined interface for the event, the 1st argument should represent the liquidity in the buffer and the 2nd argument should be the liquidity in the vault:
* @notice Emitted when liquidity is rebalanced
* @param bufferAmount The amount of liquidity in the buffer
* @param vaultAmount The amount of liquidity in the Curve vault
*/
event LiquidityRebalanced(uint256 bufferAmount, uint256 vaultAmount);
Impact
Frontend and off-chain services relying on the LiquidityRebalanced
event will receive incorrect values for liquidity in the buffer.
Tools Used
Recommendations
Pass in desiredBuffer
instead of currentBuffer
for the 1st argument in LiquidityRebalanced
:
function _rebalanceLiquidity() internal {
// if curve vault is not set, do nothing
if (address(curveVault) == address(0)) {
return;
}
uint256 totalDeposits = reserve.totalLiquidity; // Total liquidity in the system
uint256 desiredBuffer = totalDeposits.percentMul(liquidityBufferRatio);
uint256 currentBuffer = IERC20(reserve.reserveAssetAddress).balanceOf(reserve.reserveRTokenAddress);
if (currentBuffer > desiredBuffer) {
uint256 excess = currentBuffer - desiredBuffer;
// Deposit excess into the Curve vault
_depositIntoVault(excess);
} else if (currentBuffer < desiredBuffer) {
uint256 shortage = desiredBuffer - currentBuffer;
// Withdraw shortage from the Curve vault
_withdrawFromVault(shortage);
}
- emit LiquidityRebalanced(currentBuffer, totalVaultDeposits);
+ emit LiquidityRebalanced(desiredBuffer, totalVaultDeposits);
}