Skip to content
Open
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
4 changes: 0 additions & 4 deletions contracts/interfaces/IVPoolWrapper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,6 @@ interface IVPoolWrapper {
/// @param protocolFeePips the new protocol fee ratio
event ProtocolFeeUpdated(uint24 protocolFeePips);

/// @notice Emitted when funding rate override is updated
/// @param fundingRateOverrideX128 the new funding rate override value
event FundingRateOverrideUpdated(int256 fundingRateOverrideX128);

function initialize(InitializeVPoolWrapperParams memory params) external;

function vPool() external view returns (IUniswapV3Pool);
Expand Down
151 changes: 151 additions & 0 deletions contracts/libraries/FundingRateOverride.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// SPDX-License-Identifier: UNLICENSED

pragma solidity ^0.8.0;

import { AggregatorV3Interface } from '@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol';

/// @title Funding Rate Override library
/// @notice There are three modes of operation:
/// 1. NULL mode: No override is set, hence protocol uses mark and index prices.
/// 2. ORACLE mode: An address is set and value of FR is queried from it every time.
/// 3. VALUE mode: A fixed value of FR, which stays in effect until it is changed again.
library FundingRateOverride {
using FundingRateOverride for FundingRateOverride.Info;

bytes12 constant PREFIX = 'ADDRESS'; // Fits with address in one word.
bytes32 constant NULL_VALUE = bytes32(uint256(type(int256).max));

struct Info {
bytes32 data;
}

error InvalidFundingRateOracle(address oracle);
error InvalidFundingRateValueX128(int256 value);

/// @notice Emitted when funding rate override is updated
/// @param fundingRateOverrideData the new funding rate override data
event FundingRateOverrideUpdated(bytes32 fundingRateOverrideData);

/// @notice Updates state to not use any funding rate override.
/// @param info the funding rate override state
function setNull(FundingRateOverride.Info storage info) internal {
info.set(NULL_VALUE);
}

/// @notice Updates state to use a chainlink oracle for funding rates
/// @dev The oracle must provide hourly funding rates in D8 format
/// @param info the funding rate override state
/// @param oracle the address of the oracle contract
function setOracle(FundingRateOverride.Info storage info, AggregatorV3Interface oracle) internal {
info.set(packOracleAddress(address(oracle))); // reverts if zero address
}

/// @notice Sets a constant value for funding rate
/// @param info the funding rate override state
/// @param fundingRateOverrideX128 The value of funding rate per sec in X128 format
function setValueX128(FundingRateOverride.Info storage info, int256 fundingRateOverrideX128) internal {
info.set(packInt256(fundingRateOverrideX128)); // reverts if invalid
}

function set(FundingRateOverride.Info storage info, bytes32 data) internal {
info.data = data;
emit FundingRateOverrideUpdated(data);
}

/// @notice Get the funding rate override.
/// @param info The info to get the funding rate override.
/// @return success Whether the funding rate override was successfully retrieved.
/// @return fundingRateX128 The funding rate override.
function getValueX128(FundingRateOverride.Info storage info)
internal
view
returns (bool success, int256 fundingRateX128)
{
// NULL mode: if the data is set to NULL value, then no funding rate override
bytes32 data = info.data;
if (data == NULL_VALUE) {
return (false, 0);
}

// ORACLE mode: if the slot is set to an address, then query override value from the address
address oracle = unpackOracleAddress(data);
if (oracle != address(0)) {
return getValueX128FromOracle(AggregatorV3Interface(oracle));
}

// VALUE mode: use the value in the data slot
return (true, unpackInt256(data));
}

/// @notice Packs an oracle address into a bytes32 variable.
/// @dev Packed into bytes32 as: <bytes20 oracleAddress><bytes12 PREFIX>.
/// @param oracleAddress The address to pack.
/// @return data The packed address.
function packOracleAddress(address oracleAddress) internal pure returns (bytes32 data) {
if (oracleAddress == address(0)) revert InvalidFundingRateOracle(oracleAddress);
assembly {
data := or(shr(160, PREFIX), shl(96, oracleAddress))
}
}

/// @notice Packs the int256 into the data.
/// @param fundingRateOverrideX128 The funding rate override to pack.
/// @return data The funding rate override variable data.
function packInt256(int256 fundingRateOverrideX128) internal pure returns (bytes32 data) {
assembly {
data := fundingRateOverrideX128
}
// ensure the value being packed does not collide with Address or NULL_VALUE
if (fundingRateOverrideX128 == type(int256).max || unpackOracleAddress(data) != address(0)) {
revert InvalidFundingRateValueX128(fundingRateOverrideX128);
}
}

/// @notice Unpacks the slot into address.
/// @param data The funding rate override variable.
/// @return oracleAddress The address if it is packed with the PREFIX, else returns address(0).
function unpackOracleAddress(bytes32 data) internal pure returns (address oracleAddress) {
assembly {
if eq(PREFIX, shl(160, data)) {
oracleAddress := shr(96, data)
}
}
}

/// @notice Unpacks the slot into int256.
/// @dev Does not have sanity checks, null check and unpackOracleAddress should already be tried.
/// @param data The funding rate override variable.
/// @return fundingRateOverrideX128 bytes32 parsed into int256.
function unpackInt256(bytes32 data) internal pure returns (int256 fundingRateOverrideX128) {
assembly {
fundingRateOverrideX128 := data
}
}

/// @notice Gets the funding rate override from the oracle contract.
/// @param oracle The address of the oracle contract.
/// @return success Whether the funding rate override was successfully retrieved.
/// @return fundingRateX128 The funding rate override.
function getValueX128FromOracle(AggregatorV3Interface oracle)
internal
view
returns (bool success, int256 fundingRateX128)
{
bytes4 selector = oracle.latestRoundData.selector;
assembly {
mstore(0, selector)
// only copy first two words of return data to the scratch space
// gas: pass all available gas
// address: oracle address
// argsOffset: use scratch space's starting
// argsSize: 4 bytes of selector
// retOffset: use scratch space's starting
// retSize: two words, i.e. 64 bytes of return data, rest ignore
success := staticcall(gas(), oracle, 0, 4, 0, 64)
if success {
let fundingRateD8 := mload(32) // we only need second word of return data
fundingRateX128 := sdiv(shl(128, fundingRateD8), 360000000000) // divide by 10**8 and 1 hours
}
}
}
}
13 changes: 13 additions & 0 deletions contracts/libraries/SignedMath.sol
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,17 @@ library SignedMath {
if (a > b) c = a;
else c = b;
}

/// @notice if a int256 value is outside a range then give it's closest bound
/// @param val int256 value to bound
/// @param absoluteBound absolute cap of the range
function bound(int256 val, uint256 absoluteBound) internal pure returns (int256) {
int256 bound_ = int256(absoluteBound);
if (val > bound_) {
return bound_;
} else if (val < (bound_ = -bound_)) {
return bound_;
}
return val;
}
}
78 changes: 44 additions & 34 deletions contracts/protocol/wrapper/VPoolWrapper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ pragma solidity =0.8.14;

