Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions src/IRM/IRMFixedCyclicalBinary.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// SPDX-License-Identifier: GPL-2.0-or-later

pragma solidity ^0.8.0;

import {IIRM} from "evk/InterestRateModels/IIRM.sol";

/// @title IRMFixedCyclicalBinary
/// @custom:security-contact [email protected]
/// @author Euler Labs (https://www.eulerlabs.com/)
/// @notice Implementation of an interest rate model, where interest rate cycles between two fixed values,
contract IRMFixedCyclicalBinary is IIRM {
/// @notice Interest rate applied during the first part of the cycle
uint256 public immutable primaryRate;
/// @notice Interest rate applied during the second part of the cycle
uint256 public immutable secondaryRate;
/// @notice Duration of the primary part of the cycle in seconds
uint256 public immutable primaryDuration;
/// @notice Duration of the secondary part of the cycle in seconds
uint256 public immutable secondaryDuration;
/// @notice Timestamp of the start of the first cycle
uint256 public immutable startTimestamp;

/// @notice Error thrown when start timestamp is in the future
error BadStartTimestamp();
/// @notice Error thrown when duration of either primary or secondary part of the cycle is zero
/// or when the whole cycle duration overflows uint
error BadDuration();

/// @notice Creates a fixed cyclical binary interest rate model
/// @param primaryRate_ Interest rate applied during the first part of the cycle
/// @param secondaryRate_ Interest rate applied during the second part of the cycle
/// @param primaryDuration_ Duration of the primary part of the cycle in seconds
/// @param secondaryDuration_ Duration of the secondary part of the cycle in seconds
/// @param startTimestamp_ Timestamp of the start of the first cycle
constructor(
uint256 primaryRate_,
uint256 secondaryRate_,
uint256 primaryDuration_,
uint256 secondaryDuration_,
uint256 startTimestamp_
) {
if (startTimestamp_ > block.timestamp) revert BadStartTimestamp();
if (
primaryDuration_ == 0 || secondaryDuration_ == 0
|| (type(uint256).max - primaryDuration_ < secondaryDuration_)
) revert BadDuration();

primaryRate = primaryRate_;
secondaryRate = secondaryRate_;
primaryDuration = primaryDuration_;
secondaryDuration = secondaryDuration_;
startTimestamp = startTimestamp_;
}

/// @inheritdoc IIRM
function computeInterestRate(address vault, uint256, uint256) external view override returns (uint256) {
if (msg.sender != vault) revert E_IRMUpdateUnauthorized();

return computeInterestRateInternal();
}

/// @inheritdoc IIRM
function computeInterestRateView(address, uint256, uint256) external view override returns (uint256) {
return computeInterestRateInternal();
}

function computeInterestRateInternal() internal view returns (uint256) {
uint256 timeSinceStart = block.timestamp - startTimestamp;

return timeSinceStart % (primaryDuration + secondaryDuration) <= primaryDuration ? primaryRate : secondaryRate;
}
}
46 changes: 46 additions & 0 deletions src/IRMFactory/EulerFixedCyclicalBinaryIRMFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: GPL-2.0-or-later

pragma solidity ^0.8.0;

import {BaseFactory} from "../BaseFactory/BaseFactory.sol";
import {IRMFixedCyclicalBinary} from "../IRM/IRMFixedCyclicalBinary.sol";
import {IEulerFixedCyclicalBinaryIRMFactory} from "./interfaces/IEulerFixedCyclicalBinaryIRMFactory.sol";

