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

Port curve-based gas deviation logic over from chainlink repo #526

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
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
22 changes: 20 additions & 2 deletions commit/chainfee/outcome.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ import (
cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3"
)

const (
// ExecNoDeviationThresholdUSD is the lower bound no deviation threshold for exec gas. If the exec gas price is
// less than this value, we should never trigger a deviation. This is set to 10 gwei in USD terms.
ExecNoDeviationThresholdUSD = 10e9

// DataAvNoDeviationThresholdUSD is the lower bound no deviation threshold for DA gas. If the DA gas price is less
// than this value, we should never trigger a deviation. This is set to 20 gwei in USD terms.
DataAvNoDeviationThresholdUSD = 20e9
)

func (p *processor) Outcome(
ctx context.Context,
_ Outcome,
Expand Down Expand Up @@ -244,21 +254,29 @@ func (p *processor) getGasPricesToUpdate(
if feeInfo == nil {
continue
}

ci, ok := feeInfo[chain]
if !ok {
lggr.Warnf("could not find fee info for chain %d", chain)
continue
}

executionFeeDeviates := mathslib.Deviates(
if ci.DisableChainFeeDeviationCheck {
lggr.Debugf("chain fee deviation check disabled, skipping deviation check for chain %d", chain)
continue
}

executionFeeDeviates := mathslib.DeviatesOnCurve(
currentChainFee.ExecutionFeePriceUSD,
lastUpdate.ChainFee.ExecutionFeePriceUSD,
big.NewInt(ExecNoDeviationThresholdUSD),
ci.ExecDeviationPPB.Int64(),
)

dataAvFeeDeviates := mathslib.Deviates(
dataAvFeeDeviates := mathslib.DeviatesOnCurve(
currentChainFee.DataAvFeePriceUSD,
lastUpdate.ChainFee.DataAvFeePriceUSD,
big.NewInt(DataAvNoDeviationThresholdUSD),
ci.DataAvailabilityDeviationPPB.Int64(),
)

Expand Down
56 changes: 56 additions & 0 deletions internal/libs/mathslib/calc.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
package mathslib

import (
"math"
"math/big"
)

const (
// CurveBasedDeviationPPB is the deviation threshold when writing to Ethereum's PriceRegistry and is the trigger for
// using curve-based deviation logic.
CurveBasedDeviationPPB = 4e9
)

// Deviates checks if x1 and x2 deviates based on the provided ppb (parts per billion)
// ppb is calculated based on the smaller value of the two
// e.g, if x1 > x2, deviation_parts_per_billion = ((x1 - x2) / x2) * 1e9
Expand All @@ -29,3 +36,52 @@ func CalculateUsdPerUnitGas(sourceGasPrice *big.Int, usdPerFeeCoin *big.Int) *bi
tmp := new(big.Int).Mul(sourceGasPrice, usdPerFeeCoin)
return tmp.Div(tmp, big.NewInt(1e18))
}

// DeviatesOnCurve calculates a deviation threshold on the fly using xNew. For now it's only used for gas price
// deviation calculation. It's important to make sure the order of xNew and xOld is correct when passed into this
// function to get an accurate deviation threshold.
func DeviatesOnCurve(xNew, xOld, noDeviationLowerBound *big.Int, ppb int64) bool {
// This is a temporary gating mechanism that ensures we only apply the gas curve deviation logic to eth-bound price
// updates. If ppb from config is not equal to 4000000000, do not apply the gas curve.
if ppb != CurveBasedDeviationPPB {
return Deviates(xOld, xNew, ppb)
}

// If xNew < noDeviationLowerBound, Deviates should never be true
if xNew.Cmp(noDeviationLowerBound) < 0 {
return false
}

xNewFloat := new(big.Float).SetInt(xNew)
xNewFloat64, _ := xNewFloat.Float64()

// We use xNew to generate the threshold so that when going from cheap --> expensive, xNew generates a smaller
// deviation threshold so we are more likely to update the gas price on chain. When going from expensive --> cheap,
// xNew generates a larger deviation threshold since it's not as urgent to update the gas price on chain.
curveThresholdPPB := calculateCurveThresholdPPB(xNewFloat64)
return Deviates(xNew, xOld, curveThresholdPPB)
}

// calculateCurveThresholdPPB calculates the deviation threshold percentage with x using the formula:
// y = (10e11) / (x^0.665). This sliding scale curve was created by collecting several thousands of historical
// PriceRegistry gas price update samples from chains with volatile gas prices like Zircuit and Mode and then using that
// historical data to define thresholds of gas deviations that were acceptable given their USD value. The gas prices
// (X coordinates) and these new thresholds (Y coordinates) were used to fit this sliding scale curve that returns large
// thresholds at low gas prices and smaller thresholds at higher gas prices. Constructing the curve in USD terms allows
// us to more easily reason about these thresholds and also better translates to non-evm chains. For example, when the
// per unit gas price is 0.000006 USD, (6e12 USDgwei), the curve will output a threshold of around 3,000%. However, when
// the per unit gas price is 0.005 USD (5e15 USDgwei), the curve will output a threshold of only ~30%.
func calculateCurveThresholdPPB(x float64) int64 {
const constantFactor = 10e11
const exponent = 0.665
xPower := math.Pow(x, exponent)
threshold := constantFactor / xPower

// Convert curve output percentage to PPB
thresholdPPB := int64(threshold * 1e7)
return thresholdPPB
}

func MergeEpochAndRound(epoch uint32, round uint8) uint64 {
return uint64(epoch)<<8 + uint64(round)
}
149 changes: 149 additions & 0 deletions internal/libs/mathslib/calc_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package mathslib

import (
"fmt"
"math/big"
"testing"

Expand Down Expand Up @@ -128,3 +129,151 @@ func TestCalculateUsdPerUnitGas(t *testing.T) {
})
}
}

func TestDeviatesOnCurve(t *testing.T) {
type args struct {
xNew *big.Int
xOld *big.Int
noDev *big.Int
ppb int64
}
tests := []struct {
name string
args args
want bool
}{
{
name: "base case deviates from increase",
args: args{xNew: big.NewInt(4e14), xOld: big.NewInt(1e13), noDev: big.NewInt(3e13), ppb: CurveBasedDeviationPPB},
want: true,
},
{
name: "base case deviates from decrease",
args: args{xNew: big.NewInt(1e13), xOld: big.NewInt(4e15), noDev: big.NewInt(1), ppb: CurveBasedDeviationPPB},
want: true,
},
{
name: "does not deviate when equal",
args: args{xNew: big.NewInt(3e14), xOld: big.NewInt(3e14), noDev: big.NewInt(3e13), ppb: CurveBasedDeviationPPB},
want: false,
},
{
name: "does not deviate with small difference when xNew is bigger",
args: args{xNew: big.NewInt(3e14 + 1), xOld: big.NewInt(3e14), noDev: big.NewInt(3e13), ppb: CurveBasedDeviationPPB},
want: false,
},
{
name: "does not deviate with small difference when xOld is bigger",
args: args{xNew: big.NewInt(3e14), xOld: big.NewInt(3e14 + 1), noDev: big.NewInt(3e13), ppb: CurveBasedDeviationPPB},
want: false,
},
{
name: "deviates when ppb is not equal to CurveBasedDeviationPPB",
args: args{xNew: big.NewInt(1e9), xOld: big.NewInt(2e9), noDev: big.NewInt(1), ppb: 1},
want: true,
},
{
name: "does not deviate when xNew is below noDeviationLowerBound",
args: args{xNew: big.NewInt(2e13), xOld: big.NewInt(1e13), noDev: big.NewInt(3e13), ppb: CurveBasedDeviationPPB},
want: false,
},
// thresholdPPB = (10e11) / (xNew^0.665) * 1e7
// diff = (xNew - xOld) / min(xNew, xOld) * 1e9
// Deviates = abs(diff) > thresholdPPB
{
name: "xNew is just below deviation threshold and does deviate",
args: args{
xNew: big.NewInt(3e13),
xOld: big.NewInt(2.519478222838e12),
noDev: big.NewInt(1),
ppb: CurveBasedDeviationPPB,
},
want: false,
},
{
name: "xNew is just above deviation threshold and does deviate",
args: args{
xNew: big.NewInt(3e13),
xOld: big.NewInt(2.519478222838e12 - 30),
noDev: big.NewInt(1),
ppb: CurveBasedDeviationPPB,
},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equalf(
t,
tt.want,
DeviatesOnCurve(tt.args.xNew, tt.args.xOld, tt.args.noDev, tt.args.ppb),
"DeviatesOnCurve(%v, %v, %v, %v)", tt.args.xNew, tt.args.xOld, tt.args.noDev, tt.args.ppb)
})
}
}

