-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathScaleFactorOverflow.t.sol
More file actions
192 lines (164 loc) · 8.12 KB
/
ScaleFactorOverflow.t.sol
File metadata and controls
192 lines (164 loc) · 8.12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "forge-std/console.sol";
// Minimal interfaces to reproduce the vulnerability
// We don't need the full protocol — just the math
uint256 constant RAY = 1e27;
uint256 constant HALF_RAY = 0.5e27;
uint256 constant BIP_RAY_RATIO = 1e23;
uint256 constant SECONDS_IN_365_DAYS = 365 days;
library MathUtils {
function rayMul(uint256 a, uint256 b) internal pure returns (uint256 c) {
assembly {
if iszero(or(iszero(b), iszero(gt(a, div(sub(not(0), HALF_RAY), b))))) {
mstore(0, 0x4e487b71)
mstore(0x20, 0x11)
revert(0x1c, 0x24)
}
c := div(add(mul(a, b), HALF_RAY), RAY)
}
}
function toUint112(uint256 x) internal pure returns (uint112 y) {
require(x == (y = uint112(x)), "uint112 overflow");
}
}
/// @title ScaleFactorOverflow PoC
/// @notice Demonstrates that scaleFactor (uint112) overflows after ~7.8 years
/// of delinquency at max allowed parameters (100% APR + 100% DQ fee),
/// permanently bricking the market with no recovery mechanism.
contract ScaleFactorOverflowTest is Test {
using MathUtils for uint256;
/// @dev Reproduces the exact logic from FeeMath.updateScaleFactorAndFees
function updateScaleFactor(
uint112 currentScaleFactor,
uint256 annualInterestBips,
uint256 delinquencyFeeBips,
uint256 timeDelta
) internal pure returns (uint112) {
// calculateLinearInterestFromBips for base interest
uint256 baseInterestRay = (annualInterestBips * BIP_RAY_RATIO * timeDelta) / SECONDS_IN_365_DAYS;
// calculateLinearInterestFromBips for delinquency fee
// Assuming fully delinquent past grace period, timeWithPenalty = timeDelta
uint256 delinquencyFeeRay = (delinquencyFeeBips * BIP_RAY_RATIO * timeDelta) / SECONDS_IN_365_DAYS;
// updateScaleFactorAndFees logic (FeeMath.sol:168-171)
uint256 prevScaleFactor = currentScaleFactor;
uint256 scaleFactorDelta = prevScaleFactor.rayMul(baseInterestRay + delinquencyFeeRay);
return (prevScaleFactor + scaleFactorDelta).toUint112();
}
/// @notice Main PoC: shows scaleFactor overflow after ~7.8 years
function test_ScaleFactorOverflow_BricksMarket() public {
// --- Setup: Market parameters at max allowed values ---
uint112 scaleFactor = uint112(RAY); // starts at 1e27 (1.0)
uint256 annualInterestBips = 10000; // 100% APR (max allowed by factory)
uint256 delinquencyFeeBips = 10000; // 100% delinquency fee (max allowed)
// delinquencyGracePeriod = 0 (immediate penalties)
// Borrower has borrowed everything and never repays → fully delinquent
uint256 updateInterval = 1 days; // Daily state updates (realistic)
uint256 totalTime = 0;
uint256 year = 0;
console.log("=== scaleFactor Overflow PoC ===");
console.log("");
console.log("Parameters:");
console.log(" annualInterestBips: 10000 (100%)");
console.log(" delinquencyFeeBips: 10000 (100%)");
console.log(" delinquencyGracePeriod: 0");
console.log(" Effective combined rate: 200% per year");
console.log(" Starting scaleFactor: 1e27 (RAY)");
console.log(" uint112 max: ~5.19e33");
console.log("");
// --- Simulate daily interest accrual ---
while (true) {
// Try to update scaleFactor — this will eventually revert
try this.tryUpdateScaleFactor(scaleFactor, annualInterestBips, delinquencyFeeBips, updateInterval) returns (uint112 newSF) {
scaleFactor = newSF;
totalTime += updateInterval;
// Log yearly progress
if (totalTime >= (year + 1) * 365 days) {
year++;
console.log(" Year %d: scaleFactor = %e", year, uint256(scaleFactor));
}
} catch {
// scaleFactor overflow — market is bricked!
console.log("");
console.log("*** OVERFLOW at day %d (year %d) ***", totalTime / 1 days, year + 1);
console.log("*** scaleFactor exceeded uint112 max ***");
console.log("");
console.log("IMPACT: All market functions now revert permanently:");
console.log(" - deposit() -> REVERT");
console.log(" - queueWithdrawal() -> REVERT");
console.log(" - executeWithdrawal() -> REVERT");
console.log(" - borrow() -> REVERT");
console.log(" - repay() -> REVERT");
console.log(" - closeMarket() -> REVERT");
console.log(" - updateState() -> REVERT");
console.log(" - setAnnualInterestBips() -> REVERT");
console.log("");
console.log("All funds in the market contract are PERMANENTLY LOCKED.");
console.log("There is NO admin override or recovery mechanism.");
// Verify it actually reverts
vm.expectRevert();
this.tryUpdateScaleFactor(scaleFactor, annualInterestBips, delinquencyFeeBips, updateInterval);
return; // Test passes — we proved the overflow
}
// Safety: don't loop forever
if (totalTime > 10 * 365 days) {
revert("Should have overflowed by now");
}
}
}
/// @notice External wrapper to use try/catch
function tryUpdateScaleFactor(
uint112 sf,
uint256 interest,
uint256 dqFee,
uint256 timeDelta
) external pure returns (uint112) {
return updateScaleFactor(sf, interest, dqFee, timeDelta);
}
/// @notice Shows that even moderate parameters eventually overflow
function test_ModerateParameters_StillOverflow() public {
uint112 scaleFactor = uint112(RAY);
uint256 annualInterestBips = 5000; // 50% APR
uint256 delinquencyFeeBips = 5000; // 50% delinquency fee
uint256 updateInterval = 1 days;
uint256 totalTime = 0;
console.log("=== Moderate Parameters (50% + 50%) ===");
while (true) {
try this.tryUpdateScaleFactor(scaleFactor, annualInterestBips, delinquencyFeeBips, updateInterval) returns (uint112 newSF) {
scaleFactor = newSF;
totalTime += updateInterval;
} catch {
uint256 numYears = totalTime / 365 days;
console.log(" Overflow after %d years with 50%% APR + 50%% DQ fee", numYears);
return;
}
if (totalTime > 50 * 365 days) {
revert("Should have overflowed within 50 years");
}
}
}
/// @notice Shows there is no cap or recovery mechanism
function test_NoCap_NoRecovery() public {
// The scaleFactor has NO upper bound check
// The only "protection" is the uint112 safe cast which reverts
// But reverting is NOT protection — it's a permanent brick
uint112 scaleFactor = uint112(RAY);
uint256 almostMax = uint256(type(uint112).max) - RAY;
// Simulate a scaleFactor very close to max
// In a real scenario, this state exists right before the brick
scaleFactor = uint112(almostMax);
console.log("=== No Recovery Mechanism ===");
console.log(" scaleFactor near uint112 max: %e", uint256(scaleFactor));
// The next interest accrual will overflow
// Can the borrower repay to stop it? NO — repay() calls _getUpdatedState()
// which calls updateScaleFactorAndFees() which reverts.
vm.expectRevert();
this.tryUpdateScaleFactor(scaleFactor, 1, 0, 1 days); // Even 0.01% APR reverts
console.log(" Even 0.01%% APR for 1 day causes revert at this scaleFactor");
console.log(" repay() would also revert because it calls _getUpdatedState()");
console.log(" setAnnualInterestBips(0) would also revert");
console.log(" closeMarket() would also revert");
console.log(" -> NO RECOVERY POSSIBLE");
}
}