diff --git a/chains/solana/contracts/programs/ccip-router/src/fee_quoter.rs b/chains/solana/contracts/programs/ccip-router/src/fee_quoter.rs index a55940fe..7fc80323 100644 --- a/chains/solana/contracts/programs/ccip-router/src/fee_quoter.rs +++ b/chains/solana/contracts/programs/ccip-router/src/fee_quoter.rs @@ -9,6 +9,7 @@ use crate::{ PerChainPerTokenConfig, Solana2AnyMessage, SolanaTokenAmount, UnpackedDoubleU224, CCIP_LOCK_OR_BURN_V1_RET_BYTES, FEE_BILLING_SIGNER_SEEDS, MAX_TOKEN_AND_CHAIN_CONFIG_V, }; + // TODO change args and implement pub fn fee_for_msg( _dest_chain_selector: u64, @@ -43,10 +44,9 @@ pub fn fee_for_msg( let data_availability_cost = 1u32.e(18); let premium_multiplier = U256::new(fee_token_config.premium_multiplier_wei_per_eth.into()); - let amount = (dbg!(network_fee.premium.0) * dbg!(premium_multiplier) - + execution_cost - + data_availability_cost) - / dbg!(fee_token_price.0); + let amount = + (network_fee.premium.0 * premium_multiplier + execution_cost + data_availability_cost) + / fee_token_price.0; let amount: u64 = amount .try_into() .map_err(|_| CcipRouterError::InvalidTokenPrice)?; @@ -54,6 +54,12 @@ pub fn fee_for_msg( Ok(SolanaTokenAmount { amount, token }) } +/// Parses and validates the account slice in the order expected by `get_fees`: +/// +/// * First, the billing token config accounts for each token involved, including the +/// fee token, sequentially. +/// * Then, the per chain / per token config of those tokens, sequentially in the same +/// order, for the destination chain. pub fn get_accounts_for_fee_retrieval<'info>( remaining_accounts: &'info [AccountInfo<'info>], message: &Solana2AnyMessage, diff --git a/chains/solana/contracts/programs/ccip-router/src/lib.rs b/chains/solana/contracts/programs/ccip-router/src/lib.rs index a8c53ebc..977dd516 100644 --- a/chains/solana/contracts/programs/ccip-router/src/lib.rs +++ b/chains/solana/contracts/programs/ccip-router/src/lib.rs @@ -518,11 +518,14 @@ pub mod ccip_router { /// * `cfg` - The token billing configuration. pub fn set_token_billing( ctx: Context, - _chain_selector: u64, - _mint: Pubkey, + chain_selector: u64, + mint: Pubkey, cfg: TokenBilling, ) -> Result<()> { + ctx.accounts.per_chain_per_token_config.version = 1; // update this if we change the account struct ctx.accounts.per_chain_per_token_config.billing = cfg; + ctx.accounts.per_chain_per_token_config.chain_selector = chain_selector; + ctx.accounts.per_chain_per_token_config.mint = mint; Ok(()) } @@ -654,6 +657,16 @@ pub mod ccip_router { /// * `dest_chain_selector` - The chain selector for the destination chain. /// * `message` - The message to be sent. /// + /// # Additional accounts + /// + /// In addition to the fixed amount of accounts defined in the `GetFee` context, + /// the following accounts must be provided: + /// + /// * First, the billing token config accounts for each token involved, including the + /// fee token, sequentially. + /// * Then, the per chain / per token config of those tokens, sequentially in the same + /// order, for the destination chain. + /// /// # Returns /// /// The fee amount in u64. @@ -683,6 +696,16 @@ pub mod ccip_router { /// The message will be sent to the receiver on the destination chain selector. /// This message emits the event CCIPSendRequested with all the necessary data to be retrieved by the OffChain Code /// + /// # Additional accounts + /// + /// In addition to the fixed amount of accounts defined in the `GetFee` context, + /// the following accounts must be provided: + /// + /// * First, the billing token config accounts for each token involved, including the + /// fee token, sequentially. + /// * Then, the per chain / per token config of those tokens, sequentially in the same + /// order, for the destination chain. + /// /// # Arguments /// /// * `ctx` - The context containing the accounts required for sending the message. @@ -698,10 +721,8 @@ pub mod ccip_router { let dest_chain = &mut ctx.accounts.dest_chain_state; - // TODO this is grossly invalid, just putting it here for now to typecheck. These will come from elsewhere (see below how these accounts are retrieved) let (token_billing_config_accounts, per_chain_per_token_config_accounts) = get_accounts_for_fee_retrieval(&ctx.remaining_accounts, &message)?; - let fee = fee_for_msg( dest_chain_selector, &message, diff --git a/chains/solana/contracts/target/idl/ccip_router.json b/chains/solana/contracts/target/idl/ccip_router.json index 58820343..14919986 100644 --- a/chains/solana/contracts/target/idl/ccip_router.json +++ b/chains/solana/contracts/target/idl/ccip_router.json @@ -989,6 +989,16 @@ "* `dest_chain_selector` - The chain selector for the destination chain.", "* `message` - The message to be sent.", "", + "# Additional accounts", + "", + "In addition to the fixed amount of accounts defined in the `GetFee` context,", + "the following accounts must be provided:", + "", + "* First, the billing token config accounts for each token involved, including the", + "fee token, sequentially.", + "* Then, the per chain / per token config of those tokens, sequentially in the same", + "order, for the destination chain.", + "", "# Returns", "", "The fee amount in u64." @@ -1026,6 +1036,16 @@ "The message will be sent to the receiver on the destination chain selector.", "This message emits the event CCIPSendRequested with all the necessary data to be retrieved by the OffChain Code", "", + "# Additional accounts", + "", + "In addition to the fixed amount of accounts defined in the `GetFee` context,", + "the following accounts must be provided:", + "", + "* First, the billing token config accounts for each token involved, including the", + "fee token, sequentially.", + "* Then, the per chain / per token config of those tokens, sequentially in the same", + "order, for the destination chain.", + "", "# Arguments", "", "* `ctx` - The context containing the accounts required for sending the message.", diff --git a/chains/solana/contracts/tests/ccip/ccip_router_test.go b/chains/solana/contracts/tests/ccip/ccip_router_test.go index 60358b0d..b2304386 100644 --- a/chains/solana/contracts/tests/ccip/ccip_router_test.go +++ b/chains/solana/contracts/tests/ccip/ccip_router_test.go @@ -5,6 +5,7 @@ import ( "encoding/binary" "encoding/hex" "fmt" + "math/big" "sort" "testing" @@ -56,15 +57,15 @@ func TestCCIPRouter(t *testing.T) { // billing type AccountsPerToken struct { - name string - program solana.PublicKey - mint solana.PublicKey - billingATA solana.PublicKey - userATA solana.PublicKey - anotherUserATA solana.PublicKey - tokenlessUserATA solana.PublicKey - billingConfigPDA solana.PublicKey - // add other accounts as needed + name string + program solana.PublicKey + mint solana.PublicKey + billingATA solana.PublicKey + userATA solana.PublicKey + anotherUserATA solana.PublicKey + tokenlessUserATA solana.PublicKey + billingConfigPDA solana.PublicKey + perChainPerTokenConfigPDA solana.PublicKey } wsol := AccountsPerToken{name: "WSOL (pre-2022)"} token2022 := AccountsPerToken{name: "Token2022 sample token"} @@ -103,8 +104,13 @@ func TestCCIPRouter(t *testing.T) { } getTokenConfigPDA := func(mint solana.PublicKey) solana.PublicKey { - tokenBillingPDA, _, _ := solana.FindProgramAddress([][]byte{[]byte("fee_billing_token_config"), mint.Bytes()}, config.CcipRouterProgram) - return tokenBillingPDA + tokenConfigPda, _, _ := solana.FindProgramAddress([][]byte{[]byte("fee_billing_token_config"), mint.Bytes()}, config.CcipRouterProgram) + return tokenConfigPda + } + + getPerChainPerTokenConfigBillingPDA := func(mint solana.PublicKey) solana.PublicKey { + tokenBillingPda, _, _ := solana.FindProgramAddress([][]byte{[]byte("ccip_tokenpool_billing"), binary.LittleEndian.AppendUint64([]byte{}, config.EvmChainSelector), mint.Bytes()}, config.CcipRouterProgram) + return tokenBillingPda } validSourceChainConfig := ccip_router.SourceChainConfig{ @@ -118,9 +124,12 @@ func TestCCIPRouter(t *testing.T) { DefaultTxGasLimit: 1, MaxPerMsgGasLimit: 100, MaxDataBytes: 32, - MaxNumberOfTokensPerMsg: 1, + MaxNumberOfTokensPerMsg: 5, // bytes4(keccak256("CCIP ChainFamilySelector EVM")) ChainFamilySelector: [4]uint8{40, 18, 213, 44}, + + DefaultTokenFeeUsdcents: 100, + NetworkFeeUsdcents: 100, } // Small enough to fit in u160, big enough to not fall in the precompile space. validReceiverAddress := [32]byte{} @@ -225,6 +234,8 @@ func TestCCIPRouter(t *testing.T) { require.NoError(t, aerr) wsolReceiver, _, rerr := tokens.FindAssociatedTokenAddress(solana.TokenProgramID, solana.SolMint, config.BillingSignerPDA) require.NoError(t, rerr) + wsolPerChainPerTokenConfigPDA, _, perr := solana.FindProgramAddress([][]byte{[]byte("ccip_tokenpool_billing"), binary.LittleEndian.AppendUint64([]byte{}, config.EvmChainSelector), solana.SolMint.Bytes()}, ccip_router.ProgramID) + require.NoError(t, perr) wsolUserATA, _, uerr := tokens.FindAssociatedTokenAddress(solana.TokenProgramID, solana.SolMint, user.PublicKey()) require.NoError(t, uerr) wsolAnotherUserATA, _, auerr := tokens.FindAssociatedTokenAddress(solana.TokenProgramID, solana.SolMint, anotherUser.PublicKey()) @@ -240,6 +251,7 @@ func TestCCIPRouter(t *testing.T) { wsol.anotherUserATA = wsolAnotherUserATA wsol.tokenlessUserATA = wsolTokenlessUserATA wsol.billingATA = wsolReceiver + wsol.perChainPerTokenConfigPDA = wsolPerChainPerTokenConfigPDA /////////////// // Token2022 // @@ -256,6 +268,8 @@ func TestCCIPRouter(t *testing.T) { token2022PDA, _, aerr := solana.FindProgramAddress([][]byte{config.BillingTokenConfigPrefix, mintPubK.Bytes()}, ccip_router.ProgramID) require.NoError(t, aerr) + token2022PerChainPerTokenConfigPDA, _, puerr := solana.FindProgramAddress([][]byte{[]byte("ccip_tokenpool_billing"), binary.LittleEndian.AppendUint64([]byte{}, config.EvmChainSelector), mintPubK.Bytes()}, ccip_router.ProgramID) + require.NoError(t, puerr) token2022Receiver, _, rerr := tokens.FindAssociatedTokenAddress(config.Token2022Program, mintPubK, config.BillingSignerPDA) require.NoError(t, rerr) token2022UserATA, _, uerr := tokens.FindAssociatedTokenAddress(config.Token2022Program, mintPubK, user.PublicKey()) @@ -273,6 +287,7 @@ func TestCCIPRouter(t *testing.T) { token2022.anotherUserATA = token2022AnotherUserATA token2022.tokenlessUserATA = token2022TokenlessUserATA token2022.billingATA = token2022Receiver + token2022.perChainPerTokenConfigPDA = token2022PerChainPerTokenConfigPDA }) t.Run("Commit price updates address lookup table", func(t *testing.T) { @@ -815,8 +830,8 @@ func TestCCIPRouter(t *testing.T) { // Any nonzero timestamp is valid (for now) validTimestamp := int64(100) - validPriceValue := [28]uint8{} - validPriceValue[27] = 3 + value := [28]uint8{} + big.NewInt(3e18).FillBytes(value[:]) testTokens := []TestToken{ { @@ -825,10 +840,10 @@ func TestCCIPRouter(t *testing.T) { Enabled: true, Mint: solana.SolMint, UsdPerToken: ccip_router.TimestampedPackedU224{ - Value: validPriceValue, + Value: value, Timestamp: validTimestamp, }, - PremiumMultiplierWeiPerEth: 0, + PremiumMultiplierWeiPerEth: 1, }}, { Accounts: token2022, @@ -836,10 +851,10 @@ func TestCCIPRouter(t *testing.T) { Enabled: true, Mint: token2022.mint, UsdPerToken: ccip_router.TimestampedPackedU224{ - Value: validPriceValue, + Value: value, Timestamp: validTimestamp, }, - PremiumMultiplierWeiPerEth: 0, + PremiumMultiplierWeiPerEth: 1, }}, } @@ -932,11 +947,19 @@ func TestCCIPRouter(t *testing.T) { }) t.Run("When admin adds token0 with valid input it is configured", func(t *testing.T) { + // Any nonzero timestamp is valid (for now) + validTimestamp := int64(100) + value := [28]uint8{} + big.NewInt(3e18).FillBytes(value[:]) + token0Config := ccip_router.BillingTokenConfig{ - Enabled: true, - Mint: token0.Mint.PublicKey(), - UsdPerToken: ccip_router.TimestampedPackedU224{}, - PremiumMultiplierWeiPerEth: 0, + Enabled: true, + Mint: token0.Mint.PublicKey(), + UsdPerToken: ccip_router.TimestampedPackedU224{ + Timestamp: validTimestamp, + Value: value, + }, + PremiumMultiplierWeiPerEth: 1, } token0BillingPDA := getTokenConfigPDA(token0.Mint.PublicKey()) @@ -1007,98 +1030,7 @@ func TestCCIPRouter(t *testing.T) { require.Equal(t, token0Config.PremiumMultiplierWeiPerEth, final.Config.PremiumMultiplierWeiPerEth) }) - t.Run("Can remove token config", func(t *testing.T) { - token0BillingPDA := getTokenConfigPDA(token0.Mint.PublicKey()) - - var initial ccip_router.BillingTokenConfigWrapper - ierr := common.GetAccountDataBorshInto(ctx, solanaGoClient, token0BillingPDA, config.DefaultCommitment, &initial) - require.NoError(t, ierr) // it exists, initially - - receiver, _, aerr := tokens.FindAssociatedTokenAddress(token0.Program, token0.Mint.PublicKey(), config.BillingSignerPDA) - require.NoError(t, aerr) - - ixConfig, cerr := ccip_router.NewRemoveBillingTokenConfigInstruction( - config.RouterConfigPDA, - token0BillingPDA, - token0.Program, - token0.Mint.PublicKey(), - receiver, - config.BillingSignerPDA, - anotherAdmin.PublicKey(), - solana.SystemProgramID, - ).ValidateAndBuild() - require.NoError(t, cerr) - testutils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ixConfig}, anotherAdmin, config.DefaultCommitment) - - var final ccip_router.BillingTokenConfigWrapper - ferr := common.GetAccountDataBorshInto(ctx, solanaGoClient, token0BillingPDA, rpc.CommitmentProcessed, &final) - require.EqualError(t, ferr, "not found") // it no longer exists - }) - - t.Run("Can remove a pre-2022 token too", func(t *testing.T) { - mintPriv, kerr := solana.NewRandomPrivateKey() - require.NoError(t, kerr) - mint := mintPriv.PublicKey() - - // use old (pre-2022) token program - ixToken, terr := tokens.CreateToken(ctx, solana.TokenProgramID, mint, admin.PublicKey(), 9, solanaGoClient, config.DefaultCommitment) - require.NoError(t, terr) - testutils.SendAndConfirm(ctx, t, solanaGoClient, ixToken, admin, config.DefaultCommitment, common.AddSigners(mintPriv)) - - configPDA, _, perr := solana.FindProgramAddress([][]byte{config.BillingTokenConfigPrefix, mint.Bytes()}, ccip_router.ProgramID) - require.NoError(t, perr) - receiver, _, terr := tokens.FindAssociatedTokenAddress(solana.TokenProgramID, mint, config.BillingSignerPDA) - require.NoError(t, terr) - - tokenConfig := ccip_router.BillingTokenConfig{ - Enabled: true, - Mint: mint, - UsdPerToken: ccip_router.TimestampedPackedU224{}, - PremiumMultiplierWeiPerEth: 0, - } - - // add it first - ixConfig, cerr := ccip_router.NewAddBillingTokenConfigInstruction( - tokenConfig, - config.RouterConfigPDA, - configPDA, - solana.TokenProgramID, - mint, - receiver, - anotherAdmin.PublicKey(), - config.BillingSignerPDA, - tokens.AssociatedTokenProgramID, - solana.SystemProgramID, - ).ValidateAndBuild() - require.NoError(t, cerr) - - testutils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ixConfig}, anotherAdmin, config.DefaultCommitment) - - var tokenConfigAccount ccip_router.BillingTokenConfigWrapper - aerr := common.GetAccountDataBorshInto(ctx, solanaGoClient, configPDA, config.DefaultCommitment, &tokenConfigAccount) - require.NoError(t, aerr) - - require.Equal(t, tokenConfig, tokenConfigAccount.Config) - - // now, remove the added pre-2022 token, which has a balance of 0 in the receiver - ixConfig, cerr = ccip_router.NewRemoveBillingTokenConfigInstruction( - config.RouterConfigPDA, - configPDA, - solana.TokenProgramID, - mint, - receiver, - config.BillingSignerPDA, - anotherAdmin.PublicKey(), - solana.SystemProgramID, - ).ValidateAndBuild() - require.NoError(t, cerr) - - testutils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ixConfig}, anotherAdmin, config.DefaultCommitment) - var final ccip_router.BillingTokenConfigWrapper - ferr := common.GetAccountDataBorshInto(ctx, solanaGoClient, configPDA, rpc.CommitmentProcessed, &final) - require.EqualError(t, ferr, "not found") // it no longer exists - }) }) }) @@ -1902,17 +1834,81 @@ func TestCCIPRouter(t *testing.T) { FeeToken: wsol.mint, } - billingTokenConfigPDA := getTokenConfigPDA(wsol.mint) + raw := ccip_router.NewGetFeeInstruction(config.EvmChainSelector, message, config.EvmDestChainStatePDA) + raw.AccountMetaSlice.Append(solana.Meta(wsol.billingConfigPDA)) + raw.AccountMetaSlice.Append(solana.Meta(wsol.perChainPerTokenConfigPDA)) + instruction, err := raw.ValidateAndBuild() + require.NoError(t, err) - instruction, err := ccip_router.NewGetFeeInstruction(config.EvmChainSelector, message, config.EvmDestChainStatePDA, billingTokenConfigPDA).ValidateAndBuild() + feeResult := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{instruction}, user, config.DefaultCommitment) + require.NotNil(t, feeResult) + t.Log(feeResult.Meta.LogMessages) + fee := utils.ExtractTypedReturnValue(ctx, t, feeResult.Meta.LogMessages, config.CcipRouterProgram.String(), binary.LittleEndian.Uint64) + require.Equal(t, uint64(1), fee) + }) + + t.Run("Fee is retrieved for a correctly formatted message containing a nonnative token", func(t *testing.T) { + message := ccip_router.Solana2AnyMessage{ + Receiver: validReceiverAddress[:], + FeeToken: wsol.mint, + TokenAmounts: []ccip_router.SolanaTokenAmount{{Token: token0.Mint.PublicKey(), Amount: 1}}, + } + + // Set some fees that will result in some appreciable change in the message fee + billing := ccip_router.TokenBilling{ + MinFeeUsdcents: 800, + MaxFeeUsdcents: 1600, + DeciBps: 0, + DestGasOverhead: 100, + DestBytesOverhead: 100, + IsEnabled: true, + } + token0BillingConfigPda := getTokenConfigPDA(token0.Mint.PublicKey()) + token0PerChainPerConfigPda := getPerChainPerTokenConfigBillingPDA(token0.Mint.PublicKey()) + ix, err := ccip_router.NewSetTokenBillingInstruction(config.EvmChainSelector, token0.Mint.PublicKey(), billing, config.RouterConfigPDA, token0PerChainPerConfigPda, anotherAdmin.PublicKey(), solana.SystemProgramID).ValidateAndBuild() require.NoError(t, err) + utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, anotherAdmin, config.DefaultCommitment) - result := testutils.SimulateTransaction(ctx, t, solanaGoClient, []solana.Instruction{instruction}, user) - require.NotNil(t, result) + raw := ccip_router.NewGetFeeInstruction(config.EvmChainSelector, message, config.EvmDestChainStatePDA) + raw.AccountMetaSlice.Append(solana.Meta(wsol.billingConfigPDA)) + raw.AccountMetaSlice.Append(solana.Meta(token0BillingConfigPda)) - returned, err := common.ExtractTypedReturnValue(ctx, result.Value.Logs, config.CcipRouterProgram.String(), binary.LittleEndian.Uint64) + raw.AccountMetaSlice.Append(solana.Meta(wsol.perChainPerTokenConfigPDA)) + raw.AccountMetaSlice.Append(solana.Meta(token0PerChainPerConfigPda)) + instruction, err := raw.ValidateAndBuild() require.NoError(t, err) - require.Equal(t, uint64(1), returned) + + // // TESTBLOCK accounts exist + // { + // deserializedTokenConfigPda := ccip_router.BillingTokenConfigWrapper{} + // deserializedBillingPda := ccip_router.PerChainPerTokenConfig{} + // deserializedEvmDestChainStatePda := ccip_router.DestChainState{} + + // err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, wsol.billingConfigPDA, config.DefaultCommitment, &deserializedTokenConfigPda) + // require.NoError(t, err) + // t.Log(deserializedTokenConfigPda) + // err := utils.GetAccountDataBorshInto(ctx, solanaGoClient, token0BillingConfigPda, config.DefaultCommitment, &deserializedTokenConfigPda) + // require.NoError(t, err) + // t.Log(deserializedTokenConfigPda) + + // err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, wsol.perChainPerTokenConfigPDA, config.DefaultCommitment, &deserializedBillingPda) + // require.NoError(t, err) + // t.Log(deserializedBillingPda) + // err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, token0PerChainPerConfigPda, config.DefaultCommitment, &deserializedBillingPda) + // require.NoError(t, err) + // t.Log(deserializedBillingPda) + + // err = utils.GetAccountDataBorshInto(ctx, solanaGoClient, config.EvmDestChainStatePDA, config.DefaultCommitment, &deserializedEvmDestChainStatePda) + // require.NoError(t, err) + // t.Log(deserializedEvmDestChainStatePda) + + // } + + feeResult := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{instruction}, user, config.DefaultCommitment) + require.NotNil(t, feeResult) + t.Log(feeResult.Meta.LogMessages) + fee := utils.ExtractTypedReturnValue(ctx, t, feeResult.Meta.LogMessages, config.CcipRouterProgram.String(), binary.LittleEndian.Uint64) + require.Equal(t, uint64(3), fee) }) t.Run("Cannot get fee for message with invalid address", func(t *testing.T) { @@ -1927,11 +1923,15 @@ func TestCCIPRouter(t *testing.T) { for _, address := range [][32]byte{tooBigAddress, tooSmallAddress} { message := ccip_router.Solana2AnyMessage{ Receiver: address[:], - FeeToken: wsol.mint, + FeeToken: token0.Mint.PublicKey(), } - billingTokenConfigPDA := getTokenConfigPDA(wsol.mint) - instruction, err := ccip_router.NewGetFeeInstruction(config.EvmChainSelector, message, config.EvmDestChainStatePDA, billingTokenConfigPDA).ValidateAndBuild() + tokenConfigPda := getTokenConfigPDA(token0.Mint.PublicKey()) + tokenBillingPda := getPerChainPerTokenConfigBillingPDA(token0.Mint.PublicKey()) + raw := ccip_router.NewGetFeeInstruction(config.EvmChainSelector, message, config.EvmDestChainStatePDA) + raw.AccountMetaSlice.Append(solana.Meta(tokenConfigPda)) + raw.AccountMetaSlice.Append(solana.Meta(tokenBillingPda)) + instruction, err := raw.ValidateAndBuild() require.NoError(t, err) result := testutils.SendAndFailWith(ctx, t, solanaGoClient, []solana.Instruction{instruction}, user, config.DefaultCommitment, []string{"Error Code: InvalidEVMAddress"}) @@ -1953,6 +1953,7 @@ func TestCCIPRouter(t *testing.T) { FeeToken: wsol.mint, Receiver: validReceiverAddress[:], } + raw := ccip_router.NewCcipSendInstruction( destinationChainSelector, message, @@ -1970,7 +1971,10 @@ func TestCCIPRouter(t *testing.T) { config.ExternalTokenPoolsSignerPDA, ) raw.GetFeeTokenUserAssociatedAccountAccount().WRITE() + raw.AccountMetaSlice.Append(solana.Meta(wsol.billingConfigPDA)) + raw.AccountMetaSlice.Append(solana.Meta(wsol.perChainPerTokenConfigPDA)) instruction, err := raw.ValidateAndBuild() + require.NoError(t, err) result := testutils.SendAndFailWith(ctx, t, solanaGoClient, []solana.Instruction{instruction}, user, config.DefaultCommitment, []string{"Error Code: AccountNotInitialized"}) require.NotNil(t, result) @@ -2002,6 +2006,8 @@ func TestCCIPRouter(t *testing.T) { config.ExternalTokenPoolsSignerPDA, ) raw.GetFeeTokenUserAssociatedAccountAccount().WRITE() + raw.AccountMetaSlice.Append(solana.Meta(wsol.billingConfigPDA)) + raw.AccountMetaSlice.Append(solana.Meta(wsol.perChainPerTokenConfigPDA)) instruction, err := raw.ValidateAndBuild() require.NoError(t, err) result := testutils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{instruction}, user, config.DefaultCommitment) @@ -2065,6 +2071,8 @@ func TestCCIPRouter(t *testing.T) { config.ExternalTokenPoolsSignerPDA, ) raw.GetFeeTokenUserAssociatedAccountAccount().WRITE() + raw.AccountMetaSlice.Append(solana.Meta(wsol.billingConfigPDA)) + raw.AccountMetaSlice.Append(solana.Meta(wsol.perChainPerTokenConfigPDA)) instruction, err := raw.ValidateAndBuild() require.NoError(t, err) result := testutils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{instruction}, user, config.DefaultCommitment) @@ -2126,6 +2134,8 @@ func TestCCIPRouter(t *testing.T) { config.ExternalTokenPoolsSignerPDA, ) raw.GetFeeTokenUserAssociatedAccountAccount().WRITE() + raw.AccountMetaSlice.Append(solana.Meta(wsol.billingConfigPDA)) + raw.AccountMetaSlice.Append(solana.Meta(wsol.perChainPerTokenConfigPDA)) instruction, err := raw.ValidateAndBuild() require.NoError(t, err) result := testutils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{instruction}, user, config.DefaultCommitment) @@ -2188,6 +2198,8 @@ func TestCCIPRouter(t *testing.T) { config.ExternalTokenPoolsSignerPDA, ) raw.GetFeeTokenUserAssociatedAccountAccount().WRITE() + raw.AccountMetaSlice.Append(solana.Meta(wsol.billingConfigPDA)) + raw.AccountMetaSlice.Append(solana.Meta(wsol.perChainPerTokenConfigPDA)) instruction, err := raw.ValidateAndBuild() require.NoError(t, err) result := testutils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{instruction}, user, config.DefaultCommitment) @@ -2249,6 +2261,8 @@ func TestCCIPRouter(t *testing.T) { config.ExternalTokenPoolsSignerPDA, ) raw.GetFeeTokenUserAssociatedAccountAccount().WRITE() + raw.AccountMetaSlice.Append(solana.Meta(token2022.billingConfigPDA)) + raw.AccountMetaSlice.Append(solana.Meta(token2022.perChainPerTokenConfigPDA)) instruction, err := raw.ValidateAndBuild() require.NoError(t, err) result := testutils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{instruction}, user, config.DefaultCommitment) @@ -2307,6 +2321,8 @@ func TestCCIPRouter(t *testing.T) { config.ExternalTokenPoolsSignerPDA, ) raw.GetFeeTokenUserAssociatedAccountAccount().WRITE() + raw.AccountMetaSlice.Append(solana.Meta(wsol.billingConfigPDA)) + raw.AccountMetaSlice.Append(solana.Meta(wsol.perChainPerTokenConfigPDA)) instruction, err := raw.ValidateAndBuild() require.NoError(t, err) @@ -2340,6 +2356,8 @@ func TestCCIPRouter(t *testing.T) { config.ExternalTokenPoolsSignerPDA, ) raw.GetFeeTokenUserAssociatedAccountAccount().WRITE() + raw.AccountMetaSlice.Append(solana.Meta(wsol.billingConfigPDA)) + raw.AccountMetaSlice.Append(solana.Meta(wsol.perChainPerTokenConfigPDA)) instruction, err := raw.ValidateAndBuild() require.NoError(t, err) @@ -2373,7 +2391,8 @@ func TestCCIPRouter(t *testing.T) { ) // do NOT mark the user ATA as writable - + raw.AccountMetaSlice.Append(solana.Meta(wsol.billingConfigPDA)) + raw.AccountMetaSlice.Append(solana.Meta(wsol.perChainPerTokenConfigPDA)) instruction, err := raw.ValidateAndBuild() require.NoError(t, err) @@ -2420,6 +2439,8 @@ func TestCCIPRouter(t *testing.T) { config.ExternalTokenPoolsSignerPDA, ) raw.GetFeeTokenUserAssociatedAccountAccount().WRITE() + raw.AccountMetaSlice.Append(solana.Meta(wsol.billingConfigPDA)) + raw.AccountMetaSlice.Append(solana.Meta(wsol.perChainPerTokenConfigPDA)) instruction, err := raw.ValidateAndBuild() require.NoError(t, err) @@ -2462,6 +2483,8 @@ func TestCCIPRouter(t *testing.T) { config.ExternalTokenPoolsSignerPDA, ) raw.GetFeeTokenUserAssociatedAccountAccount().WRITE() + raw.AccountMetaSlice.Append(solana.Meta(token2022.billingConfigPDA)) + raw.AccountMetaSlice.Append(solana.Meta(token2022.perChainPerTokenConfigPDA)) instruction, err := raw.ValidateAndBuild() require.NoError(t, err) testutils.SendAndFailWith(ctx, t, solanaGoClient, []solana.Instruction{instruction}, anotherUser, config.DefaultCommitment, []string{ccip_router.InvalidInputs_CcipRouterError.String()}) @@ -2495,6 +2518,8 @@ func TestCCIPRouter(t *testing.T) { config.ExternalTokenPoolsSignerPDA, ) raw.GetFeeTokenUserAssociatedAccountAccount().WRITE() + raw.AccountMetaSlice.Append(solana.Meta(token2022.billingConfigPDA)) + raw.AccountMetaSlice.Append(solana.Meta(token2022.perChainPerTokenConfigPDA)) instruction, err := raw.ValidateAndBuild() require.NoError(t, err) result := testutils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{instruction}, anotherUser, config.DefaultCommitment) @@ -2748,13 +2773,17 @@ func TestCCIPRouter(t *testing.T) { Data: []byte{4, 5, 6}, } // getFee - ix, ferr := ccip_router.NewGetFeeInstruction(config.EvmChainSelector, message, config.EvmDestChainStatePDA, token.billingConfigPDA).ValidateAndBuild() - require.NoError(t, ferr) + tokenConfigPda := getTokenConfigPDA(token.mint) + perChainPerTokenConfigBillingPda := getPerChainPerTokenConfigBillingPDA(token.mint) + rawGetFeeIx := ccip_router.NewGetFeeInstruction(config.EvmChainSelector, message, config.EvmDestChainStatePDA) + rawGetFeeIx.AccountMetaSlice.Append(solana.Meta(tokenConfigPda)) + rawGetFeeIx.AccountMetaSlice.Append(solana.Meta(perChainPerTokenConfigBillingPda)) + ix, err := rawGetFeeIx.ValidateAndBuild() + require.NoError(t, err) - feeResult := testutils.SimulateTransaction(ctx, t, solanaGoClient, []solana.Instruction{ix}, user) + feeResult := utils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ix}, user, config.DefaultCommitment) require.NotNil(t, feeResult) - fee, err := common.ExtractTypedReturnValue(ctx, feeResult.Value.Logs, config.CcipRouterProgram.String(), binary.LittleEndian.Uint64) - require.NoError(t, err) + fee := utils.ExtractTypedReturnValue(ctx, t, feeResult.Meta.LogMessages, config.CcipRouterProgram.String(), binary.LittleEndian.Uint64) require.Equal(t, uint64(1), fee) initialBalance := getBalance(token.billingATA) @@ -2839,8 +2868,13 @@ func TestCCIPRouter(t *testing.T) { } // getFee - ix, ferr := ccip_router.NewGetFeeInstruction(config.EvmChainSelector, message, config.EvmDestChainStatePDA, wsol.billingConfigPDA).ValidateAndBuild() - require.NoError(t, ferr) + tokenConfigPda := getTokenConfigPDA(zeroPubkey) + perChainPerTokenConfigBillingPda := getPerChainPerTokenConfigBillingPDA(zeroPubkey) + rawGetFeeIx := ccip_router.NewGetFeeInstruction(config.EvmChainSelector, message, config.EvmDestChainStatePDA) + rawGetFeeIx.AccountMetaSlice.Append(solana.Meta(tokenConfigPda)) + rawGetFeeIx.AccountMetaSlice.Append(solana.Meta(perChainPerTokenConfigBillingPda)) + ix, err := rawGetFeeIx.ValidateAndBuild() + require.NoError(t, err) feeResult := testutils.SimulateTransaction(ctx, t, solanaGoClient, []solana.Instruction{ix}, user) require.NotNil(t, feeResult) @@ -5111,4 +5145,103 @@ func TestCCIPRouter(t *testing.T) { }) }) }) + + ////////////////////////// + // Cleanup tests // + ////////////////////////// + + t.Run("Cleanup", func(t<<<<<<< HEAD + t.Run("Can remove token config", func(t *testing.T) { + token0BillingPDA := getTokenConfigPDA(token0.Mint.PublicKey()) + + var initial ccip_router.BillingTokenConfigWrapper + ierr := common.GetAccountDataBorshInto(ctx, solanaGoClient, token0BillingPDA, config.DefaultCommitment, &initial) + require.NoError(t, ierr) // it exists, initially + + receiver, _, aerr := tokens.FindAssociatedTokenAddress(token0.Program, token0.Mint.PublicKey(), config.BillingSignerPDA) + require.NoError(t, aerr) + + ixConfig, cerr := ccip_router.NewRemoveBillingTokenConfigInstruction( + config.RouterConfigPDA, + token0BillingPDA, + token0.Program, + token0.Mint.PublicKey(), + receiver, + config.BillingSignerPDA, + anotherAdmin.PublicKey(), + solana.SystemProgramID, + ).ValidateAndBuild() + require.NoError(t, cerr) + testutils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ixConfig}, anotherAdmin, config.DefaultCommitment) + + var final ccip_router.BillingTokenConfigWrapper + ferr := common.GetAccountDataBorshInto(ctx, solanaGoClient, token0BillingPDA, rpc.CommitmentProcessed, &final) + require.EqualError(t, ferr, "not found") // it no longer exists + }) + + t.Run("Can remove a pre-2022 token too", func(t *testing.T) { + mintPriv, kerr := solana.NewRandomPrivateKey() + require.NoError(t, kerr) + mint := mintPriv.PublicKey() + + // use old (pre-2022) token program + ixToken, terr := tokens.CreateToken(ctx, solana.TokenProgramID, mint, admin.PublicKey(), 9, solanaGoClient, config.DefaultCommitment) + require.NoError(t, terr) + testutils.SendAndConfirm(ctx, t, solanaGoClient, ixToken, admin, config.DefaultCommitment, common.AddSigners(mintPriv)) + + configPDA, _, perr := solana.FindProgramAddress([][]byte{config.BillingTokenConfigPrefix, mint.Bytes()}, ccip_router.ProgramID) + require.NoError(t, perr) + receiver, _, terr := tokens.FindAssociatedTokenAddress(solana.TokenProgramID, mint, config.BillingSignerPDA) + require.NoError(t, terr) + + tokenConfig := ccip_router.BillingTokenConfig{ + Enabled: true, + Mint: mint, + UsdPerToken: ccip_router.TimestampedPackedU224{}, + PremiumMultiplierWeiPerEth: 0, + } + + // add it first + ixConfig, cerr := ccip_router.NewAddBillingTokenConfigInstruction( + tokenConfig, + config.RouterConfigPDA, + configPDA, + solana.TokenProgramID, + mint, + receiver, + anotherAdmin.PublicKey(), + config.BillingSignerPDA, + tokens.AssociatedTokenProgramID, + solana.SystemProgramID, + ).ValidateAndBuild() + require.NoError(t, cerr) + + testutils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ixConfig}, anotherAdmin, config.DefaultCommitment) + + var tokenConfigAccount ccip_router.BillingTokenConfigWrapper + aerr := common.GetAccountDataBorshInto(ctx, solanaGoClient, configPDA, config.DefaultCommitment, &tokenConfigAccount) + require.NoError(t, aerr) + + require.Equal(t, tokenConfig, tokenConfigAccount.Config) + + // now, remove the added pre-2022 token, which has a balance of 0 in the receiver + ixConfig, cerr = ccip_router.NewRemoveBillingTokenConfigInstruction( + config.RouterConfigPDA, + configPDA, + solana.TokenProgramID, + mint, + receiver, + config.BillingSignerPDA, + anotherAdmin.PublicKey(), + solana.SystemProgramID, + ).ValidateAndBuild() + require.NoError(t, cerr) + + testutils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ixConfig}, anotherAdmin, config.DefaultCommitment) + + var final ccip_router.BillingTokenConfigWrapper + ferr := common.GetAccountDataBorshInto(ctx, solanaGoClient, configPDA, rpc.CommitmentProcessed, &final) + require.EqualError(t, ferr, "not found") // it no longer exists + }) + }) } diff --git a/chains/solana/gobindings/ccip_router/CcipSend.go b/chains/solana/gobindings/ccip_router/CcipSend.go index c8aca920..8426faef 100644 --- a/chains/solana/gobindings/ccip_router/CcipSend.go +++ b/chains/solana/gobindings/ccip_router/CcipSend.go @@ -19,6 +19,16 @@ import ( // The message will be sent to the receiver on the destination chain selector. // This message emits the event CCIPSendRequested with all the necessary data to be retrieved by the OffChain Code // +// # Additional accounts +// +// In addition to the fixed amount of accounts defined in the `GetFee` context, +// the following accounts must be provided: +// +// * First, the billing token config accounts for each token involved, including the +// fee token, sequentially. +// * Then, the per chain / per token config of those tokens, sequentially in the same +// order, for the destination chain. +// // # Arguments // // * `ctx` - The context containing the accounts required for sending the message. diff --git a/chains/solana/gobindings/ccip_router/GetFee.go b/chains/solana/gobindings/ccip_router/GetFee.go index 36d763d5..db641574 100644 --- a/chains/solana/gobindings/ccip_router/GetFee.go +++ b/chains/solana/gobindings/ccip_router/GetFee.go @@ -18,6 +18,16 @@ import ( // * `dest_chain_selector` - The chain selector for the destination chain. // * `message` - The message to be sent. // +// # Additional accounts +// +// In addition to the fixed amount of accounts defined in the `GetFee` context, +// the following accounts must be provided: +// +// * First, the billing token config accounts for each token involved, including the +// fee token, sequentially. +// * Then, the per chain / per token config of those tokens, sequentially in the same +// order, for the destination chain. +// // # Returns // // The fee amount in u64. diff --git a/chains/solana/gobindings/ccip_router/instructions.go b/chains/solana/gobindings/ccip_router/instructions.go index 4f792d6f..d72a9df7 100644 --- a/chains/solana/gobindings/ccip_router/instructions.go +++ b/chains/solana/gobindings/ccip_router/instructions.go @@ -271,6 +271,16 @@ var ( // * `dest_chain_selector` - The chain selector for the destination chain. // * `message` - The message to be sent. // + // # Additional accounts + // + // In addition to the fixed amount of accounts defined in the `GetFee` context, + // the following accounts must be provided: + // + // * First, the billing token config accounts for each token involved, including the + // fee token, sequentially. + // * Then, the per chain / per token config of those tokens, sequentially in the same + // order, for the destination chain. + // // # Returns // // The fee amount in u64. @@ -285,6 +295,16 @@ var ( // The message will be sent to the receiver on the destination chain selector. // This message emits the event CCIPSendRequested with all the necessary data to be retrieved by the OffChain Code // + // # Additional accounts + // + // In addition to the fixed amount of accounts defined in the `GetFee` context, + // the following accounts must be provided: + // + // * First, the billing token config accounts for each token involved, including the + // fee token, sequentially. + // * Then, the per chain / per token config of those tokens, sequentially in the same + // order, for the destination chain. + // // # Arguments // // * `ctx` - The context containing the accounts required for sending the message.