Skip to content

Commit 088e953

Browse files
razwwclaude
andcommitted
feat(irm): accrue interest before changing borrow rate bounds
Changing a market's rate cap, rate floor, or fixed borrow rate used to take effect immediately, retroactively repricing all interest that had accrued since the market's last update at the new value. Both IRMs now call Moolah.accrueInterest for the market first, so elapsed interest is settled at the bounds that were actually in effect and only future interest uses the new ones. Affected entry points: - InterestRateModel: updateRateCap, updateRateFloor - FixedRateIrm: updateRateCap, updateRateFloor, setBorrowRate The accrual runs after the validation requires and before the storage write. Ordering matters: Moolah calls back into the IRM's borrowRate during accrual, so the callback must still observe the old value. Accrual is a no-op when the market does not exist yet (lastUpdate == 0), which keeps bounds settable ahead of market creation. FixedRateIrm needs this in particular, since setBorrowRate is documented as running before the market is created. FixedRateIrm held no reference to Moolah, so it gains a MOOLAH immutable and a constructor argument. This is a breaking constructor change: the new implementation must be deployed via the updated scripts rather than upgraded in place, and the address is baked into bytecode. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 4882d30 commit 088e953

9 files changed

Lines changed: 68 additions & 8 deletions

File tree

script/deploy_fixedRateIrm_impl.sol

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,16 @@ import { FixedRateIrm } from "interest-rate-model/FixedRateIrm.sol";
99
import { Moolah } from "moolah/Moolah.sol";
1010

