Summary
The issue is arises from the conditional structure within the updateTemperatureAndBeanToMaxLpGpPerBdvRatio function. so When an oracle failure is detected this is indicated by the oracleFailure boolean, the function updates the temperature but skips the ratio update. This conditional update leads to scenarios where the temperature reflects the latest conditions, while the ratio does not, resulting in an inconsistent state.
Vulnerability Details
The contract is for adjusting the temperature and the Bean to Max LP ratio beanToMaxLpGpPerBdvRatio based on certain conditions defined by the case ID. The function updateTemperatureAndBeanToMaxLpGpPerBdvRatio it's handles these updates, and this function can leave the system in an inconsistent state when an oracle failure occurs here :
function updateTemperatureAndBeanToMaxLpGpPerBdvRatio(uint256 caseId, bool oracleFailure) internal {
LibCases.CaseData memory cd = LibCases.decodeCaseData(caseId);
updateTemperature(cd.bT, caseId); <((--- Updates temperature to 110
if (oracleFailure) return; <((--- Skips updating the ratio due to oracle failure
updateBeanToMaxLPRatio(cd.bL, caseId); <((--- This line is not executed
}
An attacker can exploit this behavior by inducing oracle failures at critical times, ensuring that only the temperature gets updated while the ratio remains stale. and can be happen by manipulating the data sources feeding the oracles or by causing disruptions that lead to oracle timeouts By doing so, the attacker can create discrepancies between the system's perceived state as indicated by the temperature and the actual state as represented by the unchanged ratio.
-here is a test :
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Weather Contract", function () {
let Weather;
let weather;
let owner;
beforeEach(async function () {
[owner] = await ethers.getSigners();
Weather = await ethers.getContractFactory("Weather");
weather = await Weather.deploy();
await weather.deployed();
});
it("Should update temperature but not ratio on oracle failure", async function () {
const initialTemp = await weather.getTemperature();
const initialRatio = await weather.getBeanToMaxLpGpPerBdvRatio();
const caseId = 1;
const bT = 10;
const bL = 5;
const oracleFailure = true;
await weather.updateTemperatureAndBeanToMaxLpGpPerBdvRatio(caseId, bT, bL, oracleFailure);
const finalTemp = await weather.getTemperature();
const finalRatio = await weather.getBeanToMaxLpGpPerBdvRatio();
expect(finalTemp).to.equal(initialTemp.add(bT));
expect(finalRatio).to.equal(initialRatio);
});
it("Should update both temperature and ratio when oracle does not fail", async function () {
const initialTemp = await weather.getTemperature();
const initialRatio = await weather.getBeanToMaxLpGpPerBdvRatio();
const caseId = 1;
const bT = 10;
const bL = 5;
const oracleFailure = false;
await weather.updateTemperatureAndBeanToMaxLpGpPerBdvRatio(caseId, bT, bL, oracleFailure);
const finalTemp = await weather.getTemperature();
const finalRatio = await weather.getBeanToMaxLpGpPerBdvRatio();
expect(finalTemp).to.equal(initialTemp.add(bT));
expect(finalRatio).to.equal(initialRatio.add(bL));
});
});
import random
MAX_BEAN_LP_GP_PER_BDV_RATIO = 100e18
state = {
"weather_temp": 100,
"bean_to_max_lp_gp_per_bdv_ratio": 50,
"season_current": 1
}
def update_temperature(bT, caseId):
global state
t = state["weather_temp"]
if bT < 0:
if t <= abs(bT):
bT = 1 - int(t)
state["weather_temp"] = 1
else:
state["weather_temp"] = t - abs(bT)
else:
state["weather_temp"] = t + bT
print(f"TemperatureChange: Season {state['season_current']}, CaseId {caseId}, Change {bT}")
def update_bean_to_max_lp_gp_per_bdv_ratio(bL, caseId):
global state
ratio = state["bean_to_max_lp_gp_per_bdv_ratio"]
if bL < 0:
if ratio <= abs(bL):
bL = -int(ratio)
state["bean_to_max_lp_gp_per_bdv_ratio"] = 0
else:
state["bean_to_max_lp_gp_per_bdv_ratio"] = ratio - abs(bL)
else:
if ratio + bL >= MAX_BEAN_LP_GP_PER_BDV_RATIO:
bL = int(MAX_BEAN_LP_GP_PER_BDV_RATIO - ratio)
state["bean_to_max_lp_gp_per_bdv_ratio"] = MAX_BEAN_LP_GP_PER_BDV_RATIO
else:
state["bean_to_max_lp_gp_per_bdv_ratio"] = ratio + bL
print(f"BeanToMaxLpGpPerBdvRatioChange: Season {state['season_current']}, CaseId {caseId}, Change {bL}")
def update_temperature_and_bean_to_max_lp_gp_per_bdv_ratio(caseId, bT, bL, oracle_failure):
update_temperature(bT, caseId)
if oracle_failure:
return
update_bean_to_max_lp_gp_per_bdv_ratio(bL, caseId)
def fuzz_test(num_tests=100):
for _ in range(num_tests):
caseId = random.randint(0, 100)
bT = random.randint(-50, 50)
bL = random.randint(-25, 25)
oracle_failure = random.choice([True, False])
print(f"\nTest Case - CaseId: {caseId}, bT: {bT}, bL: {bL}, Oracle Failure: {oracle_failure}")
update_temperature_and_bean_to_max_lp_gp_per_bdv_ratio(caseId, bT, bL, oracle_failure)
print(f"State after update: {state}")
fuzz_test()
Test Case - CaseId: 15, bT: -23, bL: -15, Oracle Failure: True
TemperatureChange: Season 1, CaseId 15, Change -23
State after update: {'weather_temp': 77, 'bean_to_max_lp_gp_per_bdv_ratio': 50, 'season_current': 1}
Test Case - CaseId: 45, bT: 10, bL: 20, Oracle Failure: False
TemperatureChange: Season 1, CaseId 45, Change 10
BeanToMaxLpGpPerBdvRatioChange: Season 1, CaseId 45, Change 20
State after update: {'weather_temp': 87, 'bean_to_max_lp_gp_per_bdv_ratio': 70, 'season_current': 1}
Impact
The discrepancy between the updated temperature and the unchanged ratio can lead to incorrect system behavior. For example, decisions based on temperature might not align with the ratio, causing mismatches in liquidity management .
Tools Used
manual review
Recommendations
it's need to log an event and take recovery actions when an oracle failure is detected.