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

Fee boosting improvements #15418

Merged
merged 12 commits into from
Nov 29, 2024
Merged
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
13 changes: 0 additions & 13 deletions .github/e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -974,19 +974,6 @@ runner-test-matrix:
E2E_TEST_SELECTED_NETWORK: SIMULATED_1,SIMULATED_2,SIMULATED_3
E2E_JD_VERSION: 0.6.0

- id: smoke/ccip/fee_boosting_test.go:*
0xnogo marked this conversation as resolved.
Show resolved Hide resolved
0xnogo marked this conversation as resolved.
Show resolved Hide resolved
path: integration-tests/smoke/ccip/fee_boosting_test.go
test_env_type: docker
runs_on: ubuntu-latest
triggers:
- PR E2E Core Tests
- Nightly E2E Tests
test_cmd: cd integration-tests/smoke/ccip && go test fee_boosting_test.go -timeout 15m -test.parallel=1 -count=1 -json
pyroscope_env: ci-smoke-ccipv1_6-evm-simulated
test_env_vars:
E2E_TEST_SELECTED_NETWORK: SIMULATED_1,SIMULATED_2
E2E_JD_VERSION: 0.6.0

- id: smoke/ccip/ccip_rmn_test.go:^TestRMN_TwoMessagesOnTwoLanesIncludingBatching$
path: integration-tests/smoke/ccip/ccip_rmn_test.go
test_env_type: docker
Expand Down
8 changes: 8 additions & 0 deletions .github/integration-in-memory-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,12 @@ runner-test-matrix:
triggers:
- PR Integration CCIP Tests
test_cmd: cd integration-tests/smoke/ccip && go test ccip_messaging_test.go -timeout 12m -test.parallel=2 -count=1 -json

- id: smoke/ccip/ccip_fee_boosting_test.go:*
path: integration-tests/smoke/ccip/ccip_fee_boosting_test.go
test_env_type: in-memory
runs_on: ubuntu-latest
triggers:
- PR Integration CCIP Tests
test_cmd: cd integration-tests/smoke/ccip && go test ccip_fee_boosting_test.go -timeout 12m -test.parallel=2 -count=1 -json
# END: CCIP tests
5 changes: 3 additions & 2 deletions deployment/ccip/changeset/test_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,8 +252,9 @@ func mockAttestationResponse() *httptest.Server {
}

type TestConfigs struct {
IsUSDC bool
IsMultiCall3 bool
IsUSDC bool
IsMultiCall3 bool
OCRConfigOverride func(CCIPOCRParams) CCIPOCRParams
}

func NewMemoryEnvironmentWithJobsAndContracts(t *testing.T, lggr logger.Logger, numChains int, numNodes int, tCfg *TestConfigs) DeployedEnv {
Expand Down
317 changes: 317 additions & 0 deletions integration-tests/smoke/ccip/ccip_fee_boosting_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,317 @@
package smoke

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

"github.com/pkg/errors"

"github.com/smartcontractkit/chainlink-common/pkg/config"

"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/test-go/testify/require"
"golang.org/x/exp/maps"

"github.com/smartcontractkit/chainlink-common/pkg/utils/tests"
"github.com/smartcontractkit/chainlink-testing-framework/lib/utils/testcontext"
"github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/ccipevm"
"github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/fee_quoter"

cciptypes "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3"
"github.com/smartcontractkit/chainlink/deployment"
"github.com/smartcontractkit/chainlink/deployment/ccip/changeset"
"github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/onramp"
"github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/router"
"github.com/smartcontractkit/chainlink/v2/core/logger"
)

var (
linkPrice = deployment.E18Mult(100)
wethPrice = deployment.E18Mult(4000)
)

