Skip to content

Commit 31fe8eb

Browse files
Merge pull request #336 from euler-xyz/irm-cyclical-binary
add fixed cyclical binary irm
2 parents 8bd7c28 + d91ef58 commit 31fe8eb

File tree

4 files changed

+258
-0
lines changed

4 files changed

+258
-0
lines changed

src/IRM/IRMFixedCyclicalBinary.sol

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
3+
pragma solidity ^0.8.0;
4+
5+
import {IIRM} from "evk/InterestRateModels/IIRM.sol";
6+
7+
/// @title IRMFixedCyclicalBinary
8+
/// @custom:security-contact [email protected]
9+
/// @author Euler Labs (https://www.eulerlabs.com/)
10+
/// @notice Implementation of an interest rate model, where interest rate cycles between two fixed values,
11+
contract IRMFixedCyclicalBinary is IIRM {
12+
/// @notice Interest rate applied during the first part of the cycle
13+
uint256 public immutable primaryRate;
14+
/// @notice Interest rate applied during the second part of the cycle
15+
uint256 public immutable secondaryRate;
16+
/// @notice Duration of the primary part of the cycle in seconds
17+
uint256 public immutable primaryDuration;
18+
/// @notice Duration of the secondary part of the cycle in seconds
19+
uint256 public immutable secondaryDuration;
20+
/// @notice Timestamp of the start of the first cycle
21+
uint256 public immutable startTimestamp;
22+
23+
/// @notice Error thrown when start timestamp is in the future
24+
error BadStartTimestamp();
25+
/// @notice Error thrown when duration of either primary or secondary part of the cycle is zero
26+
/// or when the whole cycle duration overflows uint
27+
error BadDuration();
28+
29+
/// @notice Creates a fixed cyclical binary interest rate model
30+
/// @param primaryRate_ Interest rate applied during the first part of the cycle
31+
/// @param secondaryRate_ Interest rate applied during the second part of the cycle
32+
/// @param primaryDuration_ Duration of the primary part of the cycle in seconds
33+
/// @param secondaryDuration_ Duration of the secondary part of the cycle in seconds
34+
/// @param startTimestamp_ Timestamp of the start of the first cycle
35+
constructor(
36+
uint256 primaryRate_,
37+
uint256 secondaryRate_,
38+
uint256 primaryDuration_,
39+
uint256 secondaryDuration_,
40+
uint256 startTimestamp_
41+
) {
42+
if (startTimestamp_ > block.timestamp) revert BadStartTimestamp();
43+
if (
44+
primaryDuration_ == 0 || secondaryDuration_ == 0
45+
|| (type(uint256).max - primaryDuration_ < secondaryDuration_)
46+
) revert BadDuration();
47+
48+
primaryRate = primaryRate_;
49+
secondaryRate = secondaryRate_;
50+
primaryDuration = primaryDuration_;
51+
secondaryDuration = secondaryDuration_;
52+
startTimestamp = startTimestamp_;
53+
}
54+
55+
/// @inheritdoc IIRM
56+
function computeInterestRate(address vault, uint256, uint256) external view override returns (uint256) {
57+
if (msg.sender != vault) revert E_IRMUpdateUnauthorized();
58+
59+
return computeInterestRateInternal();
60+
}
61+
62+
/// @inheritdoc IIRM
63+
function computeInterestRateView(address, uint256, uint256) external view override returns (uint256) {
64+
return computeInterestRateInternal();
65+
}
66+
67+
function computeInterestRateInternal() internal view returns (uint256) {
68+
uint256 timeSinceStart = block.timestamp - startTimestamp;
69+
70+
return timeSinceStart % (primaryDuration + secondaryDuration) <= primaryDuration ? primaryRate : secondaryRate;
71+
}
72+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
3+
pragma solidity ^0.8.0;
4+
5+
import {BaseFactory} from "../BaseFactory/BaseFactory.sol";
6+
import {IRMFixedCyclicalBinary} from "../IRM/IRMFixedCyclicalBinary.sol";
7+
import {IEulerFixedCyclicalBinaryIRMFactory} from "./interfaces/IEulerFixedCyclicalBinaryIRMFactory.sol";
8+
9+
/// @title EulerFixedCyclicalBinaryIRMFactory
10+
/// @custom:security-contact [email protected]
11+
/// @author Euler Labs (https://www.eulerlabs.com/)
12+
/// @notice A minimal factory for Fixed Cyclical Binary IRMs.
13+
contract EulerFixedCyclicalBinaryIRMFactory is BaseFactory, IEulerFixedCyclicalBinaryIRMFactory {
14+
// corresponds to 1000% APY
15+
uint256 internal constant MAX_ALLOWED_INTEREST_RATE = 75986279153383989049;
16+
17+
/// @notice Error thrown when the computed interest rate exceeds the maximum allowed limit.
18+
error IRMFactory_ExcessiveInterestRate();
19+
20+
/// @notice Deploys a new IRMFixedCyclicalBinary.
21+
/// @param primaryRate Interest rate applied during the first part of the cycle
22+
/// @param secondaryRate Interest rate applied during the second part of the cycle
23+
/// @param primaryDuration Duration of the primary part of the cycle in seconds
24+
/// @param secondaryDuration Duration of the secondary part of the cycle in seconds
25+
/// @param startTimestamp Timestamp of the start of the first cycle
26+
/// @return The deployment address.
27+
function deploy(
28+
uint256 primaryRate,
29+
uint256 secondaryRate,
30+
uint256 primaryDuration,
31+
uint256 secondaryDuration,
32+
uint256 startTimestamp
33+
) external override returns (address) {
34+
if (primaryRate > MAX_ALLOWED_INTEREST_RATE || secondaryRate > MAX_ALLOWED_INTEREST_RATE) {
35+
revert IRMFactory_ExcessiveInterestRate();
36+
}
37+
38+
IRMFixedCyclicalBinary irm =
39+
new IRMFixedCyclicalBinary(primaryRate, secondaryRate, primaryDuration, secondaryDuration, startTimestamp);
40+
41+
deploymentInfo[address(irm)] = DeploymentInfo(msg.sender, uint96(block.timestamp));
42+
deployments.push(address(irm));
43+
emit ContractDeployed(address(irm), msg.sender, block.timestamp);
44+
return address(irm);
45+
}
46+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
3+
pragma solidity >=0.8.0;
4+
5+
import {IFactory} from "../../BaseFactory/interfaces/IFactory.sol";
6+
7+
/// @title IEulerFixedCyclicalBinaryRMFactory
8+
/// @custom:security-contact [email protected]
9+
/// @author Euler Labs (https://www.eulerlabs.com/)
10+
/// @notice A minimal factory for EulerFixedCyclicalBinaryIRM.
11+
interface IEulerFixedCyclicalBinaryIRMFactory is IFactory {
12+
/// @notice Deploys a new IRMFixedCyclicalBinary.
13+
/// @param primaryRate Interest rate applied during the first part of the cycle
14+
/// @param secondaryRate Interest rate applied during the second part of the cycle
15+
/// @param primaryDuration Duration of the primary part of the cycle in seconds
16+
/// @param secondaryDuration Duration of the secondary part of the cycle in seconds
17+
/// @param startTimestamp Timestamp of the start of the first cycle
18+
/// @return The deployment address.
19+
function deploy(
20+
uint256 primaryRate,
21+
uint256 secondaryRate,
22+
uint256 primaryDuration,
23+
uint256 secondaryDuration,
24+
uint256 startTimestamp
25+
) external returns (address);
26+
}

test/IRM/IRMFixedCyclicalBinary.t.sol

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
pragma solidity ^0.8.13;
3+
4+
import {Test} from "forge-std/Test.sol";
5+
import {IIRM} from "evk/InterestRateModels/IIRM.sol";
6+
import {IRMFixedCyclicalBinary} from "../../src/IRM/IRMFixedCyclicalBinary.sol";
7+
import {EulerFixedCyclicalBinaryIRMFactory} from "../../src/IRMFactory/EulerFixedCyclicalBinaryIRMFactory.sol";
8+
import {MathTesting} from "../utils/MathTesting.sol";
9+
10+
import {console} from "forge-std/console.sol";
11+
12+
contract IRMFixedCyclicalBinaryTest is Test, MathTesting {
13+
IRMFixedCyclicalBinary irm;
14+
EulerFixedCyclicalBinaryIRMFactory factory;
15+
16+
uint256 constant PRIMARY_RATE = 1;
17+
uint256 constant SECONDARY_RATE = 2;
18+
uint256 constant PRIMARY_DURATION = 28 days;
19+
uint256 constant SECONDARY_DURATION = 2 days;
20+
21+
// corresponds to 1000% APY
22+
uint256 internal constant MAX_ALLOWED_INTEREST_RATE = 75986279153383989049;
23+
24+
function setUp() public {
25+
irm = new IRMFixedCyclicalBinary(
26+
PRIMARY_RATE, SECONDARY_RATE, PRIMARY_DURATION, SECONDARY_DURATION, block.timestamp
27+
);
28+
29+
factory = new EulerFixedCyclicalBinaryIRMFactory();
30+
}
31+
32+
function test_OnlyVaultCanMutateIRMState() public {
33+
vm.expectRevert(IIRM.E_IRMUpdateUnauthorized.selector);
34+
irm.computeInterestRate(address(1234), 5, 6);
35+
36+
vm.prank(address(1234));
37+
irm.computeInterestRate(address(1234), 5, 6);
38+
}
39+
40+
function test_ExpectRevertStartTimeInFuture() public {
41+
vm.expectRevert(IRMFixedCyclicalBinary.BadStartTimestamp.selector);
42+
new IRMFixedCyclicalBinary(
43+
PRIMARY_RATE, SECONDARY_RATE, PRIMARY_DURATION, SECONDARY_DURATION, block.timestamp + 1
44+
);
45+
}
46+
47+
function test_ExpectRevertPrimaryDurationZero() public {
48+
vm.expectRevert(IRMFixedCyclicalBinary.BadDuration.selector);
49+
new IRMFixedCyclicalBinary(PRIMARY_RATE, SECONDARY_RATE, 0, SECONDARY_DURATION, block.timestamp);
50+
}
51+
52+
function test_ExpectRevertSecondaryDurationZero() public {
53+
vm.expectRevert(IRMFixedCyclicalBinary.BadDuration.selector);
54+
new IRMFixedCyclicalBinary(PRIMARY_RATE, SECONDARY_RATE, PRIMARY_DURATION, 0, block.timestamp);
55+
}
56+
57+
function test_ExpectRevertCycleDurationOverflows() public {
58+
vm.expectRevert(IRMFixedCyclicalBinary.BadDuration.selector);
59+
new IRMFixedCyclicalBinary(PRIMARY_RATE, SECONDARY_RATE, type(uint256).max - 1, 2, block.timestamp);
60+
}
61+
62+
function test_RateAtDeployment() public view {
63+
assertEq(getIr(), PRIMARY_RATE);
64+
}
65+
66+
function test_RateAtEndOfPrimaryPeriod() public {
67+
vm.warp(block.timestamp + PRIMARY_DURATION - 1);
68+
assertEq(getIr(), PRIMARY_RATE);
69+
70+
vm.warp(block.timestamp + 1);
71+
assertEq(getIr(), PRIMARY_RATE);
72+
73+
vm.warp(block.timestamp + 1);
74+
assertEq(getIr(), SECONDARY_RATE);
75+
}
76+
77+
function test_RateAtEndOfCycle() public {
78+
vm.warp(block.timestamp + PRIMARY_DURATION + SECONDARY_DURATION - 1);
79+
assertEq(getIr(), SECONDARY_RATE);
80+
81+
vm.warp(block.timestamp + 1);
82+
assertEq(getIr(), PRIMARY_RATE);
83+
}
84+
85+
function test_CycleRates(uint256 timeElapsed, uint256 primaryDuration, uint256 secondaryDuration) public {
86+
timeElapsed = bound(timeElapsed, 0, 1000 days);
87+
primaryDuration = bound(primaryDuration, 1, 100 days);
88+
secondaryDuration = bound(secondaryDuration, 1, 100 days);
89+
90+
irm =
91+
IRMFixedCyclicalBinary(factory.deploy(PRIMARY_RATE, SECONDARY_RATE, primaryDuration, secondaryDuration, 0));
92+
93+
vm.warp(timeElapsed);
94+
95+
uint cyclesElapsed = timeElapsed / (primaryDuration + secondaryDuration);
96+
97+
if (timeElapsed - cyclesElapsed * (primaryDuration + secondaryDuration) <= primaryDuration) {
98+
assertEq(getIr(), PRIMARY_RATE);
99+
} else {
100+
assertEq(getIr(), SECONDARY_RATE);
101+
}
102+
}
103+
104+
function test_ExpectRevertFactoryRateTooHigh() public {
105+
vm.expectRevert(EulerFixedCyclicalBinaryIRMFactory.IRMFactory_ExcessiveInterestRate.selector);
106+
factory.deploy(MAX_ALLOWED_INTEREST_RATE + 1, SECONDARY_RATE, PRIMARY_DURATION, SECONDARY_DURATION, 0);
107+
vm.expectRevert(EulerFixedCyclicalBinaryIRMFactory.IRMFactory_ExcessiveInterestRate.selector);
108+
factory.deploy(PRIMARY_RATE, MAX_ALLOWED_INTEREST_RATE + 1, PRIMARY_DURATION, SECONDARY_DURATION, 0);
109+
}
110+
111+
function getIr() private view returns (uint256) {
112+
return irm.computeInterestRateView(address(1), 0, 0);
113+
}
114+
}

0 commit comments

Comments
 (0)