Core Contracts

Regnum Aurum Acquisition Corp
HardhatReal World AssetsNFT
77,280 USDC
View results
Submission Details
Severity: high
Valid

[H-05] RToken's transferFrom() and transfer() implementations divide by the liquidity index one too many times

Summary

Implementation leads to applying two division operations instead of one compromising the results of the balances involved in a transfer/transferFrom call.

Vulnerability Details

Any call to transfer/transferFrom/mint/burn already invokes the overriden _update due to how ERC20 is implemented.
This is an issue because _update already performs rayDiv() using the up-to-date liquidity index, as intended by the "index-based" design.
With the current setup, each call to transfer() and transferFrom() will result in two division operations, leading to unexpected numerical results.
Furthermore in transferFrom() the function uses a local _liquidityIndex instead of accessing the LendingPool's one, resulting in an outdated index.

Location 1
Location 2

Impact

Any operation involving the RToken will result in erroneous/unexpected values, comprising the reliability of the protocol as a whole.

Tools Used

Manual review.

Recommendations

The transfer() and transferFrom() overrides should just call their "super" version without performing the division, since the handling of the index is already performed by the overriden _update().

Proof of Code

This snippet shows that following a transfer() between two users, the same quantity of 1 ether (taking into account the current index) for the user1 becomes less than the original/intended amount of the transfer due to it being divided once too many times by the liquidity index.
Note that in order for this to work, another bug needs to be fixed by adding something like the following to the constructor of the LendingPool:

rateData.currentLiquidityRate = 1e25;
rateData.currentUsageRate = 1e25;
import { expect } from "chai";
import hre from "hardhat";
const { ethers } = hre;
import { time, mine } from "@nomicfoundation/hardhat-network-helpers";
import { deployContracts } from './utils/deployContracts.js';
describe('Exploit Tests', function () {
// Set higher timeout for deployments
this.timeout(300000); // 5 minutes
let contracts;
let owner, user1, user2, user3, treasury, repairFund;
const INITIAL_MINT_AMOUNT = ethers.parseEther('1000');
const HOUSE_TOKEN_ID = '1021000';
const HOUSE_PRICE = ethers.parseEther('100');
const ONE_YEAR = 365 * 24 * 3600;
const FOUR_YEARS = 4 * ONE_YEAR;
const BASIS_POINTS = 10000;
before(async function () {
[owner, user1, user2, user3, treasury, repairFund] = await ethers.getSigners();
contracts = await deployContracts(owner, user1, user2, user3);
const displayContracts = Object.fromEntries(Object.entries(contracts).map(([key, value]) => [key, value.target]));
console.log(displayContracts);
// Set house price for testing
await contracts.housePrices.setHousePrice(HOUSE_TOKEN_ID, HOUSE_PRICE);
// Mint initial tokens to users
for (const user of [user1, user2, user3]) {
await contracts.crvUSD.mint(user.address, INITIAL_MINT_AMOUNT);
}
});
describe.only('Bugs:', function () {
it('[H-05] RToken\'s transferFrom() and transfer() implementations divide by the liquidity index one too many times', async function () {
/*
NEED TO ADD THIS TO CONSTRUCTOR OF LENDING POOL TO WORK (values are arbitrary)
rateData.currentLiquidityRate = 1e25;
rateData.currentUsageRate = 1e25;
*/
const DEPOSIT_AMOUNT = ethers.parseEther('1');
// user1 deposits crvUSD into lending pool to get rToken
await contracts.crvUSD.connect(user1).approve(contracts.lendingPool.target, DEPOSIT_AMOUNT);
await contracts.lendingPool.connect(user1).deposit(DEPOSIT_AMOUNT);
// See user1 balance
console.log("Amount of rToken for user1: " + await contracts.rToken.balanceOf(user1.address)); // 1000000000000000000
console.log("Scaled amount of rToken for user1: " + await contracts.rToken.scaledBalanceOf(user1.address)); // 999999999365804161
// transfer from user1 to user2
await contracts.rToken.connect(user1).transfer(user2.address, DEPOSIT_AMOUNT);
// See user2 balance
console.log("Amount of rToken for user2: " + await contracts.rToken.balanceOf(user2.address)); // 999999999365804161
console.log("Scaled amount of rToken for user2: " + await contracts.rToken.scaledBalanceOf(user2.address)); // 999999998731608322
});
});
});
Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

RToken::transfer and transferFrom double-scale amounts by dividing in both external functions and _update, causing users to transfer significantly less than intended

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

RToken::transfer and transferFrom double-scale amounts by dividing in both external functions and _update, causing users to transfer significantly less than intended

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!