func Test_CCIPFeeBoosting(t *testing.T) {
e := changeset.NewMemoryEnvironmentWithJobsAndContracts(t, logger.TestLogger(t), 2, 4, &changeset.TestConfigs{
OCRConfigOverride: func(params changeset.CCIPOCRParams) changeset.CCIPOCRParams {
// Only 1 boost (=OCR round) is enough to cover the fee
0xnogo marked this conversation as resolved.
Show resolved Hide resolved
params.ExecuteOffChainConfig.RelativeBoostPerWaitHour = 10
// Disable token price updates
params.CommitOffChainConfig.TokenPriceBatchWriteFrequency = *config.MustNewDuration(1_000_000 * time.Hour)
// Disable gas price updates
params.CommitOffChainConfig.RemoteGasPriceBatchWriteFrequency = *config.MustNewDuration(1_000_000 * time.Hour)
// Disable token price updates
params.CommitOffChainConfig.TokenInfo = nil
return params
},
})

state, err := changeset.LoadOnchainState(e.Env)
require.NoError(t, err)

allChainSelectors := maps.Keys(e.Env.Chains)
require.Len(t, allChainSelectors, 2)
sourceChain := allChainSelectors[0]
destChain := allChainSelectors[1]
t.Log("All chain selectors:", allChainSelectors,
", home chain selector:", e.HomeChainSel,
", feed chain selector:", e.FeedChainSel,
", source chain selector:", sourceChain,
", dest chain selector:", destChain,
)

fetchedGasPriceDest, err := e.Env.Chains[destChain].Client.SuggestGasPrice(tests.Context(t))
require.NoError(t, err)
originalGasPriceDestUSD := new(big.Int).Div(
new(big.Int).Mul(fetchedGasPriceDest, wethPrice),
big.NewInt(1e18),
)
t.Log("Gas price on dest chain (USD):", originalGasPriceDestUSD)

// Adjust destination gas price on source fee quoter to 95% of the current value
adjustedGasPriceDest :=
new(big.Int).Div(
new(big.Int).Mul(originalGasPriceDestUSD, big.NewInt(95)),
big.NewInt(100),
)
t.Log("Adjusted gas price on dest chain:", adjustedGasPriceDest)

initialPrices := changeset.InitialPrices{
LinkPrice: linkPrice,
WethPrice: wethPrice,
GasPrice: changeset.ToPackedFee(adjustedGasPriceDest, big.NewInt(0)),
}

laneCfg := changeset.LaneConfig{
SourceSelector: sourceChain,
DestSelector: destChain,
InitialPricesBySource: initialPrices,
FeeQuoterDestChain: changeset.DefaultFeeQuoterDestChainConfig(),
}
require.NoError(t, changeset.AddLane(e.Env, state, laneCfg, false))

// Update token prices in destination chain FeeQuoter
err = updateTokensPrices(e, state, destChain, map[common.Address]*big.Int{
state.Chains[destChain].LinkToken.Address(): linkPrice,
state.Chains[destChain].Weth9.Address(): wethPrice,
})
require.NoError(t, err)

startBlocks := make(map[uint64]*uint64)
expectedSeqNum := make(map[changeset.SourceDestPair]uint64)
expectedSeqNumExec := make(map[changeset.SourceDestPair][]uint64)

latesthdr, err := e.Env.Chains[sourceChain].Client.HeaderByNumber(testcontext.Get(t), nil)
require.NoError(t, err)
block := latesthdr.Number.Uint64()
msgSentEvent := changeset.TestSendRequest(t, e.Env, state, sourceChain, destChain, false, router.ClientEVM2AnyMessage{
Receiver: common.LeftPadBytes(state.Chains[destChain].Receiver.Address().Bytes(), 32),
Data: []byte("message that needs fee boosting"),
TokenAmounts: nil,
FeeToken: common.HexToAddress("0x0"),
ExtraArgs: nil,
})
startBlocks[sourceChain] = &block
expectedSeqNum[changeset.SourceDestPair{
SourceChainSelector: sourceChain,
DestChainSelector: destChain,
}] = msgSentEvent.SequenceNumber
expectedSeqNumExec[changeset.SourceDestPair{
SourceChainSelector: sourceChain,
DestChainSelector: destChain,
}] = []uint64{msgSentEvent.SequenceNumber}

err = updateGasPrice(e, state, sourceChain, destChain, originalGasPriceDestUSD)
require.NoError(t, err)

// Confirm gas prices are updated
srcFeeQuoter := state.Chains[sourceChain].FeeQuoter
err = changeset.ConfirmGasPriceUpdated(t, e.Env.Chains[destChain], srcFeeQuoter, 0, originalGasPriceDestUSD)
require.NoError(t, err)

// Confirm that fee boosting will be triggered
require.True(t, willTriggerFeeBoosting(t, msgSentEvent, state, sourceChain, destChain))

// hack
time.Sleep(30 * time.Second)
replayBlocks := make(map[uint64]uint64)
replayBlocks[sourceChain] = 1
replayBlocks[destChain] = 1
changeset.ReplayLogs(t, e.Env.Offchain, replayBlocks)

// Confirm that the message is committed and executed
changeset.ConfirmCommitForAllWithExpectedSeqNums(t, e.Env, state, expectedSeqNum, startBlocks)
changeset.ConfirmExecWithSeqNrsForAll(t, e.Env, state, expectedSeqNumExec, startBlocks)
}

