Skip to content

Commit

Permalink
[SC-334] Capped rate oracle (#18)
Browse files Browse the repository at this point in the history
* add capped fallback rate source; update rate source to include decimals

* add capped fallback rate source; update rate source to include decimals

* add unit tests

* add integration test

* anchor old tests to previous fork

* update to new block with correct LST rate

* add readme

* review fixes

* styling on if/else

* separate tests for decimal change and comment

* simplify the logic for getAPR()

* more extensive boundary tests
  • Loading branch information
hexonaut authored May 31, 2024
1 parent 4d44ee2 commit cbd0617
Show file tree
Hide file tree
Showing 12 changed files with 323 additions and 22 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ Please note all these oracles are designed for consumption by `AaveOracle` which

[PotRateSource](https://github.com/marsfoundation/sparklend-advanced/blob/master/src/PotRateSource.sol): Adapter to convert DSR into APR which can be consumed by one of the rate target interest rate strategies. Used for: DAI/USDC/USDT markets

[CappedFallbackRateSource](https://github.com/marsfoundation/sparklend-advanced/blob/master/src/CappedFallbackRateSource.sol): Wraps another rate source, caps the rate and protects against reverts with a fallback value. Used for: ETH market

## Usage

```bash
Expand Down
47 changes: 47 additions & 0 deletions src/CappedFallbackRateSource.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// SPDX-License-Identifier: AGPL-3.0
pragma solidity ^0.8.0;

import { IRateSource } from "./interfaces/IRateSource.sol";

/**
* @title CappedFallbackRateSource
* @notice Wraps another rate source, caps the rate and protects against reverts with a fallback value.
*/
contract CappedFallbackRateSource is IRateSource {

IRateSource public immutable source;
uint256 public immutable lowerBound;
uint256 public immutable upperBound;
uint256 public immutable defaultRate;

constructor(
address _source,
uint256 _lowerBound,
uint256 _upperBound,
uint256 _defaultRate
) {
require(_lowerBound <= _upperBound, "CappedFallbackRateSource/invalid-bounds");
require(_defaultRate >= _lowerBound && _defaultRate <= _upperBound, "CappedFallbackRateSource/invalid-default-rate");

source = IRateSource(_source);
lowerBound = _lowerBound;
upperBound = _upperBound;
defaultRate = _defaultRate;
}

function getAPR() external override view returns (uint256) {
try source.getAPR() returns (uint256 rate) {
if (rate < lowerBound) return lowerBound;
else if (rate > upperBound) return upperBound;

return rate;
} catch {
return defaultRate;
}
}

function decimals() external view override returns (uint8) {
return source.decimals();
}

}
4 changes: 4 additions & 0 deletions src/PotRateSource.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,8 @@ contract PotRateSource is IRateSource {
return (pot.dsr() - 1e27) * 365 days;
}

function decimals() external pure returns (uint8) {
return 27;
}

}
3 changes: 2 additions & 1 deletion src/RateTargetBaseInterestRateStrategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,13 @@ contract RateTargetBaseInterestRateStrategy is VariableBorrowInterestRateStrateg
variableRateSlope2
) {
RATE_SOURCE = IRateSource(rateSource);
require(RATE_SOURCE.decimals() <= 27, "RateTargetBaseInterestRateStrategy/invalid-rate-source-decimals");

_baseVariableBorrowRateSpread = baseVariableBorrowRateSpread;
}

function _getBaseVariableBorrowRate() internal override view returns (uint256) {
uint256 apr = RATE_SOURCE.getAPR();
uint256 apr = RATE_SOURCE.getAPR() * 10 ** (27 - RATE_SOURCE.decimals());
return apr + _baseVariableBorrowRateSpread;
}

Expand Down
3 changes: 2 additions & 1 deletion src/RateTargetKinkInterestRateStrategy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,15 @@ contract RateTargetKinkInterestRateStrategy is VariableBorrowInterestRateStrateg
variableRateSlope2
) {
RATE_SOURCE = IRateSource(rateSource);
require(RATE_SOURCE.decimals() <= 27, "RateTargetKinkInterestRateStrategy/invalid-rate-source-decimals");

_variableRateSlope1Spread = variableRateSlope1Spread;
}

function _getVariableRateSlope1() internal override view returns (uint256) {
// We assume all rates are below max int. This is a reasonable assumption because
// otherwise the rates will be so high that the protocol will stop working
int256 rate = int256(RATE_SOURCE.getAPR()) + _variableRateSlope1Spread - int256(_baseVariableBorrowRate);
int256 rate = int256(RATE_SOURCE.getAPR() * 10 ** (27 - RATE_SOURCE.decimals())) + _variableRateSlope1Spread - int256(_baseVariableBorrowRate);
return rate > 0 ? uint256(rate) : 0;
}

Expand Down
1 change: 1 addition & 0 deletions src/interfaces/IRateSource.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ pragma solidity >=0.8.0;

interface IRateSource {
function getAPR() external view returns (uint256);
function decimals() external view returns (uint8);
}
142 changes: 142 additions & 0 deletions test/CappedFallbackRateSource.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// SPDX-License-Identifier: AGPL-3.0
pragma solidity ^0.8.0;

import "forge-std/Test.sol";

import { RateSourceMock } from "./mocks/RateSourceMock.sol";

import { CappedFallbackRateSource } from "../src/CappedFallbackRateSource.sol";

contract RevertingRateSource {
function getAPR() external pure returns (uint256) {
revert("RevertingRateSource/some-error");
}
}

contract CappedFallbackRateSourceTest is Test {

RateSourceMock originalSource;

CappedFallbackRateSource rateSource;

function setUp() public {
originalSource = new RateSourceMock(0.037e18, 18);

rateSource = new CappedFallbackRateSource({
_source: address(originalSource),
_lowerBound: 0.01e18,
_upperBound: 0.08e18,
_defaultRate: 0.03e18
});
}

function test_constructor_lowerBoundGtUpperBoundBoundary() public {
vm.expectRevert("CappedFallbackRateSource/invalid-bounds");
new CappedFallbackRateSource({
_source: address(originalSource),
_lowerBound: 0.01e18 + 1, // Lower bound is larger than upper bound
_upperBound: 0.01e18,
_defaultRate: 0.01e18
});

new CappedFallbackRateSource({
_source: address(originalSource),
_lowerBound: 0.01e18,
_upperBound: 0.01e18,
_defaultRate: 0.01e18
});
}

function test_constructor_defaultRateLtLowerBoundBoundary() public {
vm.expectRevert("CappedFallbackRateSource/invalid-default-rate");
new CappedFallbackRateSource({
_source: address(originalSource),
_lowerBound: 0.01e18,
_upperBound: 0.08e18,
_defaultRate: 0.01e18 - 1 // Default rate is below lower bound
});

new CappedFallbackRateSource({
_source: address(originalSource),
_lowerBound: 0.01e18,
_upperBound: 0.08e18,
_defaultRate: 0.01e18
});
}

function test_constructor_defaultRateGtUpperBoundBoundary() public {
vm.expectRevert("CappedFallbackRateSource/invalid-default-rate");
new CappedFallbackRateSource({
_source: address(originalSource),
_lowerBound: 0.01e18,
_upperBound: 0.08e18,
_defaultRate: 0.08e18 + 1 // Default rate is above upper bound
});

new CappedFallbackRateSource({
_source: address(originalSource),
_lowerBound: 0.01e18,
_upperBound: 0.08e18,
_defaultRate: 0.08e18
});
}

function test_constructor() public {
assertEq(address(rateSource.source()), address(originalSource));
assertEq(rateSource.lowerBound(), 0.01e18);
assertEq(rateSource.upperBound(), 0.08e18);
assertEq(rateSource.defaultRate(), 0.03e18);
}

function test_decimals() public {
assertEq(rateSource.decimals(), 18);

originalSource.setDecimals(27);

assertEq(rateSource.decimals(), 27);
}

function test_getAPR_rateWithinBounds() public {
assertEq(rateSource.getAPR(), 0.037e18);
}

function test_getAPR_rateBelowLowerBoundBoundary() public {
originalSource.setRate(0.01e18 - 1);

assertEq(rateSource.getAPR(), 0.01e18); // Use lowerBound

originalSource.setRate(0.01e18);

assertEq(rateSource.getAPR(), 0.01e18); // Use sourceRate

originalSource.setRate(0.01e18 + 1);

assertEq(rateSource.getAPR(), 0.01e18 + 1); // Use sourceRate
}

function test_getAPR_rateAboveUpperBoundBoundary() public {
originalSource.setRate(0.08e18 + 1);

assertEq(rateSource.getAPR(), 0.08e18); // Use upperBound

originalSource.setRate(0.08e18);

assertEq(rateSource.getAPR(), 0.08e18); // Use sourceRate

originalSource.setRate(0.08e18 - 1);

assertEq(rateSource.getAPR(), 0.08e18 - 1); // Use sourceRate
}

function test_getAPR_rateReverts() public {
rateSource = new CappedFallbackRateSource({
_source: address(new RevertingRateSource()),
_lowerBound: 0.01e18,
_upperBound: 0.08e18,
_defaultRate: 0.03e18
});

assertEq(rateSource.getAPR(), 0.03e18);
}

}
1 change: 1 addition & 0 deletions test/PotRateSource.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ contract PotRateSourceTest is Test {

function test_constructor() public {
assertEq(address(rateSource.pot()), address(pot));
assertEq(rateSource.decimals(), 27);
}

function test_bad_dsr_value() public {
Expand Down
35 changes: 33 additions & 2 deletions test/RateTargetBaseInterestRateStrategy.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ contract RateTargetBaseInterestRateStrategyTest is InterestRateStrategyBaseTest
RateTargetBaseInterestRateStrategy interestStrategy;

function setUp() public {
rateSource = new RateSourceMock(0.05e27);
rateSource = new RateSourceMock(0.05e27, 27);

interestStrategy = new RateTargetBaseInterestRateStrategy({
provider: IPoolAddressesProvider(address(123)),
Expand Down Expand Up @@ -51,7 +51,7 @@ contract RateTargetBaseInterestRateStrategyTest is InterestRateStrategyBaseTest
assertEq(interestStrategy.getBaseVariableBorrowRateSpread(), 0.005e27);
}

function test_rateSource_change() public {
function test_rateSource_changeRate() public {
assertEq(interestStrategy.getVariableRateSlope1(), 0.01e27);
assertEq(interestStrategy.getVariableRateSlope2(), 0.45e27);
assertEq(interestStrategy.getBaseVariableBorrowRate(), 0.055e27);
Expand All @@ -65,4 +65,35 @@ contract RateTargetBaseInterestRateStrategyTest is InterestRateStrategyBaseTest
assertEq(interestStrategy.getMaxVariableBorrowRate(), 0.535e27);
}

function test_rateSource_changeDecimals() public {
assertEq(interestStrategy.getVariableRateSlope1(), 0.01e27);
assertEq(interestStrategy.getVariableRateSlope2(), 0.45e27);
assertEq(interestStrategy.getBaseVariableBorrowRate(), 0.055e27);
assertEq(interestStrategy.getMaxVariableBorrowRate(), 0.515e27);

// Note that the rate will still convert to 27 decimals even if it's reported at 18
rateSource.setRate(0.05e18);
rateSource.setDecimals(18);

assertEq(interestStrategy.getVariableRateSlope1(), 0.01e27);
assertEq(interestStrategy.getVariableRateSlope2(), 0.45e27);
assertEq(interestStrategy.getBaseVariableBorrowRate(), 0.055e27);
assertEq(interestStrategy.getMaxVariableBorrowRate(), 0.515e27);
}

function test_rateSource_invalidDecimals() public {
rateSource.setRate(0.07e28);
rateSource.setDecimals(28);

vm.expectRevert("RateTargetBaseInterestRateStrategy/invalid-rate-source-decimals");
new RateTargetBaseInterestRateStrategy({
provider: IPoolAddressesProvider(address(123)),
rateSource: address(rateSource),
optimalUsageRatio: 0.8e27,
baseVariableBorrowRateSpread: 0.005e27,
variableRateSlope1: 0.01e27,
variableRateSlope2: 0.45e27
});
}

}
33 changes: 32 additions & 1 deletion test/RateTargetKinkInterestRateStrategy.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ contract RateTargetKinkInterestRateStrategyTest is InterestRateStrategyBaseTest
RateTargetKinkInterestRateStrategy interestStrategyPositiveSpread;

function setUp() public {
rateSource = new RateSourceMock(0.05e27);
rateSource = new RateSourceMock(0.05e27, 27);

interestStrategy = new RateTargetKinkInterestRateStrategy({
provider: IPoolAddressesProvider(address(123)),
Expand Down Expand Up @@ -108,4 +108,35 @@ contract RateTargetKinkInterestRateStrategyTest is InterestRateStrategyBaseTest
assertEq(interestStrategy.getMaxVariableBorrowRate(), 0.56e27);
}

function test_rateSource_changeDecimals() public {
assertEq(interestStrategy.getVariableRateSlope1(), 0.035e27);
assertEq(interestStrategy.getVariableRateSlope2(), 0.55e27);
assertEq(interestStrategy.getBaseVariableBorrowRate(), 0.01e27);
assertEq(interestStrategy.getMaxVariableBorrowRate(), 0.595e27);

// Note that the rate will still convert to 27 decimals even if it's reported at 18
rateSource.setRate(0.05e18);
rateSource.setDecimals(18);

assertEq(interestStrategy.getVariableRateSlope1(), 0.035e27);
assertEq(interestStrategy.getVariableRateSlope2(), 0.55e27);
assertEq(interestStrategy.getBaseVariableBorrowRate(), 0.01e27);
assertEq(interestStrategy.getMaxVariableBorrowRate(), 0.595e27);
}

function test_rateSource_invalidDecimals() public {
rateSource.setRate(0.07e28);
rateSource.setDecimals(28);

vm.expectRevert("RateTargetKinkInterestRateStrategy/invalid-rate-source-decimals");
new RateTargetKinkInterestRateStrategy({
provider: IPoolAddressesProvider(address(123)),
rateSource: address(rateSource),
optimalUsageRatio: 0.8e27,
baseVariableBorrowRate: 0.01e27,
variableRateSlope1Spread: -0.005e27,
variableRateSlope2: 0.55e27
});
}

}
Loading

0 comments on commit cbd0617

Please sign in to comment.