import { Initializable } from '@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol';

import { AggregatorV3Interface } from '@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol';

import { IUniswapV3Pool } from '@uniswap/v3-core-0.8-support/contracts/interfaces/IUniswapV3Pool.sol';
import { IUniswapV3MintCallback } from '@uniswap/v3-core-0.8-support/contracts/interfaces/callback/IUniswapV3MintCallback.sol';
import { IUniswapV3SwapCallback } from '@uniswap/v3-core-0.8-support/contracts/interfaces/callback/IUniswapV3SwapCallback.sol';
Expand All @@ -20,6 +22,7 @@ import { IClearingHouseStructures } from '../../interfaces/clearinghouse/ICleari

import { AddressHelper } from '../../libraries/AddressHelper.sol';
import { FundingPayment } from '../../libraries/FundingPayment.sol';
import { FundingRateOverride } from '../../libraries/FundingRateOverride.sol';
import { SimulateSwap } from '../../libraries/SimulateSwap.sol';
import { TickExtended } from '../../libraries/TickExtended.sol';
import { PriceMath } from '../../libraries/PriceMath.sol';
Expand All @@ -37,6 +40,7 @@ contract VPoolWrapper is IVPoolWrapper, IUniswapV3MintCallback, IUniswapV3SwapCa
using AddressHelper for IVToken;
using FullMath for uint256;
using FundingPayment for FundingPayment.Info;
using FundingRateOverride for FundingRateOverride.Info;
using SignedMath for int256;
using SignedFullMath for int256;
using PriceMath for uint160;
Expand Down Expand Up @@ -64,8 +68,7 @@ contract VPoolWrapper is IVPoolWrapper, IUniswapV3MintCallback, IUniswapV3SwapCa
FundingPayment.Info public fpGlobal;
uint256 public sumFeeGlobalX128;