// TODO: Find a more accurate way to determine if fee boosting will be triggered
func willTriggerFeeBoosting(
t *testing.T,
msgSentEvent *onramp.OnRampCCIPMessageSent,
state changeset.CCIPOnChainState,
srcChain, destChain uint64) bool {
msg := convertToMessage(msgSentEvent.Message)
t.Log("\n=== Fee Boosting Analysis ===")
t.Logf("Src Chain: %d", msg.Header.SourceChainSelector)
t.Logf("Dest Chain: %d", msg.Header.DestChainSelector)

ep := ccipevm.NewGasEstimateProvider()
chainState, exists := state.Chains[srcChain]
require.True(t, exists)
feeQuoter := chainState.FeeQuoter

premium, err := feeQuoter.GetPremiumMultiplierWeiPerEth(&bind.CallOpts{Context: context.Background()}, chainState.Weth9.Address())
require.NoError(t, err)
t.Logf("Premium: %d", premium)

// Get LINK price
linkPrice, err := feeQuoter.GetTokenPrice(&bind.CallOpts{Context: context.Background()}, chainState.LinkToken.Address())
require.NoError(t, err)
t.Logf("LINK Price: %s", linkPrice.Value.String())
t.Logf("Juels in message: %s", msg.FeeValueJuels.String())

// Calculate fee in USD token
fee := new(big.Int).Div(
new(big.Int).Mul(linkPrice.Value, msg.FeeValueJuels.Int),
new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil),
)
t.Logf("Fee paid (in USD token): %s", fee.String())

// Calculate message gas
messageGas := new(big.Int).SetUint64(ep.CalculateMessageMaxGas(msg))
t.Logf("Estimated message gas: %s", messageGas.String())

// Get token and gas prices
nativeTokenAddress := chainState.Weth9.Address()
tokenAndGasPrice, err := feeQuoter.GetTokenAndGasPrices(&bind.CallOpts{Context: context.Background()}, nativeTokenAddress, destChain)
require.NoError(t, err)
t.Logf("Raw gas price (uint224): %s for chain: %d", tokenAndGasPrice.GasPriceValue.String(), destChain)

// Extract uint112 gas price
gasPrice, err := convertGasPriceToUint112(tokenAndGasPrice.GasPriceValue)
require.NoError(t, err)
t.Logf("Extracted gas price (uint112): %s", gasPrice.String())
t.Logf("Native token price: %s", tokenAndGasPrice.TokenPrice.String())

// Calculate total execution cost
execCost := new(big.Int).Mul(messageGas, gasPrice)
t.Logf("Total execution cost: %s", execCost.String())

// Check if fee boosting will trigger
willBoost := execCost.Cmp(fee) > 0
t.Logf("\nWill fee boosting trigger? %v", willBoost)
t.Logf("Execution cost / Fee ratio: %.2f",
new(big.Float).Quo(
new(big.Float).SetInt(execCost),
new(big.Float).SetInt(fee),
),
)

return execCost.Cmp(fee) > 0
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit

Suggested change
return execCost.Cmp(fee) > 0
return willBoost

}

func convertGasPriceToUint112(gasPrice *big.Int) (*big.Int, error) {
// Create a mask for uint112 (112 bits of 1s)
mask := new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 112), big.NewInt(1))