1111
contract FixedRateIrmDeploy is DeployBase {
12+
address moolah = 0x8F73b65B4caAf64FBA2aF91cC5D4a2A1318E5D8C;
13+
1214
function run() public {
1315
uint256 deployerPrivateKey = _deployerKey();
1416
address deployer = vm.addr(deployerPrivateKey);
1517
console.log("Deployer: ", deployer);
1618
vm.startBroadcast(deployerPrivateKey);
1719

1820
// Deploy InterestRateModel implementation
19-
FixedRateIrm impl = new FixedRateIrm();
21+
FixedRateIrm impl = new FixedRateIrm(moolah);
2022
console.log("FixedRateIrm implementation: ", address(impl));
2123

2224
vm.stopBroadcast();

script/eth/deploy_fixedRateIrm.sol

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,16 @@ import { FixedRateIrm } from "interest-rate-model/FixedRateIrm.sol";
99
import { Moolah } from "moolah/Moolah.sol";
1010

1111
contract FixedRateIrmDeploy is DeployBase {
12+
address moolah = 0xf820fB4680712CD7263a0D3D024D5b5aEA82Fd70;
13+
1214
function run() public {
1315
uint256 deployerPrivateKey = _deployerKey();
1416
address deployer = vm.addr(deployerPrivateKey);
1517
console.log("Deployer: ", deployer);
1618
vm.startBroadcast(deployerPrivateKey);
1719

1820
// Deploy InterestRateModel implementation
19-
FixedRateIrm impl = new FixedRateIrm();
21+
FixedRateIrm impl = new FixedRateIrm(moolah);
2022
console.log("FixedRateIrm implementation: ", address(impl));
2123
// Deploy InterestRateModel proxy
2224
ERC1967Proxy proxy = new ERC1967Proxy(

src/interest-rate-model/FixedRateIrm.sol

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { IIrm } from "moolah/interfaces/IIrm.sol";
88
import { IFixedRateIrm } from "./interfaces/IFixedRateIrm.sol";
99

1010
import { MarketParamsLib } from "../moolah/libraries/MarketParamsLib.sol";
11-
import { Id, MarketParams, Market } from "moolah/interfaces/IMoolah.sol";
11+
import { IMoolah, Id, MarketParams, Market } from "moolah/interfaces/IMoolah.sol";
1212
import { ErrorsLib } from "./libraries/ErrorsLib.sol";
1313

1414
/* ERRORS */
@@ -44,6 +44,11 @@ contract FixedRateIrm is UUPSUpgradeable, AccessControlEnumerableUpgradeable, IF
4444
/// @inheritdoc IFixedRateIrm
4545
int256 public constant MAX_BORROW_RATE = 8.0 ether / int256(365 days);
4646

47+
/* IMMUTABLES */
48+
49+
/// @inheritdoc IFixedRateIrm
50+
address public immutable MOOLAH;
51+
4752
/* STORAGE */
4853

4954
/// @inheritdoc IFixedRateIrm
@@ -64,8 +69,11 @@ contract FixedRateIrm is UUPSUpgradeable, AccessControlEnumerableUpgradeable, IF
6469
/* CONSTRUCTOR */
6570

6671
/// @custom:oz-upgrades-unsafe-allow constructor
67-
constructor() {
72+
/// @param moolah The address of the Moolah contract.
73+
constructor(address moolah) {
74+
require(moolah != address(0), ErrorsLib.ZERO_ADDRESS);
6875
_disableInitializers();
76+
MOOLAH = moolah;
6977
}
7078

7179
/// @notice Constructor.
@@ -96,6 +104,9 @@ contract FixedRateIrm is UUPSUpgradeable, AccessControlEnumerableUpgradeable, IF
96104
require(newBorrowRate >= int256(rateFloor[id]), "rate below floor");
97105
}
98106

107+
// settle interest accrued at the old rate before it changes
108+
_accrueInterest(id);
109+
99110
borrowRateStored[id] = newBorrowRate;
100111

101112
emit SetBorrowRate(id, newBorrowRate);
@@ -107,6 +118,9 @@ contract FixedRateIrm is UUPSUpgradeable, AccessControlEnumerableUpgradeable, IF
107118
require(newRateCap >= minCap && newRateCap != oldCap && newRateCap <= uint256(MAX_BORROW_RATE), "invalid rate cap");
108119
require(rateFloor[id] <= newRateCap, "invalid new cap vs floor");
109120

121+
// settle interest accrued under the old cap before it changes
122+
_accrueInterest(id);
123+
110124
rateCap[id] = newRateCap;
111125

112126
emit BorrowRateCapUpdate(id, oldCap, newRateCap);
@@ -122,11 +136,22 @@ contract FixedRateIrm is UUPSUpgradeable, AccessControlEnumerableUpgradeable, IF
122136
if (_cap < minCap) _cap = minCap;
123137
require(newRateFloor <= _cap, "invalid rate floor vs cap");
124138

139+
// settle interest accrued under the old floor before it changes
140+
_accrueInterest(id);
141+
125142
rateFloor[id] = newRateFloor;
126143

127144
emit BorrowRateFloorUpdate(id, oldFloor, newRateFloor);
128145
}
129146

147+
/// @dev Accrues interest on Moolah for `id` so pending interest is settled at the current rate bounds.
148+
/// @dev No-op when the market does not exist yet, so bounds can still be pre-set before market creation.
149+
function _accrueInterest(Id id) private {
150+
if (IMoolah(MOOLAH).market(id).lastUpdate == 0) return;
151+
152+
IMoolah(MOOLAH).accrueInterest(IMoolah(MOOLAH).idToMarketParams(id));
153+
}
154+
130155
/// @dev Updates the minimum borrow rate cap for all markets.
131156
function updateMinCap(uint256 newMinCap) external onlyRole(MANAGER) {
132157
uint256 oldMinCap = minCap;

src/interest-rate-model/InterestRateModel.sol

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { ExpLib } from "./libraries/ExpLib.sol";
1313
import { MathLib, WAD_INT as WAD } from "./libraries/MathLib.sol";
1414
import { ConstantsLib } from "./libraries/ConstantsLib.sol";
1515
import { MarketParamsLib } from "../moolah/libraries/MarketParamsLib.sol";
16-
import { Id, MarketParams, Market } from "moolah/interfaces/IMoolah.sol";
16+
import { IMoolah, Id, MarketParams, Market } from "moolah/interfaces/IMoolah.sol";
1717
import { MathLib as MoolahMathLib } from "moolah/libraries/MathLib.sol";
1818

1919
/// @title InterestRateModel
@@ -204,6 +204,9 @@ contract InterestRateModel is UUPSUpgradeable, AccessControlEnumerableUpgradeabl
204204
require(newRateCap >= minCap && newRateCap != oldCap, "invalid rate cap");
205205
require(rateFloor[id] <= newRateCap, "invalid new cap vs floor");
206206

207+
// settle interest accrued under the old cap before it changes
208+
_accrueInterest(id);
209+
207210
rateCap[id] = newRateCap;
208211

209212
emit BorrowRateCapUpdate(id, oldCap, newRateCap);
@@ -219,11 +222,22 @@ contract InterestRateModel is UUPSUpgradeable, AccessControlEnumerableUpgradeabl
219222
if (_cap < minCap) _cap = minCap;
220223
require(newRateFloor <= _cap, "invalid rate floor vs cap");
221224

225+
// settle interest accrued under the old floor before it changes
226+
_accrueInterest(id);
227+
222228
rateFloor[id] = newRateFloor;
223229

224230
emit BorrowRateFloorUpdate(id, oldFloor, newRateFloor);
225231
}
226232

233+
/// @dev Accrues interest on Moolah for `id` so pending interest is settled at the current rate bounds.
234+
/// @dev No-op when the market does not exist yet, so bounds can still be pre-set before market creation.
235+
function _accrueInterest(Id id) private {
236+
if (IMoolah(MOOLAH).market(id).lastUpdate == 0) return;
237+
238+
IMoolah(MOOLAH).accrueInterest(IMoolah(MOOLAH).idToMarketParams(id));
239+
}
240+
227241
/// @dev Updates the minimum borrow rate cap for all markets.
228242
function updateMinCap(uint256 newMinCap) external onlyRole(MANAGER) {
229243
uint256 oldMinCap = minCap;

src/interest-rate-model/interfaces/IFixedRateIrm.sol

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ interface IFixedRateIrm is IIrm {
1313

1414
/* EXTERNAL */
1515

16+
/// @notice The address of the Moolah contract.
17+
function MOOLAH() external view returns (address);
18+
1619
/// @notice Max settable borrow rate (800%).
1720
function MAX_BORROW_RATE() external returns (int256);
1821

test/interest-rate-model/AlphaIrmTest.sol

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ contract AlpahIrmMainnetTest is Test {
1313
address admin = 0x07D274a68393E8b8a2CCf19A2ce4Ba3518735253; // timelock
1414
address manager = 0x8d388136d578dCD791D081c6042284CED6d9B0c6;
1515
address alphaIrm = 0x5F9f9173B405C6CEAfa7f98d09e4B8447e9797E6;
16+
address moolah = 0x8F73b65B4caAf64FBA2aF91cC5D4a2A1318E5D8C;
1617

1718
FixedRateIrm fixedRateIrm = FixedRateIrm(alphaIrm);
1819

@@ -21,7 +22,7 @@ contract AlpahIrmMainnetTest is Test {
2122
function setUp() external {
2223
vm.createSelectFork("https://bsc-dataseed.bnbchain.org");
2324

24-
address impl = address(new FixedRateIrm());
25+
address impl = address(new FixedRateIrm(moolah));
2526

2627
vm.startPrank(admin);
2728
UUPSUpgradeable proxy = UUPSUpgradeable(alphaIrm);

test/interest-rate-model/FixedRateIrmTest.sol

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ contract FixedRateIrmTest is Test {
1818
address bot = makeAddr("bot");
1919

2020
function setUp() external {
21-
FixedRateIrm impl = new FixedRateIrm();
21+
// this contract stands in as MOOLAH; see the `market` stub below
22+
FixedRateIrm impl = new FixedRateIrm(address(this));
2223
ERC1967Proxy proxy = new ERC1967Proxy(
2324
address(impl),
2425
abi.encodeWithSelector(impl.initialize.selector, admin, manager)
@@ -144,4 +145,10 @@ contract FixedRateIrmTest is Test {
144145

145146
assertEq(fixedRateIrm.borrowRateView(marketPrams, market), uint256(newBorrowRate));
146147
}
148+
149+
/* MOOLAH STUB */
150+
151+
/// @dev This contract is deployed as the IRM's MOOLAH. `updateRateCap`/`updateRateFloor` probe the market
152+
/// before accruing interest; returning an empty market (lastUpdate == 0) makes the accrual a no-op.
153+
function market(Id) external pure returns (Market memory m) {}
147154
}

test/interest-rate-model/InterestRateModelTest.sol

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,12 @@ contract InterestRateModelTest is Test {
380380
assertEq(irm.rateFloor(id), newFloor);
381381
}
382382

383+
/* MOOLAH STUB */
384+
385+
/// @dev This contract is deployed as the IRM's MOOLAH. `updateRateCap`/`updateRateFloor` probe the market
386+
/// before accruing interest; returning an empty market (lastUpdate == 0) makes the accrual a no-op.
387+
function market(Id) external pure returns (Market memory m) {}
388+
383389
/* HANDLERS */
384390

385391
function handleBorrowRate(uint256 totalSupplyAssets, uint256 totalBorrowAssets, uint256 elapsed) external {

test/utils/PositionManager.t.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ contract PositionManagerTest is Test {
192192
);
193193
irm = InterestRateModel(address(irmProxy));
194194

195-
FixedRateIrm fixedIrmImpl = new FixedRateIrm();
195+
FixedRateIrm fixedIrmImpl = new FixedRateIrm(address(moolah));
196196
ERC1967Proxy fixedIrmProxy = new ERC1967Proxy(
197197
address(fixedIrmImpl),
198198
abi.encodeWithSelector(FixedRateIrm.initialize.selector, admin, manager)

0 commit comments

Comments
 (0)