Skip to content

Commit

Permalink
feat: calculations for payout
Browse files Browse the repository at this point in the history
  • Loading branch information
xenide committed Jul 26, 2024
1 parent 85cc147 commit 33d0980
Show file tree
Hide file tree
Showing 5 changed files with 55 additions and 51 deletions.
85 changes: 40 additions & 45 deletions src/ReservoirPriceOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ contract ReservoirPriceOracle is IPriceOracle, Owned(msg.sender), ReentrancyGuar
// EVENTS //
///////////////////////////////////////////////////////////////////////////////////////////////

event BpDiffForMaxReward(address token0, address token1, uint16 bpForMaxReward);
event DesignatePair(address token0, address token1, ReservoirPair pair);
event FallbackOracleSet(address fallbackOracle);
event PriceDeviationThreshold(uint256 newThreshold);
event RewardGasAmount(uint256 newAmount);
event Route(address token0, address token1, address[] route);
event TwapPeriod(uint256 newPeriod);
Expand All @@ -44,12 +44,7 @@ contract ReservoirPriceOracle is IPriceOracle, Owned(msg.sender), ReentrancyGuar
/// @dev If `address(0)` then there is no fallback.
address public fallbackOracle;

// The following 3 storage variables take up 1 storage slot.

/// @notice percentage change greater than which, a price update may result in a reward payout of native tokens,
/// subject to availability of rewards.
/// 1e18 == 100%
uint64 public priceDeviationThreshold;
// The following 2 storage variables take up 1 storage slot.

/// @notice This number is multiplied by the base fee to determine the reward for keepers.
uint64 public rewardGasAmount;
Expand All @@ -64,8 +59,7 @@ contract ReservoirPriceOracle is IPriceOracle, Owned(msg.sender), ReentrancyGuar
// CONSTRUCTOR, FALLBACKS //
///////////////////////////////////////////////////////////////////////////////////////////////

constructor(uint64 aThreshold, uint64 aTwapPeriod, uint64 aMultiplier, PriceType aType) {
updatePriceDeviationThreshold(aThreshold);
constructor(uint64 aTwapPeriod, uint64 aMultiplier, PriceType aType) {
updateTwapPeriod(aTwapPeriod);
updateRewardGasAmount(aMultiplier);
PRICE_TYPE = aType;
Expand Down Expand Up @@ -128,7 +122,8 @@ contract ReservoirPriceOracle is IPriceOracle, Owned(msg.sender), ReentrancyGuar
function updatePrice(address aTokenA, address aTokenB, address aRewardRecipient) external nonReentrant {
(address lToken0, address lToken1) = Utils.sortTokens(aTokenA, aTokenB);

(address[] memory lRoute,, uint256 lPrevPrice,) = _getRouteDecimalDifferencePrice(lToken0, lToken1);
(address[] memory lRoute,, uint256 lPrevPrice, uint256 lBpDiffForMaxReward) =
_getRouteDecimalDifferencePrice(lToken0, lToken1);
if (lRoute.length == 0) revert OracleErrors.NoPath();

for (uint256 i = 0; i < lRoute.length - 1; ++i) {
Expand All @@ -150,11 +145,7 @@ contract ReservoirPriceOracle is IPriceOracle, Owned(msg.sender), ReentrancyGuar
}

_writePriceCache(lToken0, lToken1, lNewPrice);

// determine if price has moved beyond the threshold, and pay out reward if so
if (_calcPercentageDiff(lPrevPrice, lNewPrice) >= priceDeviationThreshold) {
_rewardUpdater(aRewardRecipient);
}
_rewardUpdater(lPrevPrice, lNewPrice, aRewardRecipient, lBpDiffForMaxReward);
}
}

Expand Down Expand Up @@ -192,9 +183,17 @@ contract ReservoirPriceOracle is IPriceOracle, Owned(msg.sender), ReentrancyGuar
}
}