func TestCalculateCurveThresholdPPB(t *testing.T) {
tests := []struct {
x float64
ppbLowerBound int64
ppbUpperBound int64
}{
{
x: 500_000,
ppbLowerBound: 1_000_000_000_000_000, // 100,000,000%
ppbUpperBound: 3_000_000_000_000_000, // 300,000,000%
},
{
x: 50_000_000,
ppbLowerBound: 70_000_000_000_000, // 7,000,000%
ppbUpperBound: 90_000_000_000_000, // 9,000,000%
},
{
x: 350_000_000,
ppbLowerBound: 20_000_000_000_000, // 2,000,000%
ppbUpperBound: 30_000_000_000_000, // 3,000,000%
},
{
x: 200_000_000_000,
ppbLowerBound: 300_000_000_000, // 30,000%
ppbUpperBound: 400_000_000_000, // 40,000%
},
{
x: 6_000_000_000_000,
ppbLowerBound: 30_000_000_000, // 3,000%
ppbUpperBound: 40000000000, // 4,000%
},
{
x: 50_000_000_000_000,
ppbLowerBound: 7_000_000_000, // 700%
ppbUpperBound: 8_000_000_000, // 800%
},
{
x: 225_000_000_000_000,
ppbLowerBound: 2_000_000_000, // 200%
ppbUpperBound: 3_000_000_000, // 300%
},
{
x: 5_000_000_000_000_000,
ppbLowerBound: 300_000_000, // 30%
ppbUpperBound: 400_000_000, // 40%
},
{
x: 70_000_000_000_000_000,
ppbLowerBound: 60_000_000, // 6%
ppbUpperBound: 70_000_000, // 7%
},
{
x: 500_000_000_000_000_000,
ppbLowerBound: 10_000_000, // 1%
ppbUpperBound: 20_000_000, // 2%
},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("%f", tt.x), func(t *testing.T) {
thresholdPPB := calculateCurveThresholdPPB(tt.x)
assert.GreaterOrEqual(t, thresholdPPB, tt.ppbLowerBound)
assert.LessOrEqual(t, thresholdPPB, tt.ppbUpperBound)
})
}
}
5 changes: 3 additions & 2 deletions pluginconfig/commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ const (
)

type FeeInfo struct {
ExecDeviationPPB cciptypes.BigInt `json:"execDeviationPPB"`
DataAvailabilityDeviationPPB cciptypes.BigInt `json:"dataAvailabilityDeviationPPB"`
ExecDeviationPPB cciptypes.BigInt `json:"execDeviationPPB"`
DataAvailabilityDeviationPPB cciptypes.BigInt `json:"dataAvailabilityDeviationPPB"`
DisableChainFeeDeviationCheck bool `json:"disableChainFeeDeviationCheck"`
}

type TokenInfo struct {
Expand Down
Loading