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
2 changes: 2 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,6 @@ depth = 10
dictionary_weight = 80
fail_on_revert = false

[fmt]
ignore = ['test/adapter/maker/SDaiOracle.diff.t.sol']
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
2 changes: 2 additions & 0 deletions src/adapter/maker/IPot.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ pragma solidity 0.8.23;

interface IPot {
function chi() external view returns (uint256);
function rho() external view returns (uint256);
function dsr() external view returns (uint256);
}
19 changes: 16 additions & 3 deletions src/adapter/maker/SDaiOracle.sol
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity 0.8.23;

import {FixedPointMathLib} from "@solady/utils/FixedPointMathLib.sol";
import {BaseAdapter, Errors} from "src/adapter/BaseAdapter.sol";
import {IPot} from "src/adapter/maker/IPot.sol";

/// @title SDaiOracle
/// @author Euler Labs (https://www.eulerlabs.com/)
/// @notice Adapter for pricing Maker sDAI <-> DAI via the DSR Pot contract.
contract SDaiOracle is BaseAdapter {
uint256 internal constant RAY = 1e27;
/// @dev The address of the DAI token.
address public immutable dai;
/// @dev The address of the sDAI token.
Expand All @@ -27,17 +29,28 @@ contract SDaiOracle is BaseAdapter {
}

/// @notice Get a quote by querying the exchange rate from the DSR Pot contract.
/// @dev Calls `chi`, the interest rate accumulator, to get the exchange rate.
/// @param inAmount The amount of `base` to convert.
/// @param base The token that is being priced. Either `sDai` or `dai`.
/// @param quote The token that is the unit of account. Either `dai` or `sDai`.
/// @return The converted amount.
function _getQuote(uint256 inAmount, address base, address quote) internal view override returns (uint256) {
if (base == sDai && quote == dai) {
return inAmount * IPot(dsrPot).chi() / 1e27;
return inAmount * _getExchangeRate() / RAY;
} else if (base == dai && quote == sDai) {
return inAmount * 1e27 / IPot(dsrPot).chi();
return inAmount * RAY / _getExchangeRate();
}
revert Errors.PriceOracle_NotSupported(base, quote);
}

/// @notice Get the exchange rate from the DSR Pot contract.
/// @dev This function replicates `IPot.drip`, compounding the savings rate for the time since last update.
/// Calling `drip` directly is not an option because it is state-mutating, making these functions non-view.
/// @return The sDAI/DAI exchange rate.
function _getExchangeRate() internal view returns (uint256) {
uint256 lastUpdatedAt = IPot(dsrPot).rho();
uint256 exchangeRate = IPot(dsrPot).chi();
if (lastUpdatedAt == block.timestamp) return exchangeRate;
uint256 ratePerSecond = IPot(dsrPot).dsr();
return FixedPointMathLib.rpow(ratePerSecond, block.timestamp - lastUpdatedAt, RAY) * exchangeRate / RAY;
}
}
1 change: 1 addition & 0 deletions test/adapter/AdapterHelper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ contract AdapterHelper is Test {
FeedReturnsZeroPrice,
FeedReturnsTooLargePrice,
FeedReturnsStalePrice,
FeedReturnsStaleRate,
FeedReturnsConfTooWide,
FeedReturnsExpoTooLow,
FeedReturnsExpoTooHigh,
Expand Down
55 changes: 55 additions & 0 deletions test/adapter/maker/SDaiOracle.diff.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity 0.8.23;

import {Test} from "forge-std/Test.sol";
import {FixedPointMathLib} from "@solady/utils/FixedPointMathLib.sol";

contract SDaiOracleDiffHarness {
function rpowSolady(uint256 x, uint256 y, uint256 b) external pure returns (uint256) {
return FixedPointMathLib.rpow(x, y, b);
}

/// @dev From https://etherscan.io/address/0x197E90f9FAD81970bA7976f33CbD77088E5D7cf7#code#L134
function rpowMaker(uint256 x, uint256 n, uint256 base) external pure returns (uint256 z) {
assembly {
switch x case 0 {switch n case 0 {z := base} default {z := 0}}
default {
switch mod(n, 2) case 0 { z := base } default { z := x }
let half := div(base, 2) // for rounding.
for { n := div(n, 2) } n { n := div(n,2) } {
let xx := mul(x, x)
if iszero(eq(div(xx, x), x)) { revert(0,0) }
let xxRound := add(xx, half)
if lt(xxRound, xx) { revert(0,0) }
x := div(xxRound, base)
if mod(n,2) {
let zx := mul(z, x)
if and(iszero(iszero(x)), iszero(eq(div(zx, x), z))) { revert(0,0) }
let zxRound := add(zx, half)
if lt(zxRound, zx) { revert(0,0) }
z := div(zxRound, base)
}
}
}
}
}
}

contract SDaiOracleDiffTest is Test {
SDaiOracleDiffHarness harness;

constructor() {
harness = new SDaiOracleDiffHarness();
}

/// forge-config: default.fuzz.runs = 100000
function test_SoladyRpow_EquivalentTo_MakerRpow(uint256 x, uint256 n, uint256 base) public {
(bool successSolady, bytes memory dataSolady) =
address(harness).call(abi.encodeCall(SDaiOracleDiffHarness.rpowSolady, (x, n, base)));
(bool successMaker, bytes memory dataMaker) =
address(harness).call(abi.encodeCall(SDaiOracleDiffHarness.rpowMaker, (x, n, base)));

assertEq(successSolady, successMaker);
if (successSolady) assertEq(dataSolady, dataMaker);
}
}
27 changes: 27 additions & 0 deletions test/adapter/maker/SDaiOracle.fork.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pragma solidity 0.8.23;

import {DAI, SDAI, DSR_POT} from "test/utils/EthereumAddresses.sol";
import {ForkTest} from "test/utils/ForkTest.sol";
import {IPot} from "src/adapter/maker/IPot.sol";
import {SDaiOracle} from "src/adapter/maker/SDaiOracle.sol";

contract SDaiOracleForkTest is ForkTest {
Expand All @@ -21,6 +22,18 @@ contract SDaiOracleForkTest is ForkTest {
assertApproxEqRel(daiSDai, 950e6, 0.1e18);
}

function test_GetQuote_Integrity_EquivalentToDrip() public {
uint256 sDaiDaiBeforeDrip = oracle.getQuote(1000e6, SDAI, DAI);
uint256 daiSDaiBeforeDrip = oracle.getQuote(1000e6, DAI, SDAI);
(bool success,) = DSR_POT.call(abi.encodeWithSelector(bytes4(keccak256("drip()"))));
assertTrue(success);

uint256 sDaiDaiAfterDrip = oracle.getQuote(1000e6, SDAI, DAI);
uint256 daiSDaiAfterDrip = oracle.getQuote(1000e6, DAI, SDAI);
assertEq(sDaiDaiBeforeDrip, sDaiDaiAfterDrip);
assertEq(daiSDaiBeforeDrip, daiSDaiAfterDrip);
}

function test_GetQuotes_Integrity() public view {
(uint256 sDaiDaiBid, uint256 sDaiDaiAsk) = oracle.getQuotes(1000e6, SDAI, DAI);
assertApproxEqRel(sDaiDaiBid, 1050e6, 0.1e18);
Expand All @@ -30,4 +43,18 @@ contract SDaiOracleForkTest is ForkTest {
assertApproxEqRel(daiSDaiBid, 950e6, 0.1e18);
assertApproxEqRel(daiSDaiAsk, 950e6, 0.1e18);
}

function test_GetQuotes_Integrity_EquivalentToDrip() public {
(uint256 sDaiDaiBidBeforeDrip, uint256 sDaiDaiAskBeforeDrip) = oracle.getQuotes(1000e6, SDAI, DAI);
(uint256 daiSDaiBidBeforeDrip, uint256 daiSDaiAskBeforeDrip) = oracle.getQuotes(1000e6, DAI, SDAI);
(bool success,) = DSR_POT.call(abi.encodeWithSelector(bytes4(keccak256("drip()"))));
assertTrue(success);

(uint256 sDaiDaiBidAfterDrip, uint256 sDaiDaiAskAfterDrip) = oracle.getQuotes(1000e6, SDAI, DAI);
(uint256 daiSDaiBidAfterDrip, uint256 daiSDaiAskAfterDrip) = oracle.getQuotes(1000e6, DAI, SDAI);
assertEq(sDaiDaiBidBeforeDrip, sDaiDaiBidAfterDrip);
assertEq(sDaiDaiAskBeforeDrip, sDaiDaiAskAfterDrip);
assertEq(daiSDaiBidBeforeDrip, daiSDaiBidAfterDrip);
assertEq(daiSDaiAskBeforeDrip, daiSDaiAskAfterDrip);
}
}
38 changes: 33 additions & 5 deletions test/adapter/maker/SDaiOracle.unit.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,7 @@ contract SDaiOracleTest is SDaiOracleHelper {
assertEq(SDaiOracle(oracle).dsrPot(), POT);
}

function test_GetQuote_GetQuotes_RevertsWhen_InvalidTokens(FuzzableState memory s, address otherA, address otherB)
public
{
function test_Quote_RevertsWhen_InvalidTokens(FuzzableState memory s, address otherA, address otherB) public {
setUpState(s);
vm.assume(otherA != SDAI && otherA != DAI);
vm.assume(otherB != SDAI && otherB != DAI);
Expand All @@ -41,7 +39,7 @@ contract SDaiOracleTest is SDaiOracleHelper {
function test_Quote_SDai_Dai_Integrity(FuzzableState memory s) public {
setUpState(s);

uint256 expectedOutAmount = s.inAmount * s.rate / 1e27;
uint256 expectedOutAmount = s.inAmount * s.chi / 1e27;

uint256 outAmount = SDaiOracle(oracle).getQuote(s.inAmount, SDAI, DAI);
assertEq(outAmount, expectedOutAmount);
Expand All @@ -54,7 +52,37 @@ contract SDaiOracleTest is SDaiOracleHelper {
function test_Quote_Dai_SDai_Integrity(FuzzableState memory s) public {
setUpState(s);

uint256 expectedOutAmount = s.inAmount * 1e27 / s.rate;
uint256 expectedOutAmount = s.inAmount * 1e27 / s.chi;

uint256 outAmount = SDaiOracle(oracle).getQuote(s.inAmount, DAI, SDAI);
assertEq(outAmount, expectedOutAmount);

(uint256 bidOutAmount, uint256 askOutAmount) = SDaiOracle(oracle).getQuotes(s.inAmount, DAI, SDAI);
assertEq(bidOutAmount, expectedOutAmount);
assertEq(askOutAmount, expectedOutAmount);
}

function test_Quote_SDai_Dai_Integrity_Stale(FuzzableState memory s) public {
setBehavior(Behavior.FeedReturnsStaleRate, true);
setUpState(s);

uint256 rate = getUpdatedRate(s);
uint256 expectedOutAmount = s.inAmount * rate / 1e27;

uint256 outAmount = SDaiOracle(oracle).getQuote(s.inAmount, SDAI, DAI);
assertEq(outAmount, expectedOutAmount);

(uint256 bidOutAmount, uint256 askOutAmount) = SDaiOracle(oracle).getQuotes(s.inAmount, SDAI, DAI);
assertEq(bidOutAmount, expectedOutAmount);
assertEq(askOutAmount, expectedOutAmount);
}

function test_Quote_Dai_SDai_Integrity_Stale(FuzzableState memory s) public {
setBehavior(Behavior.FeedReturnsStaleRate, true);
setUpState(s);

uint256 rate = getUpdatedRate(s);
uint256 expectedOutAmount = s.inAmount * 1e27 / rate;

uint256 outAmount = SDaiOracle(oracle).getQuote(s.inAmount, DAI, SDAI);
assertEq(outAmount, expectedOutAmount);
Expand Down
29 changes: 23 additions & 6 deletions test/adapter/maker/SDaiOracleHelper.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity 0.8.23;

import {FixedPointMathLib} from "@solady/utils/FixedPointMathLib.sol";
import {SDaiOracle} from "src/adapter/maker/SDaiOracle.sol";
import {AdapterHelper} from "test/adapter/AdapterHelper.sol";
import {StubMCDPot} from "test/adapter/maker/StubMCDPot.sol";
Expand All @@ -9,30 +10,46 @@ contract SDaiOracleHelper is AdapterHelper {
address internal DAI = makeAddr("DAI");
address internal SDAI = makeAddr("SDAI");
address internal POT;
uint256 internal RAY = 1e27;

struct FuzzableState {
address base;
address quote;
// Answer
uint256 rate;
// DSR Pot
uint256 rho; // last update
uint256 chi; // exchange rate at last update
uint256 dsr; // accumulation rate per second
// Environment
uint256 inAmount;
uint256 timestamp;
}

function setUpState(FuzzableState memory s) internal {
s.rate = bound(s.rate, 1e27, 1e36);

POT = address(new StubMCDPot());
oracle = address(new SDaiOracle(DAI, SDAI, POT));

s.base = SDAI;
s.quote = DAI;

s.inAmount = bound(s.inAmount, 1, type(uint128).max);
s.timestamp = bound(s.timestamp, 2 ** 30 + 1, 2 ** 31);
if (behaviors[Behavior.FeedReturnsStaleRate]) {
s.rho = bound(s.rho, s.timestamp - 31_536_000, s.timestamp - 1);
} else {
s.rho = s.timestamp;
}
s.chi = bound(s.chi, 1e27, 1e28);
s.dsr = bound(s.dsr, 1e27 + 1, 1e27 + 1e20);

StubMCDPot(POT).setParams(s.chi, s.rho, s.dsr);
if (behaviors[Behavior.FeedReverts]) {
StubMCDPot(POT).setRevert(true);
} else {
StubMCDPot(POT).setRate(s.rate);
}

vm.warp(s.timestamp);
}

function getUpdatedRate(FuzzableState memory s) internal view returns (uint256) {
return FixedPointMathLib.rpow(s.dsr, block.timestamp - s.rho, RAY) * s.chi / RAY;
}
}
16 changes: 15 additions & 1 deletion test/adapter/maker/StubMCDPot.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,33 @@ import {IPot} from "src/adapter/maker/IPot.sol";

contract StubMCDPot is IPot {
uint256 _chi;
uint256 _rho;
uint256 _dsr;
bool doRevert;
string revertMsg = "oops";

function setRevert(bool _doRevert) external {
doRevert = _doRevert;
}

function setRate(uint256 chi_) external {
function setParams(uint256 chi_, uint256 rho_, uint256 dsr_) external {
_chi = chi_;
_rho = rho_;
_dsr = dsr_;
}

function chi() external view returns (uint256) {
if (doRevert) revert(revertMsg);
return _chi;
}

function rho() external view returns (uint256) {
if (doRevert) revert(revertMsg);
return _rho;
}

function dsr() external view returns (uint256) {
if (doRevert) revert(revertMsg);
return _dsr;
}
}
2 changes: 1 addition & 1 deletion test/utils/TestUtils.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ function boundAddr(address addr) pure returns (address) {
if (
uint160(addr) < 256 || addr == 0x4e59b44847b379578588920cA78FbF26c0B4956C
|| addr == 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D || addr == 0x000000000000000000636F6e736F6c652e6c6f67
|| addr == 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
|| addr == 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f || addr == 0x2e234DAe75C793f67A35089C9d99245E1C58470b
) return address(uint160(addr) + 256);

return addr;
Expand Down