function _rewardUpdater(address aRecipient) private {
function _rewardUpdater(uint256 aPrevPrice, uint256 aNewPrice, address aRecipient, uint256 aBpDiffForMaxReward)
private
{
uint256 lPercentDiff = _calcPercentageDiff(aPrevPrice, aNewPrice);
if (lPercentDiff == 0) return;
if (aRecipient == address(0)) return;

// can be unchecked
uint256 lBpForMaxRewardWAD = aBpDiffForMaxReward * Constants.WAD / Constants.BP_SCALE;
uint256 lReward = lPercentDiff > lBpForMaxRewardWAD ? lBpForMaxRewardWAD : lPercentDiff;

// N.B. Revisit this whenever deployment on a new chain is needed
//
// we use `block.basefee` instead of `ArbGasInfo::getMinimumGasPrice()`
Expand All @@ -205,7 +204,7 @@ contract ReservoirPriceOracle is IPriceOracle, Owned(msg.sender), ReentrancyGuar
// SAFETY: this mul will not overflow even in extreme cases of `block.basefee`.
uint256 lPayoutAmt;
unchecked {
lPayoutAmt = block.basefee * rewardGasAmount;
lPayoutAmt = block.basefee * rewardGasAmount * lReward / lBpForMaxRewardWAD;
}

// does not revert under any circumstance
Expand Down Expand Up @@ -270,7 +269,7 @@ contract ReservoirPriceOracle is IPriceOracle, Owned(msg.sender), ReentrancyGuar

/// Calculate the storage slot for this intermediate segment and read it to see if there is an existing
/// route. If there isn't an existing route, we write it as well.
function _checkAndPopulateIntermediateRoute(address aTokenA, address aTokenB) private {
function _checkAndPopulateIntermediateRoute(address aTokenA, address aTokenB, uint16 aBpMaxReward) private {
(address lToken0, address lToken1) = Utils.sortTokens(aTokenA, aTokenB);

bytes32 lSlot = Utils.calculateSlot(lToken0, lToken1);
Expand All @@ -282,16 +281,14 @@ contract ReservoirPriceOracle is IPriceOracle, Owned(msg.sender), ReentrancyGuar
address[] memory lIntermediateRoute = new address[](2);
lIntermediateRoute[0] = lToken0;
lIntermediateRoute[1] = lToken1;
setRoute(lToken0, lToken1, lIntermediateRoute);
uint16[] memory asd = new uint16[](1);
asd[0] = aBpMaxReward;
setRoute(lToken0, lToken1, lIntermediateRoute, asd);
}
}

// performs an SLOAD to load 1 word which contains the simple price and decimal difference
function _priceCache(address aToken0, address aToken1)
private
view
returns (uint256 rPrice, int256 rDecimalDiff)
{
function _priceCache(address aToken0, address aToken1) private view returns (uint256 rPrice, int256 rDecimalDiff) {
bytes32 lSlot = Utils.calculateSlot(aToken0, aToken1);

bytes32 lData;
Expand All @@ -314,9 +311,7 @@ contract ReservoirPriceOracle is IPriceOracle, Owned(msg.sender), ReentrancyGuar
}
if (!lData.isSimplePrice()) revert OracleErrors.WriteToNonSimpleRoute();

int256 lDiff = lData.getDecimalDifference();

lData = RoutesLib.packSimplePrice(lDiff, aNewPrice);
lData = RoutesLib.packSimplePrice(lData.getDecimalDifference(), aNewPrice, lData.getBpDiffForMaxReward());
assembly ("memory-safe") {
sstore(lSlot, lData)
}
Expand Down Expand Up @@ -421,15 +416,6 @@ contract ReservoirPriceOracle is IPriceOracle, Owned(msg.sender), ReentrancyGuar
emit FallbackOracleSet(aFallbackOracle);
}

function updatePriceDeviationThreshold(uint64 aNewThreshold) public onlyOwner {
if (aNewThreshold > Constants.MAX_DEVIATION_THRESHOLD) {
revert OracleErrors.PriceDeviationThresholdTooHigh();
}

priceDeviationThreshold = aNewThreshold;
emit PriceDeviationThreshold(aNewThreshold);
}

function updateTwapPeriod(uint64 aNewPeriod) public onlyOwner {
if (aNewPeriod == 0 || aNewPeriod > Constants.MAX_TWAP_PERIOD) {
revert OracleErrors.InvalidTwapPeriod();
Expand Down Expand Up @@ -461,16 +447,21 @@ contract ReservoirPriceOracle is IPriceOracle, Owned(msg.sender), ReentrancyGuar
emit DesignatePair(aToken0, aToken1, ReservoirPair(address(0)));
}

/// @notice Sets the price route between aToken0 and aToken1, and also intermediate routes if previously undefined
/// @param aToken0 Address of the lower token
/// @param aToken1 Address of the higher token
/// @param aRoute Path with which the price between aToken0 and aToken1 should be derived
function setRoute(address aToken0, address aToken1, address[] memory aRoute) public onlyOwner {
/// @notice Sets the price route between aToken0 and aToken1, and also intermediate routes if previously undefined.
/// @param aToken0 Address of the lower token.
/// @param aToken1 Address of the higher token.
/// @param aRoute Path with which the price between aToken0 and aToken1 should be derived.
/// @param aBpDiffForMaxReward Array of basis points at and beyond which the bounty payout for a price update is maximum.
function setRoute(address aToken0, address aToken1, address[] memory aRoute, uint16[] memory aBpDiffForMaxReward)
public
onlyOwner
{
uint256 lRouteLength = aRoute.length;

_validateTokens(aToken0, aToken1);
if (lRouteLength > Constants.MAX_ROUTE_LENGTH || lRouteLength < 2) revert OracleErrors.InvalidRouteLength();
if (aRoute[0] != aToken0 || aRoute[lRouteLength - 1] != aToken1) revert OracleErrors.InvalidRoute();
if (aBpDiffForMaxReward.length != lRouteLength - 1) revert OracleErrors.InvalidArrayLengthBpForMaxReward();

bytes32 lSlot = Utils.calculateSlot(aToken0, aToken1);

Expand All @@ -482,11 +473,15 @@ contract ReservoirPriceOracle is IPriceOracle, Owned(msg.sender), ReentrancyGuar

int256 lDiff = int256(lToken1Decimals) - int256(lToken0Decimals);

bytes32 lData = RoutesLib.packSimplePrice(lDiff, 0);
if (aBpDiffForMaxReward[0] > Constants.BP_SCALE) revert OracleErrors.InvalidBpForMaxReward();

bytes32 lData = RoutesLib.packSimplePrice(lDiff, 0, aBpDiffForMaxReward[0]);
assembly ("memory-safe") {
// Write data to storage.
sstore(lSlot, lData)
}

emit BpDiffForMaxReward(aToken0, aToken1, aBpDiffForMaxReward[0]);
}
// composite route
else {
Expand All @@ -506,10 +501,10 @@ contract ReservoirPriceOracle is IPriceOracle, Owned(msg.sender), ReentrancyGuar
sstore(lSlot, lFirstWord)
sstore(add(lSlot, 1), lSecondWord)
}
_checkAndPopulateIntermediateRoute(lThirdToken, aToken1);
_checkAndPopulateIntermediateRoute(lThirdToken, aToken1, aBpDiffForMaxReward[2]);
}
_checkAndPopulateIntermediateRoute(aToken0, lSecondToken);
_checkAndPopulateIntermediateRoute(lSecondToken, lThirdToken);
_checkAndPopulateIntermediateRoute(aToken0, lSecondToken, aBpDiffForMaxReward[0]);
_checkAndPopulateIntermediateRoute(lSecondToken, lThirdToken, aBpDiffForMaxReward[1]);
}
emit Route(aToken0, aToken1, aRoute);
}
Expand Down
1 change: 1 addition & 0 deletions src/libraries/Constants.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ library Constants {
uint256 public constant WAD = 1e18;
uint256 public constant MAX_SUPPORTED_PRICE = type(uint128).max;
uint256 public constant MAX_AMOUNT_IN = type(uint128).max;
uint256 public constant BP_SCALE = 1e4;
}
2 changes: 2 additions & 0 deletions src/libraries/OracleErrors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ pragma solidity ^0.8.0;
library OracleErrors {
// config errors
error IncorrectTokensDesignatePair();
error InvalidBpForMaxReward();
error InvalidArrayLengthBpForMaxReward();
error InvalidRoute();
error InvalidRouteLength();
error InvalidTokensProvided();
Expand Down
15 changes: 11 additions & 4 deletions src/libraries/RoutesLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,15 @@ library RoutesLib {

// Assumes that aDecimalDifference is between -18 and 18
// Assumes that aPrice is between 1 and 1e36
function packSimplePrice(int256 aDecimalDifference, uint256 aPrice) internal pure returns (bytes32 rPacked) {
// Assumes that aBpDiffForMaxReward is <= Constants.BP_SCALE
function packSimplePrice(int256 aDecimalDifference, uint256 aPrice, uint16 aBpDiffForMaxReward)
internal
pure
returns (bytes32 rPacked)
{
bytes32 lDecimalDifferenceRaw = bytes1(uint8(int8(aDecimalDifference)));
rPacked = FLAG_SIMPLE_PRICE | lDecimalDifferenceRaw >> 8 | bytes32(aPrice);
bytes32 lBpDiffForMaxReward = bytes2(uint16(aBpDiffForMaxReward));
rPacked = FLAG_SIMPLE_PRICE | lDecimalDifferenceRaw >> 8 | lBpDiffForMaxReward >> 16 | bytes32(aPrice);
}

function pack2HopRoute(address aSecondToken) internal pure returns (bytes32 rPacked) {
Expand Down Expand Up @@ -59,8 +65,9 @@ library RoutesLib {
rPrice = uint256(aData & 0x00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff);
}

function getBpDiffForMaxReward(bytes32 aData) internal pure returns (uint256 rBpDiffForMaxReward) {
rBpDiffForMaxReward = uint256((aData & 0x0000ffff00000000000000000000000000000000000000000000000000000000) >> 224);
function getBpDiffForMaxReward(bytes32 aData) internal pure returns (uint16 rBpDiffForMaxReward) {
rBpDiffForMaxReward =
uint16(uint256((aData & 0x0000ffff00000000000000000000000000000000000000000000000000000000) >> 224));
}

function getTokenFirstWord(bytes32 aData) internal pure returns (address rToken) {
Expand Down
3 changes: 1 addition & 2 deletions test/__fixtures/BaseTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ contract BaseTest is Test {
GenericFactory internal _factory = new GenericFactory();
ReservoirPair internal _pair;

ReservoirPriceOracle internal _oracle =
new ReservoirPriceOracle(0.02e18, 15 minutes, 500_000, PriceType.CLAMPED_PRICE);
ReservoirPriceOracle internal _oracle = new ReservoirPriceOracle(0.15 minutes, 500_000, PriceType.CLAMPED_PRICE);

MintableERC20 internal _tokenA = MintableERC20(address(0x100));
MintableERC20 internal _tokenB = MintableERC20(address(0x200));
Expand Down

0 comments on commit 33d0980

Please sign in to comment.