/// @title EulerFixedCyclicalBinaryIRMFactory
/// @custom:security-contact [email protected]
/// @author Euler Labs (https://www.eulerlabs.com/)
/// @notice A minimal factory for Fixed Cyclical Binary IRMs.
contract EulerFixedCyclicalBinaryIRMFactory is BaseFactory, IEulerFixedCyclicalBinaryIRMFactory {
// corresponds to 1000% APY
uint256 internal constant MAX_ALLOWED_INTEREST_RATE = 75986279153383989049;

/// @notice Error thrown when the computed interest rate exceeds the maximum allowed limit.
error IRMFactory_ExcessiveInterestRate();

/// @notice Deploys a new IRMFixedCyclicalBinary.
/// @param primaryRate Interest rate applied during the first part of the cycle
/// @param secondaryRate Interest rate applied during the second part of the cycle
/// @param primaryDuration Duration of the primary part of the cycle in seconds
/// @param secondaryDuration Duration of the secondary part of the cycle in seconds
/// @param startTimestamp Timestamp of the start of the first cycle
/// @return The deployment address.
function deploy(
uint256 primaryRate,
uint256 secondaryRate,
uint256 primaryDuration,
uint256 secondaryDuration,
uint256 startTimestamp
) external override returns (address) {
if (primaryRate > MAX_ALLOWED_INTEREST_RATE || secondaryRate > MAX_ALLOWED_INTEREST_RATE) {
revert IRMFactory_ExcessiveInterestRate();
}

IRMFixedCyclicalBinary irm =
new IRMFixedCyclicalBinary(primaryRate, secondaryRate, primaryDuration, secondaryDuration, startTimestamp);

deploymentInfo[address(irm)] = DeploymentInfo(msg.sender, uint96(block.timestamp));
deployments.push(address(irm));
emit ContractDeployed(address(irm), msg.sender, block.timestamp);
return address(irm);
}
}
26 changes: 26 additions & 0 deletions src/IRMFactory/interfaces/IEulerFixedCyclicalBinaryIRMFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: GPL-2.0-or-later

pragma solidity >=0.8.0;

import {IFactory} from "../../BaseFactory/interfaces/IFactory.sol";

/// @title IEulerFixedCyclicalBinaryRMFactory
/// @custom:security-contact [email protected]
/// @author Euler Labs (https://www.eulerlabs.com/)
/// @notice A minimal factory for EulerFixedCyclicalBinaryIRM.
interface IEulerFixedCyclicalBinaryIRMFactory is IFactory {
/// @notice Deploys a new IRMFixedCyclicalBinary.
/// @param primaryRate Interest rate applied during the first part of the cycle
/// @param secondaryRate Interest rate applied during the second part of the cycle
/// @param primaryDuration Duration of the primary part of the cycle in seconds
/// @param secondaryDuration Duration of the secondary part of the cycle in seconds
/// @param startTimestamp Timestamp of the start of the first cycle
/// @return The deployment address.
function deploy(
uint256 primaryRate,
uint256 secondaryRate,
uint256 primaryDuration,
uint256 secondaryDuration,
uint256 startTimestamp
) external returns (address);
}
114 changes: 114 additions & 0 deletions test/IRM/IRMFixedCyclicalBinary.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.13;

import {Test} from "forge-std/Test.sol";
import {IIRM} from "evk/InterestRateModels/IIRM.sol";
import {IRMFixedCyclicalBinary} from "../../src/IRM/IRMFixedCyclicalBinary.sol";
import {EulerFixedCyclicalBinaryIRMFactory} from "../../src/IRMFactory/EulerFixedCyclicalBinaryIRMFactory.sol";
import {MathTesting} from "../utils/MathTesting.sol";

import {console} from "forge-std/console.sol";

