Summary
Transfer amount is scaled twice in RToken.sol
Vulnerability Details
In the RToken.sol
contract, transfer()
and transferFrom()
functions are overwritten as:
function transfer(address recipient, uint256 amount) public override(ERC20, IERC20) returns (bool) {
uint256 scaledAmount = amount.rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
return super.transfer(recipient, scaledAmount);
}
function transferFrom(address sender, address recipient, uint256 amount) public override(ERC20, IERC20) returns (bool) {
uint256 scaledAmount = amount.rayDiv(_liquidityIndex);
return super.transferFrom(sender, recipient, scaledAmount);
}
here, amount
is scaled beforehand.
But, _update()
is also overwritten:
function _update(address from, address to, uint256 amount) internal override {
uint256 scaledAmount = amount.rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
super._update(from, to, scaledAmount);
}
And it scales the amount again for transfer()
functions.
As we know, _update
is called in transfer() functions again for ERC20 tokens. Thus, amount will be scaled twice with index.
Impact
Due to scaling twice, while transferring RTokens, the receiver will receive more or less than expected. i.e more if index < 1 and less if index > 1.
In the withdraw()
function of StabilitiyPool.sol
:
function withdraw(uint256 deCRVUSDAmount) external nonReentrant whenNotPaused validAmount(deCRVUSDAmount) {
_update();
if (deToken.balanceOf(msg.sender) < deCRVUSDAmount) revert InsufficientBalance();
uint256 rcrvUSDAmount = calculateRcrvUSDAmount(deCRVUSDAmount);
uint256 raacRewards = calculateRaacRewards(msg.sender);
if (userDeposits[msg.sender] < rcrvUSDAmount) revert InsufficientBalance();
userDeposits[msg.sender] -= rcrvUSDAmount;
if (userDeposits[msg.sender] == 0) {
delete userDeposits[msg.sender];
}
deToken.burn(msg.sender, deCRVUSDAmount);
rToken.safeTransfer(msg.sender, rcrvUSDAmount);
if (raacRewards > 0) {
raacToken.safeTransfer(msg.sender, raacRewards);
}
emit Withdraw(msg.sender, rcrvUSDAmount, deCRVUSDAmount, raacRewards);
}
msg.sender
is transferred RTokens
and if index is >1, the msg.sender
withdrawing tokens will receive less than expected. Thus, loss of funds for the user.
And if index is <1, loss of funds for the protocol. Thus, the high severity.
Tools Used
Manual Analysis
Recommendations
Modify the _update()
function as:
function _update(address from, address to, uint256 amount) internal override {
if (from == address(0) || to == address(0)) {
uint256 scaledAmount = amount.rayDiv(ILendingPool(_reservePool).getNormalizedIncome());
super._update(from, to, scaledAmount);
return;
}
}
This will exclude the transfer
functions.