int256 constant FUNDING_RATE_OVERRIDE_NULL_VALUE = type(int256).max;
int256 public fundingRateOverrideX128;
FundingRateOverride.Info internal fundingRateOverride;

mapping(int24 => TickExtended.Info) public ticksExtended;

Expand Down Expand Up @@ -128,7 +131,7 @@ contract VPoolWrapper is IVPoolWrapper, IUniswapV3MintCallback, IUniswapV3SwapCa
liquidityFeePips = params.liquidityFeePips;
protocolFeePips = params.protocolFeePips;

fundingRateOverrideX128 = type(int256).max;
fundingRateOverride.setNull();

// initializes the funding payment state by zeroing the funding payment for time 0 to blockTimestamp
fpGlobal.update({
Expand All @@ -152,15 +155,8 @@ contract VPoolWrapper is IVPoolWrapper, IUniswapV3MintCallback, IUniswapV3SwapCa
/// @notice Update the global funding state, from clearing house
/// @dev Done when clearing house is paused or unpaused, to prevent funding payments from being received
/// or paid when clearing house is in paused mode.
function updateGlobalFundingState(bool useZeroFundingRate) public onlyClearingHouse {
(int256 fundingRateX128, uint256 virtualPriceX128) = getFundingRateAndVirtualPrice();
fpGlobal.update({
vTokenAmount: 0,
liquidity: 1,
blockTimestamp: _blockTimestamp(),
virtualPriceX128: virtualPriceX128,
fundingRateX128: useZeroFundingRate ? int256(0) : fundingRateX128
});
function updateGlobalFundingState(bool useZeroFundingRate) external onlyClearingHouse {
_updateGlobalFundingState(useZeroFundingRate);
}

/**
Expand All @@ -179,15 +175,25 @@ contract VPoolWrapper is IVPoolWrapper, IUniswapV3MintCallback, IUniswapV3SwapCa
emit ProtocolFeeUpdated(protocolFeePips_);
}

function setFundingRateOverride(int256 fundingRateOverrideX128_) external onlyGovernanceOrTeamMultisig {
uint256 fundingRateOverrideX128Abs = fundingRateOverrideX128_.absUint();
// ensure that funding rate magnitude is < 100% APR
if (
fundingRateOverrideX128_ != FUNDING_RATE_OVERRIDE_NULL_VALUE &&
fundingRateOverrideX128Abs > FixedPoint128.Q128 / (365 days)
) revert InvalidSetting(0x30);
fundingRateOverrideX128 = fundingRateOverrideX128_;
emit FundingRateOverrideUpdated(fundingRateOverrideX128_);
/// @notice Updates state to not use any funding rate override.
function unsetFundingRateOverride() external onlyGovernanceOrTeamMultisig {
_updateGlobalFundingState({ useZeroFundingRate: true });
fundingRateOverride.setNull();
}

/// @notice Updates state to use a chainlink oracle for funding rates
/// @dev The oracle must provide hourly funding rates in D8 format
/// @param chainlinkOracle The address of the chainlink oracle
function setFundingRateOverride(AggregatorV3Interface chainlinkOracle) external onlyGovernanceOrTeamMultisig {
_updateGlobalFundingState({ useZeroFundingRate: true });
fundingRateOverride.setOracle(chainlinkOracle);
}

/// @notice Sets a constant value for funding rate
/// @param fundingRateOverrideX128 The value of funding rate per sec in X128 format
function setFundingRateOverride(int256 fundingRateOverrideX128) external onlyGovernanceOrTeamMultisig {
_updateGlobalFundingState({ useZeroFundingRate: true });
fundingRateOverride.setValueX128(fundingRateOverrideX128);
}

/**
Expand Down Expand Up @@ -283,7 +289,7 @@ contract VPoolWrapper is IVPoolWrapper, IUniswapV3MintCallback, IUniswapV3SwapCa
)
{
// records the funding payment for last updated timestamp to blockTimestamp using current price difference
_updateGlobalFundingState();
_updateGlobalFundingState({ useZeroFundingRate: false });

wrapperValuesInside = _updateTicks(tickLower, tickUpper, liquidity.toInt128(), vPool.tickCurrent());

Expand Down Expand Up @@ -314,7 +320,7 @@ contract VPoolWrapper is IVPoolWrapper, IUniswapV3MintCallback, IUniswapV3SwapCa
)
{
// records the funding payment for last updated timestamp to blockTimestamp using current price difference
_updateGlobalFundingState();
_updateGlobalFundingState({ useZeroFundingRate: false });

wrapperValuesInside = _updateTicks(tickLower, tickUpper, -liquidity.toInt128(), vPool.tickCurrent());

Expand Down Expand Up @@ -361,21 +367,21 @@ contract VPoolWrapper is IVPoolWrapper, IUniswapV3MintCallback, IUniswapV3SwapCa
VIEW METHODS
*/

function getFundingRateAndVirtualPrice() public view returns (int256 fundingRateX128, uint256 virtualPriceX128) {
int256 _fundingRateOverrideX128 = fundingRateOverrideX128;
bool shouldUseActualPrices = _fundingRateOverrideX128 == FUNDING_RATE_OVERRIDE_NULL_VALUE;

function getFundingRateAndVirtualPrice() public view returns (int256, uint256) {
uint32 poolId = vToken.truncate();
virtualPriceX128 = clearingHouse.getVirtualTwapPriceX128(poolId);
uint256 virtualPriceX128 = clearingHouse.getVirtualTwapPriceX128(poolId);

if (shouldUseActualPrices) {
(bool shouldUseOverrides, int256 fundingRateX128) = fundingRateOverride.getValueX128();
if (!shouldUseOverrides) {
// uses actual price to calculate funding rate
uint256 realPriceX128 = clearingHouse.getRealTwapPriceX128(poolId);
fundingRateX128 = FundingPayment.getFundingRate(realPriceX128, virtualPriceX128);
} else {
// uses funding rate override
fundingRateX128 = _fundingRateOverrideX128;
}

// ensure that abs(funding rate) < 100% APR
fundingRateX128 = fundingRateX128.bound(FixedPoint128.Q128 / (365 days));

return (fundingRateX128, virtualPriceX128);
}

function getSumAX128() external view returns (int256) {
Expand Down Expand Up @@ -435,6 +441,10 @@ contract VPoolWrapper is IVPoolWrapper, IUniswapV3MintCallback, IUniswapV3SwapCa
) = ticksExtended.getTickExtendedStateInside(tickLower, tickUpper, currentTick, _fpGlobal, sumFeeGlobalX128);
}

function getFundingRateOverride() public view returns (bytes32) {
return fundingRateOverride.data;
}

/**
INTERNAL HELPERS
*/
Expand Down Expand Up @@ -529,14 +539,14 @@ contract VPoolWrapper is IVPoolWrapper, IUniswapV3MintCallback, IUniswapV3SwapCa
}

/// @notice Update global funding payment, by getting prices from Clearing House
function _updateGlobalFundingState() internal {
function _updateGlobalFundingState(bool useZeroFundingRate) internal {
(int256 fundingRateX128, uint256 virtualPriceX128) = getFundingRateAndVirtualPrice();
fpGlobal.update({
vTokenAmount: 0,
liquidity: 1,
blockTimestamp: _blockTimestamp(),
virtualPriceX128: virtualPriceX128,
fundingRateX128: fundingRateX128
fundingRateX128: useZeroFundingRate ? int256(0) : fundingRateX128
});
}

Expand Down
Loading