contract IRMFixedCyclicalBinaryTest is Test, MathTesting {
IRMFixedCyclicalBinary irm;
EulerFixedCyclicalBinaryIRMFactory factory;

uint256 constant PRIMARY_RATE = 1;
uint256 constant SECONDARY_RATE = 2;
uint256 constant PRIMARY_DURATION = 28 days;
uint256 constant SECONDARY_DURATION = 2 days;

// corresponds to 1000% APY
uint256 internal constant MAX_ALLOWED_INTEREST_RATE = 75986279153383989049;

function setUp() public {
irm = new IRMFixedCyclicalBinary(
PRIMARY_RATE, SECONDARY_RATE, PRIMARY_DURATION, SECONDARY_DURATION, block.timestamp
);

factory = new EulerFixedCyclicalBinaryIRMFactory();
}

function test_OnlyVaultCanMutateIRMState() public {
vm.expectRevert(IIRM.E_IRMUpdateUnauthorized.selector);
irm.computeInterestRate(address(1234), 5, 6);

vm.prank(address(1234));
irm.computeInterestRate(address(1234), 5, 6);
}

function test_ExpectRevertStartTimeInFuture() public {
vm.expectRevert(IRMFixedCyclicalBinary.BadStartTimestamp.selector);
new IRMFixedCyclicalBinary(
PRIMARY_RATE, SECONDARY_RATE, PRIMARY_DURATION, SECONDARY_DURATION, block.timestamp + 1
);
}

function test_ExpectRevertPrimaryDurationZero() public {
vm.expectRevert(IRMFixedCyclicalBinary.BadDuration.selector);
new IRMFixedCyclicalBinary(PRIMARY_RATE, SECONDARY_RATE, 0, SECONDARY_DURATION, block.timestamp);
}

function test_ExpectRevertSecondaryDurationZero() public {
vm.expectRevert(IRMFixedCyclicalBinary.BadDuration.selector);
new IRMFixedCyclicalBinary(PRIMARY_RATE, SECONDARY_RATE, PRIMARY_DURATION, 0, block.timestamp);
}

function test_ExpectRevertCycleDurationOverflows() public {
vm.expectRevert(IRMFixedCyclicalBinary.BadDuration.selector);
new IRMFixedCyclicalBinary(PRIMARY_RATE, SECONDARY_RATE, type(uint256).max - 1, 2, block.timestamp);
}

function test_RateAtDeployment() public view {
assertEq(getIr(), PRIMARY_RATE);
}

function test_RateAtEndOfPrimaryPeriod() public {
vm.warp(block.timestamp + PRIMARY_DURATION - 1);
assertEq(getIr(), PRIMARY_RATE);

vm.warp(block.timestamp + 1);
assertEq(getIr(), PRIMARY_RATE);

vm.warp(block.timestamp + 1);
assertEq(getIr(), SECONDARY_RATE);
}

function test_RateAtEndOfCycle() public {
vm.warp(block.timestamp + PRIMARY_DURATION + SECONDARY_DURATION - 1);
assertEq(getIr(), SECONDARY_RATE);

vm.warp(block.timestamp + 1);
assertEq(getIr(), PRIMARY_RATE);
}

function test_CycleRates(uint256 timeElapsed, uint256 primaryDuration, uint256 secondaryDuration) public {
timeElapsed = bound(timeElapsed, 0, 1000 days);
primaryDuration = bound(primaryDuration, 1, 100 days);
secondaryDuration = bound(secondaryDuration, 1, 100 days);

irm =
IRMFixedCyclicalBinary(factory.deploy(PRIMARY_RATE, SECONDARY_RATE, primaryDuration, secondaryDuration, 0));

vm.warp(timeElapsed);

uint cyclesElapsed = timeElapsed / (primaryDuration + secondaryDuration);

if (timeElapsed - cyclesElapsed * (primaryDuration + secondaryDuration) <= primaryDuration) {
assertEq(getIr(), PRIMARY_RATE);
} else {
assertEq(getIr(), SECONDARY_RATE);
}
}

function test_ExpectRevertFactoryRateTooHigh() public {
vm.expectRevert(EulerFixedCyclicalBinaryIRMFactory.IRMFactory_ExcessiveInterestRate.selector);
factory.deploy(MAX_ALLOWED_INTEREST_RATE + 1, SECONDARY_RATE, PRIMARY_DURATION, SECONDARY_DURATION, 0);
vm.expectRevert(EulerFixedCyclicalBinaryIRMFactory.IRMFactory_ExcessiveInterestRate.selector);
factory.deploy(PRIMARY_RATE, MAX_ALLOWED_INTEREST_RATE + 1, PRIMARY_DURATION, SECONDARY_DURATION, 0);
}

function getIr() private view returns (uint256) {
return irm.computeInterestRateView(address(1), 0, 0);
}
}