Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
60 changes: 60 additions & 0 deletions src/adapter/CrossAdapter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity 0.8.23;

import {BaseAdapter} from "src/adapter/BaseAdapter.sol";
import {IPriceOracle} from "src/interfaces/IPriceOracle.sol";
import {ScaleUtils, Scale} from "src/lib/ScaleUtils.sol";

/// @title CrossAdapter
/// @author Euler Labs (https://www.eulerlabs.com/)
/// @notice PriceOracle that chains two adapters.
/// @dev For example, CrossAdapter can price wstETH/USD by querying a wstETH/stETH oracle and a stETH/USD oracle.
contract CrossAdapter is BaseAdapter {
/// @notice The address of the base asset.
address public immutable base;
/// @notice The address of the cross/through asset.
address public immutable cross;
/// @notice The address of the quote asset.
address public immutable quote;
/// @notice The oracle that resolves base/cross and cross/base.
/// @dev The oracle MUST be bidirectional.
address public immutable oracleBaseCross;
/// @notice The oracle that resolves quote/cross and cross/quote.
/// @dev The oracle MUST be bidirectional.
address public immutable oracleQuoteCross;

/// @notice Deploy a CrossAdapter.
/// @param _base The address of the base asset.
/// @param _cross The address of the cross/through asset.
/// @param _quote The address of the quote asset.
/// @param _oracleBaseCross The oracle that resolves base/cross and cross/base.
/// @param _oracleQuoteCross The oracle that resolves quote/cross and cross/quote.
/// @dev Both cross oracles MUST be bidirectional.
/// @dev Does not support bid/ask pricing.
constructor(address _base, address _cross, address _quote, address _oracleBaseCross, address _oracleQuoteCross) {
base = _base;
cross = _cross;
quote = _quote;
oracleBaseCross = _oracleBaseCross;
oracleQuoteCross = _oracleQuoteCross;
}

/// @notice Get a quote by chaining the cross oracles.
/// For the forward direction it calculates base/cross * cross/quote.
/// For the inverse direction it calculates quote/cross * cross/base.
/// @param inAmount The amount of `base` to convert.
/// @param _base The token that is being priced.
/// @param _quote The token that is the unit of account.
/// @return The converted amount by chaining the cross oracles.
function _getQuote(uint256 inAmount, address _base, address _quote) internal view override returns (uint256) {
bool inverse = ScaleUtils.getDirectionOrRevert(_base, base, _quote, quote);

if (inverse) {
inAmount = IPriceOracle(oracleQuoteCross).getQuote(inAmount, quote, cross);
return IPriceOracle(oracleBaseCross).getQuote(inAmount, cross, base);
} else {
inAmount = IPriceOracle(oracleBaseCross).getQuote(inAmount, base, cross);
return IPriceOracle(oracleQuoteCross).getQuote(inAmount, cross, quote);
}
}
}
76 changes: 76 additions & 0 deletions test/unit/adapter/CrossAdapter.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity 0.8.23;

import {Test} from "forge-std/Test.sol";
import {CrossAdapter} from "src/adapter/CrossAdapter.sol";

contract StubPriceOracle {
mapping(address => mapping(address => uint256)) prices;

function setPrice(address base, address quote, uint256 price) external {
prices[base][quote] = price;
}

function getQuote(uint256 inAmount, address base, address quote) external view returns (uint256) {
return _calcQuote(inAmount, base, quote);
}

function getQuotes(uint256 inAmount, address base, address quote) external view returns (uint256, uint256) {
return (_calcQuote(inAmount, base, quote), _calcQuote(inAmount, base, quote));
}

function _calcQuote(uint256 inAmount, address base, address quote) internal view returns (uint256) {
return inAmount * prices[base][quote] / 1e18;
}
}

contract CrossAdapterTest is Test {
address BASE = makeAddr("BASE");
address CROSS = makeAddr("CROSS");
address QUOTE = makeAddr("QUOTE");
StubPriceOracle oracleBaseCross;
StubPriceOracle oracleQuoteCross;
CrossAdapter oracle;

function setUp() public {
oracleBaseCross = new StubPriceOracle();
oracleQuoteCross = new StubPriceOracle();
oracle = new CrossAdapter(BASE, CROSS, QUOTE, address(oracleBaseCross), address(oracleQuoteCross));
}

function test_Constructor_Integrity() public view {
assertEq(oracle.base(), BASE);
assertEq(oracle.cross(), CROSS);
assertEq(oracle.quote(), QUOTE);
assertEq(oracle.oracleBaseCross(), address(oracleBaseCross));
assertEq(oracle.oracleQuoteCross(), address(oracleQuoteCross));
}



function test_GetQuote_Integrity(uint256 inAmount, uint256 priceBaseCross, uint256 priceCrossQuote) public {
inAmount = bound(inAmount, 0, type(uint128).max);
priceBaseCross = bound(priceBaseCross, 1, 1e27);
priceCrossQuote = bound(priceCrossQuote, 1, 1e27);

oracleBaseCross.setPrice(BASE, CROSS, priceBaseCross);
oracleQuoteCross.setPrice(CROSS, QUOTE, priceCrossQuote);

uint256 expectedOutAmount = inAmount * priceBaseCross / 1e18 * priceCrossQuote / 1e18;
assertEq(oracle.getQuote(inAmount, BASE, QUOTE), expectedOutAmount);
}

function test_GetQuote_Integrity_Inverse(uint256 inAmount, uint256 priceQuoteCross, uint256 priceCrossBase)
public
{
inAmount = bound(inAmount, 0, type(uint128).max);
priceQuoteCross = bound(priceQuoteCross, 1, 1e27);
priceCrossBase = bound(priceCrossBase, 1, 1e27);

oracleQuoteCross.setPrice(QUOTE, CROSS, priceQuoteCross);
oracleBaseCross.setPrice(CROSS, BASE, priceCrossBase);

uint256 expectedOutAmount = inAmount * priceQuoteCross / 1e18 * priceCrossBase / 1e18;
assertEq(oracle.getQuote(inAmount, QUOTE, BASE), expectedOutAmount);
}
}