// Extract the lower 112 bits using AND operation
result := new(big.Int).And(gasPrice, mask)

return result, nil
}

func convertToMessage(msg onramp.InternalEVM2AnyRampMessage) cciptypes.Message {
// Convert header
header := cciptypes.RampMessageHeader{
MessageID: cciptypes.Bytes32(msg.Header.MessageId),
SourceChainSelector: cciptypes.ChainSelector(msg.Header.SourceChainSelector),
DestChainSelector: cciptypes.ChainSelector(msg.Header.DestChainSelector),
SequenceNumber: cciptypes.SeqNum(msg.Header.SequenceNumber),
Nonce: msg.Header.Nonce,
}

// Convert token amounts
tokenAmounts := make([]cciptypes.RampTokenAmount, len(msg.TokenAmounts))
for i, ta := range msg.TokenAmounts {
tokenAmounts[i] = cciptypes.RampTokenAmount{
SourcePoolAddress: cciptypes.UnknownAddress(ta.SourcePoolAddress.Bytes()),
DestTokenAddress: cciptypes.UnknownAddress(ta.DestTokenAddress),
ExtraData: cciptypes.Bytes(ta.ExtraData),
Amount: cciptypes.BigInt{Int: ta.Amount},
DestExecData: cciptypes.Bytes(ta.DestExecData),
}
}

return cciptypes.Message{
Header: header,
Sender: cciptypes.UnknownAddress(msg.Sender.Bytes()),
Data: cciptypes.Bytes(msg.Data),
Receiver: cciptypes.UnknownAddress(msg.Receiver),
ExtraArgs: cciptypes.Bytes(msg.ExtraArgs),
FeeToken: cciptypes.UnknownAddress(msg.FeeToken.Bytes()),
FeeTokenAmount: cciptypes.BigInt{Int: msg.FeeTokenAmount},
FeeValueJuels: cciptypes.BigInt{Int: msg.FeeValueJuels},
TokenAmounts: tokenAmounts,
}
}

func updateGasPrice(env changeset.DeployedEnv, state changeset.CCIPOnChainState, srcChain, destChain uint64, gasPrice *big.Int) error {
chainState, exists := state.Chains[srcChain]
if !exists {
return fmt.Errorf("chain state not found for selector: %d", srcChain)
}

feeQuoter := chainState.FeeQuoter
// Update gas price
auth := env.Env.Chains[srcChain].DeployerKey
tx, err := feeQuoter.UpdatePrices(auth, fee_quoter.InternalPriceUpdates{
TokenPriceUpdates: nil,
GasPriceUpdates: []fee_quoter.InternalGasPriceUpdate{
{
DestChainSelector: destChain,
UsdPerUnitGas: gasPrice,
},
},
})
if err != nil {
return errors.Wrapf(err, "updating gas price on chain %d", srcChain)
}
if _, err := deployment.ConfirmIfNoError(env.Env.Chains[srcChain], tx, err); err != nil {
return err
}

return nil
}

func updateTokensPrices(env changeset.DeployedEnv, state changeset.CCIPOnChainState, chain uint64, tokenPrices map[common.Address]*big.Int) error {
chainState, exists := state.Chains[chain]
if !exists {
return fmt.Errorf("chain state not found for selector: %d", chain)
}

feeQuoter := chainState.FeeQuoter
// Update token prices
auth := env.Env.Chains[chain].DeployerKey
tokenPricesUpdates := make([]fee_quoter.InternalTokenPriceUpdate, 0, len(tokenPrices))
for token, price := range tokenPrices {
tokenPricesUpdates = append(tokenPricesUpdates, fee_quoter.InternalTokenPriceUpdate{
SourceToken: token,
UsdPerToken: price,
})
}
tx, err := feeQuoter.UpdatePrices(auth, fee_quoter.InternalPriceUpdates{
TokenPriceUpdates: tokenPricesUpdates,
GasPriceUpdates: nil,
})
if err != nil {
return errors.Wrapf(err, "updating token prices on chain %d", chain)
}
if _, err := deployment.ConfirmIfNoError(env.Env.Chains[chain], tx, err); err != nil {
return err
}

return nil
}
Loading
Loading