Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SC-885] OraclePrices library #75

Merged
merged 13 commits into from
Aug 10, 2023
5 changes: 2 additions & 3 deletions contracts/MultiWrapper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@
pragma solidity 0.8.19;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import "./interfaces/IWrapper.sol";

contract MultiWrapper is Ownable {
using SafeMath for uint256;
using EnumerableSet for EnumerableSet.AddressSet;

error WrapperAlreadyAdded();
Expand Down Expand Up @@ -69,7 +68,7 @@ contract MultiWrapper is Ownable {
}
if (!used) {
memWrappedTokens[len] = wrappedToken2;
memRates[len] = rate.mul(rate2).div(1e18);
memRates[len] = Math.mulDiv(rate, rate2, 1e18);
len += 1;
}
} catch { continue; }
Expand Down
127 changes: 39 additions & 88 deletions contracts/OffchainOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,17 @@
pragma solidity 0.8.19;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import "./interfaces/IOracle.sol";
import "./interfaces/IWrapper.sol";
import "./MultiWrapper.sol";
import "./libraries/Sqrt.sol";
import "./libraries/OraclePrices.sol";

contract OffchainOracle is Ownable {
using Math for uint256;
using SafeMath for uint256;
using Sqrt for uint256;
using EnumerableSet for EnumerableSet.AddressSet;
using OraclePrices for OraclePrices.Data;

error ArraysLengthMismatch();
error OracleAlreadyAdded();
Expand All @@ -34,9 +32,14 @@ contract OffchainOracle is Ownable {
event ConnectorRemoved(IERC20 connector);
event MultiWrapperUpdated(MultiWrapper multiWrapper);

struct OraclePrice {
uint256 rate;
uint256 weight;
struct GetRateImplParams {
IOracle oracle;
IERC20 srcToken;
uint256 srcTokenRate;
IERC20 dstToken;
uint256 dstTokenRate;
IERC20 connector;
uint256 thresholdFilter;
}

EnumerableSet.AddressSet private _wethOracles;
Expand Down Expand Up @@ -260,22 +263,12 @@ contract OffchainOracle is Ownable {
IERC20[][2] memory allConnectors = _getAllConnectors(customConnectors);

uint256 maxArrLength = wrappedSrcTokens.length * wrappedDstTokens.length * (allConnectors[0].length + allConnectors[1].length) * allOracles.length;
OraclePrice[] memory oraclePrices;
// Memory allocation in assembly to avoid array zeroing
assembly ("memory-safe") { // solhint-disable-line no-inline-assembly
oraclePrices := mload(0x40)
mstore(0x40, add(oraclePrices, add(0x20, mul(maxArrLength, 0x40))))
mstore(oraclePrices, maxArrLength)
}

uint256 oracleIndex;
uint256 maxOracleWeight;

OraclePrices.Data memory ratesAndWeights = OraclePrices.init(maxArrLength);
unchecked {
for (uint256 k1 = 0; k1 < wrappedSrcTokens.length; k1++) {
for (uint256 k2 = 0; k2 < wrappedDstTokens.length; k2++) {
if (wrappedSrcTokens[k1] == wrappedDstTokens[k2]) {
return srcRates[k1].mul(dstRates[k2]).div(1e18);
return srcRates[k1] * dstRates[k2] / 1e18;
}
for (uint256 k3 = 0; k3 < 2; k3++) {
for (uint256 j = 0; j < allConnectors[k3].length; j++) {
Expand All @@ -284,38 +277,22 @@ contract OffchainOracle is Ownable {
continue;
}
for (uint256 i = 0; i < allOracles.length; i++) {
(OraclePrice memory oraclePrice) = _getRateImpl(allOracles[i], wrappedSrcTokens[k1], srcRates[k1], wrappedDstTokens[k2], dstRates[k2], connector);
if (oraclePrice.weight > 0) {
oraclePrices[oracleIndex] = oraclePrice;
oracleIndex++;
if (oraclePrice.weight > maxOracleWeight) {
maxOracleWeight = oraclePrice.weight;
}
}
GetRateImplParams memory params = GetRateImplParams({
oracle: allOracles[i],
srcToken: wrappedSrcTokens[k1],
srcTokenRate: srcRates[k1],
dstToken: wrappedDstTokens[k2],
dstTokenRate: dstRates[k2],
connector: connector,
thresholdFilter: thresholdFilter
});
ratesAndWeights.append(_getRateImpl(params));
}
}
}
}
}
assembly ("memory-safe") { // solhint-disable-line no-inline-assembly
mstore(oraclePrices, oracleIndex)
}

uint256 totalWeight;
for (uint256 i = 0; i < oraclePrices.length; i++) {
if (oraclePrices[i].weight * 100 < maxOracleWeight * thresholdFilter) {
continue;
}
(bool ok, uint256 weightedRateI) = oraclePrices[i].rate.tryMul(oraclePrices[i].weight);
if (ok) {
(ok, weightedRate) = _tryAdd(weightedRate, weightedRateI);
if (ok) totalWeight += oraclePrices[i].weight;
}
}

if (totalWeight > 0) {
weightedRate = weightedRate / totalWeight;
}
(weightedRate,) = ratesAndWeights.getRateAndWeightWithSafeMath(thresholdFilter);
}
}

Expand Down Expand Up @@ -353,17 +330,7 @@ contract OffchainOracle is Ownable {
IERC20[][2] memory allConnectors = _getAllConnectors(customConnectors);

uint256 maxArrLength = wrappedSrcTokens.length * wrappedDstTokens.length * (allConnectors[0].length + allConnectors[1].length) * (wrappedOracles[0].length + wrappedOracles[1].length);
OraclePrice[] memory oraclePrices;
// Memory allocation in assembly to avoid array zeroing
assembly ("memory-safe") { // solhint-disable-line no-inline-assembly
oraclePrices := mload(0x40)
mstore(0x40, add(oraclePrices, mul(maxArrLength, 0x40)))
mstore(oraclePrices, maxArrLength)
}

uint256 oracleIndex;
uint256 maxOracleWeight;

OraclePrices.Data memory ratesAndWeights = OraclePrices.init(maxArrLength);
unchecked {
for (uint256 k1 = 0; k1 < wrappedSrcTokens.length; k1++) {
for (uint256 k2 = 0; k2 < wrappedDstTokens.length; k2++) {
Expand All @@ -377,38 +344,22 @@ contract OffchainOracle is Ownable {
continue;
}
for (uint256 i = 0; i < wrappedOracles[k2].length; i++) {
(OraclePrice memory oraclePrice) = _getRateImpl(IOracle(address(uint160(uint256(wrappedOracles[k2][i])))), wrappedSrcTokens[k1], srcRates[k1], wrappedDstTokens[k2], 1e18, connector);
if (oraclePrice.weight > 0) {
oraclePrices[oracleIndex] = oraclePrice;
oracleIndex++;
if (oraclePrice.weight > maxOracleWeight) {
maxOracleWeight = oraclePrice.weight;
}
}
GetRateImplParams memory params = GetRateImplParams({
oracle: IOracle(address(uint160(uint256(wrappedOracles[k2][i])))),
srcToken: wrappedSrcTokens[k1],
srcTokenRate: srcRates[k1],
dstToken: wrappedDstTokens[k2],
dstTokenRate: 1e18,
connector: connector,
thresholdFilter: thresholdFilter
});
ratesAndWeights.append(_getRateImpl(params));
}
}
}
}
}
assembly ("memory-safe") { // solhint-disable-line no-inline-assembly
mstore(oraclePrices, oracleIndex)
}

uint256 totalWeight;
for (uint256 i = 0; i < oracleIndex; i++) {
if (oraclePrices[i].weight < maxOracleWeight * thresholdFilter / 100) {
continue;
}
(bool ok, uint256 weightedRateI) = oraclePrices[i].rate.tryMul(oraclePrices[i].weight);
if (ok) {
(ok, weightedRate) = _tryAdd(weightedRate, weightedRateI);
if (ok) totalWeight += oraclePrices[i].weight;
}
}

if (totalWeight > 0) {
weightedRate = weightedRate / totalWeight;
}
(weightedRate,) = ratesAndWeights.getRateAndWeightWithSafeMath(thresholdFilter);
}
}

Expand All @@ -433,10 +384,10 @@ contract OffchainOracle is Ownable {
allConnectors[1] = customConnectors;
}

function _getRateImpl(IOracle oracle, IERC20 srcToken, uint256 srcTokenRate, IERC20 dstToken, uint256 dstTokenRate, IERC20 connector) private view returns (OraclePrice memory oraclePrice) {
try oracle.getRate(srcToken, dstToken, connector) returns (uint256 rate, uint256 weight) {
uint256 result = _scaledMul([srcTokenRate, rate, dstTokenRate], 1e18);
oraclePrice = OraclePrice(result, result == 0 ? 0 : weight);
function _getRateImpl(GetRateImplParams memory p) private view returns (OraclePrices.OraclePrice memory oraclePrice) {
try p.oracle.getRate(p.srcToken, p.dstToken, p.connector, p.thresholdFilter) returns (uint256 rate, uint256 weight) {
uint256 result = _scaledMul([p.srcTokenRate, rate, p.dstTokenRate], 1e18);
oraclePrice = OraclePrices.OraclePrice(result, result == 0 ? 0 : weight);
} catch {} // solhint-disable-line no-empty-blocks
}

Expand Down
2 changes: 1 addition & 1 deletion contracts/interfaces/IOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ interface IOracle {
error PoolNotFound();
error PoolWithConnectorNotFound();

function getRate(IERC20 srcToken, IERC20 dstToken, IERC20 connector) external view returns (uint256 rate, uint256 weight);
function getRate(IERC20 srcToken, IERC20 dstToken, IERC20 connector, uint256 thresholdFilter) external view returns (uint256 rate, uint256 weight);
}
142 changes: 142 additions & 0 deletions contracts/libraries/OraclePrices.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// SPDX-License-Identifier: MIT

pragma solidity 0.8.19;

import "@openzeppelin/contracts/utils/math/SafeMath.sol";

/**
* @title OraclePrices
* @notice A library that provides functionalities for processing and analyzing token rate and weight data provided by an oracle.
* The library is used when an oracle uses multiple pools to determine a token's price.
* It allows to filter out pools with low weight and significantly incorrect price, which could distort the weighted price.
* The level of low-weight pool filtering can be managed using the thresholdFilter parameter.
*/
library OraclePrices {
using SafeMath for uint256;

/**
* @title Oracle Price Data Structure
* @notice This structure encapsulates the rate and weight information for tokens as provided by an oracle
* @dev An array of OraclePrice structures can be used to represent oracle data for multiple pools
* @param rate The oracle-provided rate for a token
* @param weight The oracle-provided derived weight for a token
*/
struct OraclePrice {
uint256 rate;
uint256 weight;
}

/**
* @title Oracle Prices Data Structure
* @notice This structure encapsulates information about a list of oracles prices and weights
* @dev The structure is initialized with a maximum possible length by the `init` function
* @param oraclePrices An array of OraclePrice structures, each containing a rate and weight
* @param maxOracleWeight The maximum weight among the OraclePrice elements in the oraclePrices array
* @param size The number of meaningful OraclePrice elements added to the oraclePrices array
*/
struct Data {
uint256 maxOracleWeight;
uint256 size;
OraclePrice[] oraclePrices;
}

/**
* @notice Initializes an array of OraclePrices with a given maximum length and returns it wrapped inside a Data struct
* @dev Uses inline assembly for memory allocation to avoid array zeroing
* @param maxArrLength The maximum length of the oraclePrices array
* @return data Returns an instance of Data struct containing an OraclePrice array with a specified maximum length
*/
function init(uint256 maxArrLength) internal pure returns (Data memory data) {
OraclePrice[] memory oraclePrices;
assembly ("memory-safe") { // solhint-disable-line no-inline-assembly
oraclePrices := mload(0x40)
mstore(0x40, add(oraclePrices, add(0x20, mul(maxArrLength, 0x40))))
mstore(oraclePrices, maxArrLength)
}
data = Data(0, 0, oraclePrices);
Copy link
Member

@ZumZoom ZumZoom Aug 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
OraclePrice[] memory oraclePrices;
assembly ("memory-safe") { // solhint-disable-line no-inline-assembly
oraclePrices := mload(0x40)
mstore(0x40, add(oraclePrices, add(0x20, mul(maxArrLength, 0x40))))
mstore(oraclePrices, maxArrLength)
}
data = Data(0, 0, oraclePrices);
assembly ("memory-safe") { // solhint-disable-line no-inline-assembly
data := mload(0x40)
mstore(0x40, add(data, add(0x80, mul(maxArrLength, 0x40))))
mstore(add(data, 0x00), 0)
mstore(add(data, 0x20), 0)
mstore(add(data, 0x40), add(data, 0x60))
mstore(add(data, 0x60), maxArrLength)
}

}

/**
* @notice Appends an OraclePrice to the oraclePrices array in the provided Data struct if the OraclePrice has a non-zero weight
* @dev If the weight of the OraclePrice is greater than the current maxOracleWeight, the maxOracleWeight is updated. The size (number of meaningful elements) of the array is incremented after appending the OraclePrice.
* @param data The Data struct that contains the oraclePrices array, maxOracleWeight, and the current size
* @param oraclePrice The OraclePrice to be appended to the oraclePrices array
* @return isAppended A flag indicating whether the oraclePrice was appended or not
*/
function append(Data memory data, OraclePrice memory oraclePrice) internal pure returns (bool isAppended) {
if (oraclePrice.weight > 0) {
data.oraclePrices[data.size] = oraclePrice;
data.size++;
if (oraclePrice.weight > data.maxOracleWeight) {
data.maxOracleWeight = oraclePrice.weight;
}
return true;
}
return false;
}

/**
* @notice Calculates the weighted rate from the oracle prices data using a threshold filter
* @dev Shrinks the `oraclePrices` array to remove any unused space, then calculates the weighted rate
* considering only the oracle prices whose weight is above the threshold which is percent from max weight
* @param data The data structure containing oracle prices, the maximum oracle weight and the size of the used oracle prices array
* @param thresholdFilter The threshold to filter oracle prices based on their weight
* @return weightedRate The calculated weighted rate
* @return totalWeight The total weight of the oracle prices that passed the threshold
*/
function getRateAndWeight(Data memory data, uint256 thresholdFilter) internal pure returns (uint256 weightedRate, uint256 totalWeight) {
// shrink oraclePrices array
uint256 size = data.size;
assembly ("memory-safe") { // solhint-disable-line no-inline-assembly
let ptr := mload(add(data, 64))
mstore(ptr, size)
}
ZumZoom marked this conversation as resolved.
Show resolved Hide resolved

// calculate weighted rate
for (uint256 i = 0; i < size; i++) {
if (data.oraclePrices[i].weight * 100 < data.maxOracleWeight * thresholdFilter) {
continue;
}
weightedRate += data.oraclePrices[i].rate * data.oraclePrices[i].weight;
totalWeight += data.oraclePrices[i].weight;
}
if (totalWeight > 0) {
unchecked { weightedRate /= totalWeight; }
}
}

/**
* @notice See `getRateAndWeight`. It uses SafeMath to prevent overflows.
*/
function getRateAndWeightWithSafeMath(Data memory data, uint256 thresholdFilter) internal pure returns (uint256 weightedRate, uint256 totalWeight) {
// shrink oraclePrices array
uint256 size = data.size;
assembly ("memory-safe") { // solhint-disable-line no-inline-assembly
let ptr := mload(add(data, 64))
mstore(ptr, size)
}

// calculate weighted rate
for (uint256 i = 0; i < size; i++) {
if (data.oraclePrices[i].weight * 100 < data.maxOracleWeight * thresholdFilter) {
continue;
}
(bool ok, uint256 weightedRateI) = data.oraclePrices[i].rate.tryMul(data.oraclePrices[i].weight);
if (ok) {
(ok, weightedRate) = _tryAdd(weightedRate, weightedRateI);
if (ok) totalWeight += data.oraclePrices[i].weight;
}
}
if (totalWeight > 0) {
unchecked { weightedRate /= totalWeight; }
}
}

function _tryAdd(uint256 value, uint256 addition) private pure returns (bool, uint256) {
unchecked {
uint256 result = value + addition;
if (result < value) return (false, value);
return (true, result);
}
}
}
20 changes: 0 additions & 20 deletions contracts/libraries/Sqrt.sol

This file was deleted.

Loading