diff --git a/bindings/bind/compile.go b/bindings/bind/compile.go index 3ca856c0e..43f9e8615 100644 --- a/bindings/bind/compile.go +++ b/bindings/bind/compile.go @@ -299,6 +299,7 @@ func compilePackageInternal(packageName contracts.Package, namedAddresses map[st filepath.Join(dstRoot, "ccip", "ccip_offramp"), filepath.Join(dstRoot, "ccip", "ccip_burn_mint_token"), filepath.Join(dstRoot, "ccip", "ccip_dummy_receiver"), + filepath.Join(dstRoot, "ccip", "ccip_broken_receiver"), filepath.Join(dstRoot, "ccip", "managed_token"), filepath.Join(dstRoot, "ccip", "managed_token_faucet"), filepath.Join(dstRoot, "ccip", "mock_eth_token"), @@ -505,6 +506,32 @@ func compilePackageInternal(packageName contracts.Package, namedAddresses map[st } } + if packageName == contracts.CCIPBrokenReceiver { + mcmsAddr := namedAddresses["mcms"] + if !isZeroAddress(mcmsAddr) { + mcmsDir := filepath.Join(dstRoot, "mcms", "mcms") + if err := managePackage(mcmsDir, 1, rpcURL, env, mcmsAddr, mcmsAddr, pubfilePath); err != nil { + return PackageArtifact{}, fmt.Errorf("failed to manage MCMS dependency: %w", err) + } + } else { + fmt.Println("Skipping manage-package for MCMS (no published address found)") + } + + ccipAddr := namedAddresses["ccip"] + if !isZeroAddress(ccipAddr) { + ccipDir := filepath.Join(dstRoot, "ccip", "ccip") + ccipOrigID := ccipAddr + if orig := namedAddresses["original_ccip_pkg"]; !isZeroAddress(orig) { + ccipOrigID = orig + } + if err := managePackage(ccipDir, 1, rpcURL, env, ccipOrigID, ccipAddr, pubfilePath); err != nil { + return PackageArtifact{}, fmt.Errorf("failed to manage CCIP dependency: %w", err) + } + } else { + fmt.Println("Skipping manage-package for CCIP (no published address found)") + } + } + if packageName == contracts.CCIPRouter { mcmsAddr := namedAddresses["mcms"] if !isZeroAddress(mcmsAddr) { diff --git a/bindings/generated/ccip/ccip/offramp_state_helper/offramp_state_helper.go b/bindings/generated/ccip/ccip/offramp_state_helper/offramp_state_helper.go index 3c3438092..0ff6ac893 100644 --- a/bindings/generated/ccip/ccip/offramp_state_helper/offramp_state_helper.go +++ b/bindings/generated/ccip/ccip/offramp_state_helper/offramp_state_helper.go @@ -19,7 +19,7 @@ var ( _ = big.NewInt ) -const FunctionInfo = `[{"package":"ccip","module":"offramp_state_helper","name":"add_dest_token_transfer","parameters":[{"name":"_","type":"DestTransferCap"},{"name":"receiver_params","type":"ReceiverParams"},{"name":"token_receiver","type":"address"},{"name":"remote_chain_selector","type":"u64"},{"name":"source_amount","type":"u256"},{"name":"dest_token_address","type":"address"},{"name":"dest_token_pool_package_id","type":"address"},{"name":"source_pool_address","type":"vector"},{"name":"source_pool_data","type":"vector"},{"name":"offchain_data","type":"vector"}]},{"package":"ccip","module":"offramp_state_helper","name":"complete_token_transfer","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"receiver_params","type":"ReceiverParams"},{"name":"_","type":"TypeProof"}]},{"package":"ccip","module":"offramp_state_helper","name":"consume_any2sui_message","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"message","type":"Any2SuiMessage"},{"name":"_","type":"TypeProof"}]},{"package":"ccip","module":"offramp_state_helper","name":"create_receiver_params","parameters":[{"name":"_","type":"DestTransferCap"},{"name":"source_chain_selector","type":"u64"}]},{"package":"ccip","module":"offramp_state_helper","name":"deconstruct_receiver_params","parameters":[{"name":"_","type":"DestTransferCap"},{"name":"receiver_params","type":"ReceiverParams"}]},{"package":"ccip","module":"offramp_state_helper","name":"extract_any2sui_message","parameters":[{"name":"receiver_params","type":"ReceiverParams"}]},{"package":"ccip","module":"offramp_state_helper","name":"get_dest_token_transfer_data","parameters":[{"name":"receiver_params","type":"ReceiverParams"}]},{"package":"ccip","module":"offramp_state_helper","name":"get_source_chain_selector","parameters":[{"name":"receiver_params","type":"ReceiverParams"}]},{"package":"ccip","module":"offramp_state_helper","name":"get_token_param_data","parameters":[{"name":"receiver_params","type":"ReceiverParams"}]},{"package":"ccip","module":"offramp_state_helper","name":"new_any2sui_message","parameters":[{"name":"_","type":"DestTransferCap"},{"name":"message_id","type":"vector"},{"name":"source_chain_selector","type":"u64"},{"name":"sender","type":"vector"},{"name":"data","type":"vector"},{"name":"message_receiver","type":"address"},{"name":"token_receiver","type":"address"},{"name":"token_addresses","type":"vector
"},{"name":"token_amounts","type":"vector"}]},{"package":"ccip","module":"offramp_state_helper","name":"new_dest_transfer_cap","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"owner_cap","type":"OwnerCap"}]},{"package":"ccip","module":"offramp_state_helper","name":"populate_message","parameters":[{"name":"_","type":"DestTransferCap"},{"name":"receiver_params","type":"ReceiverParams"},{"name":"any2sui_message","type":"Any2SuiMessage"}]}]` +const FunctionInfo = `[{"package":"ccip","module":"offramp_state_helper","name":"add_dest_token_transfer","parameters":[{"name":"_","type":"DestTransferCap"},{"name":"receiver_params","type":"ReceiverParams"},{"name":"token_receiver","type":"address"},{"name":"remote_chain_selector","type":"u64"},{"name":"source_amount","type":"u256"},{"name":"dest_token_address","type":"address"},{"name":"dest_token_pool_package_id","type":"address"},{"name":"source_pool_address","type":"vector"},{"name":"source_pool_data","type":"vector"},{"name":"offchain_data","type":"vector"}]},{"package":"ccip","module":"offramp_state_helper","name":"add_dest_token_transfer_v2","parameters":[{"name":"_","type":"DestTransferCap"},{"name":"receiver_params","type":"ReceiverParamsV2"},{"name":"token_receiver","type":"address"},{"name":"remote_chain_selector","type":"u64"},{"name":"source_amount","type":"u256"},{"name":"dest_token_address","type":"address"},{"name":"dest_token_pool_package_id","type":"address"},{"name":"source_pool_address","type":"vector"},{"name":"source_pool_data","type":"vector"},{"name":"offchain_data","type":"vector"}]},{"package":"ccip","module":"offramp_state_helper","name":"complete_token_transfer","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"receiver_params","type":"ReceiverParams"},{"name":"_","type":"TypeProof"}]},{"package":"ccip","module":"offramp_state_helper","name":"complete_token_transfer_v2","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"receiver_params","type":"ReceiverParamsV2"},{"name":"_","type":"TypeProof"}]},{"package":"ccip","module":"offramp_state_helper","name":"consume_any2sui_message","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"message","type":"Any2SuiMessage"},{"name":"_","type":"TypeProof"}]},{"package":"ccip","module":"offramp_state_helper","name":"consume_any2sui_message_v2","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"message","type":"client::Any2SuiMessageV2"},{"name":"_","type":"TypeProof"}]},{"package":"ccip","module":"offramp_state_helper","name":"create_receiver_params","parameters":[{"name":"_","type":"DestTransferCap"},{"name":"source_chain_selector","type":"u64"}]},{"package":"ccip","module":"offramp_state_helper","name":"create_receiver_params_v2","parameters":[{"name":"_","type":"DestTransferCap"},{"name":"source_chain_selector","type":"u64"}]},{"package":"ccip","module":"offramp_state_helper","name":"deconstruct_receiver_params","parameters":[{"name":"_","type":"DestTransferCap"},{"name":"receiver_params","type":"ReceiverParams"}]},{"package":"ccip","module":"offramp_state_helper","name":"deconstruct_receiver_params_v2","parameters":[{"name":"_","type":"DestTransferCap"},{"name":"receiver_params","type":"ReceiverParamsV2"}]},{"package":"ccip","module":"offramp_state_helper","name":"extract_any2sui_message","parameters":[{"name":"receiver_params","type":"ReceiverParams"}]},{"package":"ccip","module":"offramp_state_helper","name":"extract_any2sui_message_v2","parameters":[{"name":"receiver_params","type":"ReceiverParamsV2"},{"name":"used_object_ids","type":"vector
"}]},{"package":"ccip","module":"offramp_state_helper","name":"get_dest_token_transfer_data","parameters":[{"name":"receiver_params","type":"ReceiverParams"}]},{"package":"ccip","module":"offramp_state_helper","name":"get_dest_token_transfer_data_v2","parameters":[{"name":"receiver_params","type":"ReceiverParamsV2"}]},{"package":"ccip","module":"offramp_state_helper","name":"get_source_chain_selector","parameters":[{"name":"receiver_params","type":"ReceiverParams"}]},{"package":"ccip","module":"offramp_state_helper","name":"get_token_param_data","parameters":[{"name":"receiver_params","type":"ReceiverParams"}]},{"package":"ccip","module":"offramp_state_helper","name":"new_any2sui_message","parameters":[{"name":"_","type":"DestTransferCap"},{"name":"message_id","type":"vector"},{"name":"source_chain_selector","type":"u64"},{"name":"sender","type":"vector"},{"name":"data","type":"vector"},{"name":"message_receiver","type":"address"},{"name":"token_receiver","type":"address"},{"name":"token_addresses","type":"vector
"},{"name":"token_amounts","type":"vector"}]},{"package":"ccip","module":"offramp_state_helper","name":"new_any2sui_message_v2","parameters":[{"name":"_","type":"DestTransferCap"},{"name":"message_id","type":"vector"},{"name":"source_chain_selector","type":"u64"},{"name":"sender","type":"vector"},{"name":"data","type":"vector"},{"name":"message_receiver","type":"address"},{"name":"token_receiver","type":"address"},{"name":"receiver_object_ids","type":"vector
"},{"name":"token_addresses","type":"vector
"},{"name":"token_amounts","type":"vector"}]},{"package":"ccip","module":"offramp_state_helper","name":"new_dest_transfer_cap","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"owner_cap","type":"OwnerCap"}]},{"package":"ccip","module":"offramp_state_helper","name":"populate_message","parameters":[{"name":"_","type":"DestTransferCap"},{"name":"receiver_params","type":"ReceiverParams"},{"name":"any2sui_message","type":"Any2SuiMessage"}]},{"package":"ccip","module":"offramp_state_helper","name":"populate_message_v2","parameters":[{"name":"_","type":"DestTransferCap"},{"name":"receiver_params","type":"ReceiverParamsV2"},{"name":"any2sui_message","type":"client::Any2SuiMessageV2"}]}]` type IOfframpStateHelper interface { NewDestTransferCap(ctx context.Context, opts *bind.CallOpts, ref bind.Object, ownerCap bind.Object) (*models.SuiTransactionBlockResponse, error) @@ -34,6 +34,15 @@ type IOfframpStateHelper interface { NewAny2suiMessage(ctx context.Context, opts *bind.CallOpts, param bind.Object, messageId []byte, sourceChainSelector uint64, sender []byte, data []byte, messageReceiver string, tokenReceiver string, tokenAddresses []string, tokenAmounts []*big.Int) (*models.SuiTransactionBlockResponse, error) ConsumeAny2suiMessage(ctx context.Context, opts *bind.CallOpts, typeArgs []string, ref bind.Object, message bind.Object, param bind.Object) (*models.SuiTransactionBlockResponse, error) DeconstructReceiverParams(ctx context.Context, opts *bind.CallOpts, param bind.Object, receiverParams ReceiverParams) (*models.SuiTransactionBlockResponse, error) + NewAny2suiMessageV2(ctx context.Context, opts *bind.CallOpts, param bind.Object, messageId []byte, sourceChainSelector uint64, sender []byte, data []byte, messageReceiver string, tokenReceiver string, receiverObjectIds []string, tokenAddresses []string, tokenAmounts []*big.Int) (*models.SuiTransactionBlockResponse, error) + CreateReceiverParamsV2(ctx context.Context, opts *bind.CallOpts, param bind.Object, sourceChainSelector uint64) (*models.SuiTransactionBlockResponse, error) + AddDestTokenTransferV2(ctx context.Context, opts *bind.CallOpts, param bind.Object, receiverParams ReceiverParamsV2, tokenReceiver string, remoteChainSelector uint64, sourceAmount *big.Int, destTokenAddress string, destTokenPoolPackageId string, sourcePoolAddress []byte, sourcePoolData []byte, offchainData []byte) (*models.SuiTransactionBlockResponse, error) + PopulateMessageV2(ctx context.Context, opts *bind.CallOpts, param bind.Object, receiverParams ReceiverParamsV2, any2suiMessage bind.Object) (*models.SuiTransactionBlockResponse, error) + ExtractAny2suiMessageV2(ctx context.Context, opts *bind.CallOpts, receiverParams ReceiverParamsV2, usedObjectIds []string) (*models.SuiTransactionBlockResponse, error) + ConsumeAny2suiMessageV2(ctx context.Context, opts *bind.CallOpts, typeArgs []string, ref bind.Object, message bind.Object, param bind.Object) (*models.SuiTransactionBlockResponse, error) + GetDestTokenTransferDataV2(ctx context.Context, opts *bind.CallOpts, receiverParams ReceiverParamsV2) (*models.SuiTransactionBlockResponse, error) + CompleteTokenTransferV2(ctx context.Context, opts *bind.CallOpts, typeArgs []string, ref bind.Object, receiverParams ReceiverParamsV2, param bind.Object) (*models.SuiTransactionBlockResponse, error) + DeconstructReceiverParamsV2(ctx context.Context, opts *bind.CallOpts, param bind.Object, receiverParams ReceiverParamsV2) (*models.SuiTransactionBlockResponse, error) DevInspect() IOfframpStateHelperDevInspect Encoder() OfframpStateHelperEncoder Bound() bind.IBoundContract @@ -48,6 +57,11 @@ type IOfframpStateHelperDevInspect interface { ExtractAny2suiMessage(ctx context.Context, opts *bind.CallOpts, receiverParams ReceiverParams) (bind.Object, error) NewAny2suiMessage(ctx context.Context, opts *bind.CallOpts, param bind.Object, messageId []byte, sourceChainSelector uint64, sender []byte, data []byte, messageReceiver string, tokenReceiver string, tokenAddresses []string, tokenAmounts []*big.Int) (bind.Object, error) ConsumeAny2suiMessage(ctx context.Context, opts *bind.CallOpts, typeArgs []string, ref bind.Object, message bind.Object, param bind.Object) ([]any, error) + NewAny2suiMessageV2(ctx context.Context, opts *bind.CallOpts, param bind.Object, messageId []byte, sourceChainSelector uint64, sender []byte, data []byte, messageReceiver string, tokenReceiver string, receiverObjectIds []string, tokenAddresses []string, tokenAmounts []*big.Int) (bind.Object, error) + CreateReceiverParamsV2(ctx context.Context, opts *bind.CallOpts, param bind.Object, sourceChainSelector uint64) (ReceiverParamsV2, error) + ExtractAny2suiMessageV2(ctx context.Context, opts *bind.CallOpts, receiverParams ReceiverParamsV2, usedObjectIds []string) (bind.Object, error) + ConsumeAny2suiMessageV2(ctx context.Context, opts *bind.CallOpts, typeArgs []string, ref bind.Object, message bind.Object, param bind.Object) ([]any, error) + GetDestTokenTransferDataV2(ctx context.Context, opts *bind.CallOpts, receiverParams ReceiverParamsV2) ([]any, error) } type OfframpStateHelperEncoder interface { @@ -75,6 +89,24 @@ type OfframpStateHelperEncoder interface { ConsumeAny2suiMessageWithArgs(typeArgs []string, args ...any) (*bind.EncodedCall, error) DeconstructReceiverParams(param bind.Object, receiverParams ReceiverParams) (*bind.EncodedCall, error) DeconstructReceiverParamsWithArgs(args ...any) (*bind.EncodedCall, error) + NewAny2suiMessageV2(param bind.Object, messageId []byte, sourceChainSelector uint64, sender []byte, data []byte, messageReceiver string, tokenReceiver string, receiverObjectIds []string, tokenAddresses []string, tokenAmounts []*big.Int) (*bind.EncodedCall, error) + NewAny2suiMessageV2WithArgs(args ...any) (*bind.EncodedCall, error) + CreateReceiverParamsV2(param bind.Object, sourceChainSelector uint64) (*bind.EncodedCall, error) + CreateReceiverParamsV2WithArgs(args ...any) (*bind.EncodedCall, error) + AddDestTokenTransferV2(param bind.Object, receiverParams ReceiverParamsV2, tokenReceiver string, remoteChainSelector uint64, sourceAmount *big.Int, destTokenAddress string, destTokenPoolPackageId string, sourcePoolAddress []byte, sourcePoolData []byte, offchainData []byte) (*bind.EncodedCall, error) + AddDestTokenTransferV2WithArgs(args ...any) (*bind.EncodedCall, error) + PopulateMessageV2(param bind.Object, receiverParams ReceiverParamsV2, any2suiMessage bind.Object) (*bind.EncodedCall, error) + PopulateMessageV2WithArgs(args ...any) (*bind.EncodedCall, error) + ExtractAny2suiMessageV2(receiverParams ReceiverParamsV2, usedObjectIds []string) (*bind.EncodedCall, error) + ExtractAny2suiMessageV2WithArgs(args ...any) (*bind.EncodedCall, error) + ConsumeAny2suiMessageV2(typeArgs []string, ref bind.Object, message bind.Object, param bind.Object) (*bind.EncodedCall, error) + ConsumeAny2suiMessageV2WithArgs(typeArgs []string, args ...any) (*bind.EncodedCall, error) + GetDestTokenTransferDataV2(receiverParams ReceiverParamsV2) (*bind.EncodedCall, error) + GetDestTokenTransferDataV2WithArgs(args ...any) (*bind.EncodedCall, error) + CompleteTokenTransferV2(typeArgs []string, ref bind.Object, receiverParams ReceiverParamsV2, param bind.Object) (*bind.EncodedCall, error) + CompleteTokenTransferV2WithArgs(typeArgs []string, args ...any) (*bind.EncodedCall, error) + DeconstructReceiverParamsV2(param bind.Object, receiverParams ReceiverParamsV2) (*bind.EncodedCall, error) + DeconstructReceiverParamsV2WithArgs(args ...any) (*bind.EncodedCall, error) } type OfframpStateHelperContract struct { @@ -146,6 +178,13 @@ type DestTokenTransfer struct { OffchainTokenData []byte `move:"vector"` } +type ReceiverParamsV2 struct { + TokenTransfer *DestTokenTransfer `move:"0x1::option::Option"` + Message *bind.Object `move:"0x1::option::Option"` + SourceChainSelector uint64 `move:"u64"` + Receipt *CompletedDestTokenTransfer `move:"0x1::option::Option"` +} + type bcsCompletedDestTokenTransfer struct { TokenReceiver [32]byte DestTokenAddress [32]byte @@ -302,6 +341,23 @@ func init() { } return results, nil }) + bind.RegisterStructDecoder("ccip::offramp_state_helper::ReceiverParamsV2", func(data []byte) (interface{}, error) { + var result ReceiverParamsV2 + _, err := mystenbcs.Unmarshal(data, &result) + if err != nil { + return nil, err + } + return result, nil + }) + // Register vector decoder for ReceiverParamsV2 + bind.RegisterStructDecoder("vector", func(data []byte) (interface{}, error) { + var results []ReceiverParamsV2 + _, err := mystenbcs.Unmarshal(data, &results) + if err != nil { + return nil, err + } + return results, nil + }) } // NewDestTransferCap executes the new_dest_transfer_cap Move function. @@ -424,6 +480,96 @@ func (c *OfframpStateHelperContract) DeconstructReceiverParams(ctx context.Conte return c.ExecuteTransaction(ctx, opts, encoded) } +// NewAny2suiMessageV2 executes the new_any2sui_message_v2 Move function. +func (c *OfframpStateHelperContract) NewAny2suiMessageV2(ctx context.Context, opts *bind.CallOpts, param bind.Object, messageId []byte, sourceChainSelector uint64, sender []byte, data []byte, messageReceiver string, tokenReceiver string, receiverObjectIds []string, tokenAddresses []string, tokenAmounts []*big.Int) (*models.SuiTransactionBlockResponse, error) { + encoded, err := c.offrampStateHelperEncoder.NewAny2suiMessageV2(param, messageId, sourceChainSelector, sender, data, messageReceiver, tokenReceiver, receiverObjectIds, tokenAddresses, tokenAmounts) + if err != nil { + return nil, fmt.Errorf("failed to encode function call: %w", err) + } + + return c.ExecuteTransaction(ctx, opts, encoded) +} + +// CreateReceiverParamsV2 executes the create_receiver_params_v2 Move function. +func (c *OfframpStateHelperContract) CreateReceiverParamsV2(ctx context.Context, opts *bind.CallOpts, param bind.Object, sourceChainSelector uint64) (*models.SuiTransactionBlockResponse, error) { + encoded, err := c.offrampStateHelperEncoder.CreateReceiverParamsV2(param, sourceChainSelector) + if err != nil { + return nil, fmt.Errorf("failed to encode function call: %w", err) + } + + return c.ExecuteTransaction(ctx, opts, encoded) +} + +// AddDestTokenTransferV2 executes the add_dest_token_transfer_v2 Move function. +func (c *OfframpStateHelperContract) AddDestTokenTransferV2(ctx context.Context, opts *bind.CallOpts, param bind.Object, receiverParams ReceiverParamsV2, tokenReceiver string, remoteChainSelector uint64, sourceAmount *big.Int, destTokenAddress string, destTokenPoolPackageId string, sourcePoolAddress []byte, sourcePoolData []byte, offchainData []byte) (*models.SuiTransactionBlockResponse, error) { + encoded, err := c.offrampStateHelperEncoder.AddDestTokenTransferV2(param, receiverParams, tokenReceiver, remoteChainSelector, sourceAmount, destTokenAddress, destTokenPoolPackageId, sourcePoolAddress, sourcePoolData, offchainData) + if err != nil { + return nil, fmt.Errorf("failed to encode function call: %w", err) + } + + return c.ExecuteTransaction(ctx, opts, encoded) +} + +// PopulateMessageV2 executes the populate_message_v2 Move function. +func (c *OfframpStateHelperContract) PopulateMessageV2(ctx context.Context, opts *bind.CallOpts, param bind.Object, receiverParams ReceiverParamsV2, any2suiMessage bind.Object) (*models.SuiTransactionBlockResponse, error) { + encoded, err := c.offrampStateHelperEncoder.PopulateMessageV2(param, receiverParams, any2suiMessage) + if err != nil { + return nil, fmt.Errorf("failed to encode function call: %w", err) + } + + return c.ExecuteTransaction(ctx, opts, encoded) +} + +// ExtractAny2suiMessageV2 executes the extract_any2sui_message_v2 Move function. +func (c *OfframpStateHelperContract) ExtractAny2suiMessageV2(ctx context.Context, opts *bind.CallOpts, receiverParams ReceiverParamsV2, usedObjectIds []string) (*models.SuiTransactionBlockResponse, error) { + encoded, err := c.offrampStateHelperEncoder.ExtractAny2suiMessageV2(receiverParams, usedObjectIds) + if err != nil { + return nil, fmt.Errorf("failed to encode function call: %w", err) + } + + return c.ExecuteTransaction(ctx, opts, encoded) +} + +// ConsumeAny2suiMessageV2 executes the consume_any2sui_message_v2 Move function. +func (c *OfframpStateHelperContract) ConsumeAny2suiMessageV2(ctx context.Context, opts *bind.CallOpts, typeArgs []string, ref bind.Object, message bind.Object, param bind.Object) (*models.SuiTransactionBlockResponse, error) { + encoded, err := c.offrampStateHelperEncoder.ConsumeAny2suiMessageV2(typeArgs, ref, message, param) + if err != nil { + return nil, fmt.Errorf("failed to encode function call: %w", err) + } + + return c.ExecuteTransaction(ctx, opts, encoded) +} + +// GetDestTokenTransferDataV2 executes the get_dest_token_transfer_data_v2 Move function. +func (c *OfframpStateHelperContract) GetDestTokenTransferDataV2(ctx context.Context, opts *bind.CallOpts, receiverParams ReceiverParamsV2) (*models.SuiTransactionBlockResponse, error) { + encoded, err := c.offrampStateHelperEncoder.GetDestTokenTransferDataV2(receiverParams) + if err != nil { + return nil, fmt.Errorf("failed to encode function call: %w", err) + } + + return c.ExecuteTransaction(ctx, opts, encoded) +} + +// CompleteTokenTransferV2 executes the complete_token_transfer_v2 Move function. +func (c *OfframpStateHelperContract) CompleteTokenTransferV2(ctx context.Context, opts *bind.CallOpts, typeArgs []string, ref bind.Object, receiverParams ReceiverParamsV2, param bind.Object) (*models.SuiTransactionBlockResponse, error) { + encoded, err := c.offrampStateHelperEncoder.CompleteTokenTransferV2(typeArgs, ref, receiverParams, param) + if err != nil { + return nil, fmt.Errorf("failed to encode function call: %w", err) + } + + return c.ExecuteTransaction(ctx, opts, encoded) +} + +// DeconstructReceiverParamsV2 executes the deconstruct_receiver_params_v2 Move function. +func (c *OfframpStateHelperContract) DeconstructReceiverParamsV2(ctx context.Context, opts *bind.CallOpts, param bind.Object, receiverParams ReceiverParamsV2) (*models.SuiTransactionBlockResponse, error) { + encoded, err := c.offrampStateHelperEncoder.DeconstructReceiverParamsV2(param, receiverParams) + if err != nil { + return nil, fmt.Errorf("failed to encode function call: %w", err) + } + + return c.ExecuteTransaction(ctx, opts, encoded) +} + // NewDestTransferCap executes the new_dest_transfer_cap Move function using DevInspect to get return values. // // Returns: DestTransferCap @@ -591,6 +737,112 @@ func (d *OfframpStateHelperDevInspect) ConsumeAny2suiMessage(ctx context.Context return d.contract.Call(ctx, opts, encoded) } +// NewAny2suiMessageV2 executes the new_any2sui_message_v2 Move function using DevInspect to get return values. +// +// Returns: client::Any2SuiMessageV2 +func (d *OfframpStateHelperDevInspect) NewAny2suiMessageV2(ctx context.Context, opts *bind.CallOpts, param bind.Object, messageId []byte, sourceChainSelector uint64, sender []byte, data []byte, messageReceiver string, tokenReceiver string, receiverObjectIds []string, tokenAddresses []string, tokenAmounts []*big.Int) (bind.Object, error) { + encoded, err := d.contract.offrampStateHelperEncoder.NewAny2suiMessageV2(param, messageId, sourceChainSelector, sender, data, messageReceiver, tokenReceiver, receiverObjectIds, tokenAddresses, tokenAmounts) + if err != nil { + return bind.Object{}, fmt.Errorf("failed to encode function call: %w", err) + } + results, err := d.contract.Call(ctx, opts, encoded) + if err != nil { + return bind.Object{}, err + } + if len(results) == 0 { + return bind.Object{}, fmt.Errorf("no return value") + } + result, ok := results[0].(bind.Object) + if !ok { + return bind.Object{}, fmt.Errorf("unexpected return type: expected bind.Object, got %T", results[0]) + } + return result, nil +} + +// CreateReceiverParamsV2 executes the create_receiver_params_v2 Move function using DevInspect to get return values. +// +// Returns: ReceiverParamsV2 +func (d *OfframpStateHelperDevInspect) CreateReceiverParamsV2(ctx context.Context, opts *bind.CallOpts, param bind.Object, sourceChainSelector uint64) (ReceiverParamsV2, error) { + encoded, err := d.contract.offrampStateHelperEncoder.CreateReceiverParamsV2(param, sourceChainSelector) + if err != nil { + return ReceiverParamsV2{}, fmt.Errorf("failed to encode function call: %w", err) + } + results, err := d.contract.Call(ctx, opts, encoded) + if err != nil { + return ReceiverParamsV2{}, err + } + if len(results) == 0 { + return ReceiverParamsV2{}, fmt.Errorf("no return value") + } + result, ok := results[0].(ReceiverParamsV2) + if !ok { + return ReceiverParamsV2{}, fmt.Errorf("unexpected return type: expected ReceiverParamsV2, got %T", results[0]) + } + return result, nil +} + +// ExtractAny2suiMessageV2 executes the extract_any2sui_message_v2 Move function using DevInspect to get return values. +// +// Returns: client::Any2SuiMessageV2 +func (d *OfframpStateHelperDevInspect) ExtractAny2suiMessageV2(ctx context.Context, opts *bind.CallOpts, receiverParams ReceiverParamsV2, usedObjectIds []string) (bind.Object, error) { + encoded, err := d.contract.offrampStateHelperEncoder.ExtractAny2suiMessageV2(receiverParams, usedObjectIds) + if err != nil { + return bind.Object{}, fmt.Errorf("failed to encode function call: %w", err) + } + results, err := d.contract.Call(ctx, opts, encoded) + if err != nil { + return bind.Object{}, err + } + if len(results) == 0 { + return bind.Object{}, fmt.Errorf("no return value") + } + result, ok := results[0].(bind.Object) + if !ok { + return bind.Object{}, fmt.Errorf("unexpected return type: expected bind.Object, got %T", results[0]) + } + return result, nil +} + +// ConsumeAny2suiMessageV2 executes the consume_any2sui_message_v2 Move function using DevInspect to get return values. +// +// Returns: +// +// [0]: vector +// [1]: u64 +// [2]: vector +// [3]: vector +// [4]: address +// [5]: address +// [6]: vector
+// [7]: vector +func (d *OfframpStateHelperDevInspect) ConsumeAny2suiMessageV2(ctx context.Context, opts *bind.CallOpts, typeArgs []string, ref bind.Object, message bind.Object, param bind.Object) ([]any, error) { + encoded, err := d.contract.offrampStateHelperEncoder.ConsumeAny2suiMessageV2(typeArgs, ref, message, param) + if err != nil { + return nil, fmt.Errorf("failed to encode function call: %w", err) + } + return d.contract.Call(ctx, opts, encoded) +} + +// GetDestTokenTransferDataV2 executes the get_dest_token_transfer_data_v2 Move function using DevInspect to get return values. +// +// Returns: +// +// [0]: address +// [1]: u64 +// [2]: u256 +// [3]: address +// [4]: address +// [5]: vector +// [6]: vector +// [7]: vector +func (d *OfframpStateHelperDevInspect) GetDestTokenTransferDataV2(ctx context.Context, opts *bind.CallOpts, receiverParams ReceiverParamsV2) ([]any, error) { + encoded, err := d.contract.offrampStateHelperEncoder.GetDestTokenTransferDataV2(receiverParams) + if err != nil { + return nil, fmt.Errorf("failed to encode function call: %w", err) + } + return d.contract.Call(ctx, opts, encoded) +} + type offrampStateHelperEncoder struct { *bind.BoundContract } @@ -1060,3 +1312,374 @@ func (c offrampStateHelperEncoder) DeconstructReceiverParamsWithArgs(args ...any typeParamsList := []string{} return c.EncodeCallArgsWithGenerics("deconstruct_receiver_params", typeArgsList, typeParamsList, expectedParams, args, nil) } + +// NewAny2suiMessageV2 encodes a call to the new_any2sui_message_v2 Move function. +func (c offrampStateHelperEncoder) NewAny2suiMessageV2(param bind.Object, messageId []byte, sourceChainSelector uint64, sender []byte, data []byte, messageReceiver string, tokenReceiver string, receiverObjectIds []string, tokenAddresses []string, tokenAmounts []*big.Int) (*bind.EncodedCall, error) { + typeArgsList := []string{} + typeParamsList := []string{} + return c.EncodeCallArgsWithGenerics("new_any2sui_message_v2", typeArgsList, typeParamsList, []string{ + "&DestTransferCap", + "vector", + "u64", + "vector", + "vector", + "address", + "address", + "vector
", + "vector
", + "vector", + }, []any{ + param, + messageId, + sourceChainSelector, + sender, + data, + messageReceiver, + tokenReceiver, + receiverObjectIds, + tokenAddresses, + tokenAmounts, + }, []string{ + "client::Any2SuiMessageV2", + }) +} + +// NewAny2suiMessageV2WithArgs encodes a call to the new_any2sui_message_v2 Move function using arbitrary arguments. +// This method allows passing both regular values and transaction.Argument values for PTB chaining. +func (c offrampStateHelperEncoder) NewAny2suiMessageV2WithArgs(args ...any) (*bind.EncodedCall, error) { + expectedParams := []string{ + "&DestTransferCap", + "vector", + "u64", + "vector", + "vector", + "address", + "address", + "vector
", + "vector
", + "vector", + } + + if len(args) != len(expectedParams) { + return nil, fmt.Errorf("expected %d arguments, got %d", len(expectedParams), len(args)) + } + typeArgsList := []string{} + typeParamsList := []string{} + return c.EncodeCallArgsWithGenerics("new_any2sui_message_v2", typeArgsList, typeParamsList, expectedParams, args, []string{ + "client::Any2SuiMessageV2", + }) +} + +// CreateReceiverParamsV2 encodes a call to the create_receiver_params_v2 Move function. +func (c offrampStateHelperEncoder) CreateReceiverParamsV2(param bind.Object, sourceChainSelector uint64) (*bind.EncodedCall, error) { + typeArgsList := []string{} + typeParamsList := []string{} + return c.EncodeCallArgsWithGenerics("create_receiver_params_v2", typeArgsList, typeParamsList, []string{ + "&DestTransferCap", + "u64", + }, []any{ + param, + sourceChainSelector, + }, []string{ + "ccip::offramp_state_helper::ReceiverParamsV2", + }) +} + +// CreateReceiverParamsV2WithArgs encodes a call to the create_receiver_params_v2 Move function using arbitrary arguments. +// This method allows passing both regular values and transaction.Argument values for PTB chaining. +func (c offrampStateHelperEncoder) CreateReceiverParamsV2WithArgs(args ...any) (*bind.EncodedCall, error) { + expectedParams := []string{ + "&DestTransferCap", + "u64", + } + + if len(args) != len(expectedParams) { + return nil, fmt.Errorf("expected %d arguments, got %d", len(expectedParams), len(args)) + } + typeArgsList := []string{} + typeParamsList := []string{} + return c.EncodeCallArgsWithGenerics("create_receiver_params_v2", typeArgsList, typeParamsList, expectedParams, args, []string{ + "ccip::offramp_state_helper::ReceiverParamsV2", + }) +} + +// AddDestTokenTransferV2 encodes a call to the add_dest_token_transfer_v2 Move function. +func (c offrampStateHelperEncoder) AddDestTokenTransferV2(param bind.Object, receiverParams ReceiverParamsV2, tokenReceiver string, remoteChainSelector uint64, sourceAmount *big.Int, destTokenAddress string, destTokenPoolPackageId string, sourcePoolAddress []byte, sourcePoolData []byte, offchainData []byte) (*bind.EncodedCall, error) { + typeArgsList := []string{} + typeParamsList := []string{} + return c.EncodeCallArgsWithGenerics("add_dest_token_transfer_v2", typeArgsList, typeParamsList, []string{ + "&DestTransferCap", + "&mut ReceiverParamsV2", + "address", + "u64", + "u256", + "address", + "address", + "vector", + "vector", + "vector", + }, []any{ + param, + receiverParams, + tokenReceiver, + remoteChainSelector, + sourceAmount, + destTokenAddress, + destTokenPoolPackageId, + sourcePoolAddress, + sourcePoolData, + offchainData, + }, nil) +} + +// AddDestTokenTransferV2WithArgs encodes a call to the add_dest_token_transfer_v2 Move function using arbitrary arguments. +// This method allows passing both regular values and transaction.Argument values for PTB chaining. +func (c offrampStateHelperEncoder) AddDestTokenTransferV2WithArgs(args ...any) (*bind.EncodedCall, error) { + expectedParams := []string{ + "&DestTransferCap", + "&mut ReceiverParamsV2", + "address", + "u64", + "u256", + "address", + "address", + "vector", + "vector", + "vector", + } + + if len(args) != len(expectedParams) { + return nil, fmt.Errorf("expected %d arguments, got %d", len(expectedParams), len(args)) + } + typeArgsList := []string{} + typeParamsList := []string{} + return c.EncodeCallArgsWithGenerics("add_dest_token_transfer_v2", typeArgsList, typeParamsList, expectedParams, args, nil) +} + +// PopulateMessageV2 encodes a call to the populate_message_v2 Move function. +func (c offrampStateHelperEncoder) PopulateMessageV2(param bind.Object, receiverParams ReceiverParamsV2, any2suiMessage bind.Object) (*bind.EncodedCall, error) { + typeArgsList := []string{} + typeParamsList := []string{} + return c.EncodeCallArgsWithGenerics("populate_message_v2", typeArgsList, typeParamsList, []string{ + "&DestTransferCap", + "&mut ReceiverParamsV2", + "client::Any2SuiMessageV2", + }, []any{ + param, + receiverParams, + any2suiMessage, + }, nil) +} + +// PopulateMessageV2WithArgs encodes a call to the populate_message_v2 Move function using arbitrary arguments. +// This method allows passing both regular values and transaction.Argument values for PTB chaining. +func (c offrampStateHelperEncoder) PopulateMessageV2WithArgs(args ...any) (*bind.EncodedCall, error) { + expectedParams := []string{ + "&DestTransferCap", + "&mut ReceiverParamsV2", + "client::Any2SuiMessageV2", + } + + if len(args) != len(expectedParams) { + return nil, fmt.Errorf("expected %d arguments, got %d", len(expectedParams), len(args)) + } + typeArgsList := []string{} + typeParamsList := []string{} + return c.EncodeCallArgsWithGenerics("populate_message_v2", typeArgsList, typeParamsList, expectedParams, args, nil) +} + +// ExtractAny2suiMessageV2 encodes a call to the extract_any2sui_message_v2 Move function. +func (c offrampStateHelperEncoder) ExtractAny2suiMessageV2(receiverParams ReceiverParamsV2, usedObjectIds []string) (*bind.EncodedCall, error) { + typeArgsList := []string{} + typeParamsList := []string{} + return c.EncodeCallArgsWithGenerics("extract_any2sui_message_v2", typeArgsList, typeParamsList, []string{ + "&mut ReceiverParamsV2", + "vector
", + }, []any{ + receiverParams, + usedObjectIds, + }, []string{ + "client::Any2SuiMessageV2", + }) +} + +// ExtractAny2suiMessageV2WithArgs encodes a call to the extract_any2sui_message_v2 Move function using arbitrary arguments. +// This method allows passing both regular values and transaction.Argument values for PTB chaining. +func (c offrampStateHelperEncoder) ExtractAny2suiMessageV2WithArgs(args ...any) (*bind.EncodedCall, error) { + expectedParams := []string{ + "&mut ReceiverParamsV2", + "vector
", + } + + if len(args) != len(expectedParams) { + return nil, fmt.Errorf("expected %d arguments, got %d", len(expectedParams), len(args)) + } + typeArgsList := []string{} + typeParamsList := []string{} + return c.EncodeCallArgsWithGenerics("extract_any2sui_message_v2", typeArgsList, typeParamsList, expectedParams, args, []string{ + "client::Any2SuiMessageV2", + }) +} + +// ConsumeAny2suiMessageV2 encodes a call to the consume_any2sui_message_v2 Move function. +func (c offrampStateHelperEncoder) ConsumeAny2suiMessageV2(typeArgs []string, ref bind.Object, message bind.Object, param bind.Object) (*bind.EncodedCall, error) { + typeArgsList := typeArgs + typeParamsList := []string{ + "TypeProof", + } + return c.EncodeCallArgsWithGenerics("consume_any2sui_message_v2", typeArgsList, typeParamsList, []string{ + "&CCIPObjectRef", + "client::Any2SuiMessageV2", + "TypeProof", + }, []any{ + ref, + message, + param, + }, []string{ + "vector", + "u64", + "vector", + "vector", + "address", + "address", + "vector
", + "vector", + }) +} + +// ConsumeAny2suiMessageV2WithArgs encodes a call to the consume_any2sui_message_v2 Move function using arbitrary arguments. +// This method allows passing both regular values and transaction.Argument values for PTB chaining. +func (c offrampStateHelperEncoder) ConsumeAny2suiMessageV2WithArgs(typeArgs []string, args ...any) (*bind.EncodedCall, error) { + expectedParams := []string{ + "&CCIPObjectRef", + "client::Any2SuiMessageV2", + "TypeProof", + } + + if len(args) != len(expectedParams) { + return nil, fmt.Errorf("expected %d arguments, got %d", len(expectedParams), len(args)) + } + typeArgsList := typeArgs + typeParamsList := []string{ + "TypeProof", + } + return c.EncodeCallArgsWithGenerics("consume_any2sui_message_v2", typeArgsList, typeParamsList, expectedParams, args, []string{ + "vector", + "u64", + "vector", + "vector", + "address", + "address", + "vector
", + "vector", + }) +} + +// GetDestTokenTransferDataV2 encodes a call to the get_dest_token_transfer_data_v2 Move function. +func (c offrampStateHelperEncoder) GetDestTokenTransferDataV2(receiverParams ReceiverParamsV2) (*bind.EncodedCall, error) { + typeArgsList := []string{} + typeParamsList := []string{} + return c.EncodeCallArgsWithGenerics("get_dest_token_transfer_data_v2", typeArgsList, typeParamsList, []string{ + "&ReceiverParamsV2", + }, []any{ + receiverParams, + }, []string{ + "address", + "u64", + "u256", + "address", + "address", + "vector", + "vector", + "vector", + }) +} + +// GetDestTokenTransferDataV2WithArgs encodes a call to the get_dest_token_transfer_data_v2 Move function using arbitrary arguments. +// This method allows passing both regular values and transaction.Argument values for PTB chaining. +func (c offrampStateHelperEncoder) GetDestTokenTransferDataV2WithArgs(args ...any) (*bind.EncodedCall, error) { + expectedParams := []string{ + "&ReceiverParamsV2", + } + + if len(args) != len(expectedParams) { + return nil, fmt.Errorf("expected %d arguments, got %d", len(expectedParams), len(args)) + } + typeArgsList := []string{} + typeParamsList := []string{} + return c.EncodeCallArgsWithGenerics("get_dest_token_transfer_data_v2", typeArgsList, typeParamsList, expectedParams, args, []string{ + "address", + "u64", + "u256", + "address", + "address", + "vector", + "vector", + "vector", + }) +} + +// CompleteTokenTransferV2 encodes a call to the complete_token_transfer_v2 Move function. +func (c offrampStateHelperEncoder) CompleteTokenTransferV2(typeArgs []string, ref bind.Object, receiverParams ReceiverParamsV2, param bind.Object) (*bind.EncodedCall, error) { + typeArgsList := typeArgs + typeParamsList := []string{ + "TypeProof", + } + return c.EncodeCallArgsWithGenerics("complete_token_transfer_v2", typeArgsList, typeParamsList, []string{ + "&CCIPObjectRef", + "&mut ReceiverParamsV2", + "TypeProof", + }, []any{ + ref, + receiverParams, + param, + }, nil) +} + +// CompleteTokenTransferV2WithArgs encodes a call to the complete_token_transfer_v2 Move function using arbitrary arguments. +// This method allows passing both regular values and transaction.Argument values for PTB chaining. +func (c offrampStateHelperEncoder) CompleteTokenTransferV2WithArgs(typeArgs []string, args ...any) (*bind.EncodedCall, error) { + expectedParams := []string{ + "&CCIPObjectRef", + "&mut ReceiverParamsV2", + "TypeProof", + } + + if len(args) != len(expectedParams) { + return nil, fmt.Errorf("expected %d arguments, got %d", len(expectedParams), len(args)) + } + typeArgsList := typeArgs + typeParamsList := []string{ + "TypeProof", + } + return c.EncodeCallArgsWithGenerics("complete_token_transfer_v2", typeArgsList, typeParamsList, expectedParams, args, nil) +} + +// DeconstructReceiverParamsV2 encodes a call to the deconstruct_receiver_params_v2 Move function. +func (c offrampStateHelperEncoder) DeconstructReceiverParamsV2(param bind.Object, receiverParams ReceiverParamsV2) (*bind.EncodedCall, error) { + typeArgsList := []string{} + typeParamsList := []string{} + return c.EncodeCallArgsWithGenerics("deconstruct_receiver_params_v2", typeArgsList, typeParamsList, []string{ + "&DestTransferCap", + "ccip::offramp_state_helper::ReceiverParamsV2", + }, []any{ + param, + receiverParams, + }, nil) +} + +// DeconstructReceiverParamsV2WithArgs encodes a call to the deconstruct_receiver_params_v2 Move function using arbitrary arguments. +// This method allows passing both regular values and transaction.Argument values for PTB chaining. +func (c offrampStateHelperEncoder) DeconstructReceiverParamsV2WithArgs(args ...any) (*bind.EncodedCall, error) { + expectedParams := []string{ + "&DestTransferCap", + "ccip::offramp_state_helper::ReceiverParamsV2", + } + + if len(args) != len(expectedParams) { + return nil, fmt.Errorf("expected %d arguments, got %d", len(expectedParams), len(args)) + } + typeArgsList := []string{} + typeParamsList := []string{} + return c.EncodeCallArgsWithGenerics("deconstruct_receiver_params_v2", typeArgsList, typeParamsList, expectedParams, args, nil) +} diff --git a/bindings/generated/ccip/ccip/rmn_remote/rmn_remote.go b/bindings/generated/ccip/ccip/rmn_remote/rmn_remote.go index 2f58ad24e..c0ec4c502 100644 --- a/bindings/generated/ccip/ccip/rmn_remote/rmn_remote.go +++ b/bindings/generated/ccip/ccip/rmn_remote/rmn_remote.go @@ -19,7 +19,7 @@ var ( _ = big.NewInt ) -const FunctionInfo = `[{"package":"ccip","module":"rmn_remote","name":"curse","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"owner_cap","type":"OwnerCap"},{"name":"subject","type":"vector"}]},{"package":"ccip","module":"rmn_remote","name":"curse_multiple","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"owner_cap","type":"OwnerCap"},{"name":"subjects","type":"vector>"}]},{"package":"ccip","module":"rmn_remote","name":"get_cursed_subjects","parameters":[{"name":"ref","type":"CCIPObjectRef"}]},{"package":"ccip","module":"rmn_remote","name":"get_local_chain_selector","parameters":[{"name":"ref","type":"CCIPObjectRef"}]},{"package":"ccip","module":"rmn_remote","name":"get_report_digest_header","parameters":null},{"package":"ccip","module":"rmn_remote","name":"get_versioned_config","parameters":[{"name":"ref","type":"CCIPObjectRef"}]},{"package":"ccip","module":"rmn_remote","name":"initialize","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"owner_cap","type":"OwnerCap"},{"name":"local_chain_selector","type":"u64"}]},{"package":"ccip","module":"rmn_remote","name":"is_cursed","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"subject","type":"vector"}]},{"package":"ccip","module":"rmn_remote","name":"is_cursed_global","parameters":[{"name":"ref","type":"CCIPObjectRef"}]},{"package":"ccip","module":"rmn_remote","name":"is_cursed_u128","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"subject_value","type":"u128"}]},{"package":"ccip","module":"rmn_remote","name":"set_config","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"owner_cap","type":"OwnerCap"},{"name":"rmn_home_contract_config_digest","type":"vector"},{"name":"signer_onchain_public_keys","type":"vector>"},{"name":"node_indexes","type":"vector"},{"name":"f_sign","type":"u64"}]},{"package":"ccip","module":"rmn_remote","name":"type_and_version","parameters":null},{"package":"ccip","module":"rmn_remote","name":"uncurse","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"owner_cap","type":"OwnerCap"},{"name":"subject","type":"vector"}]},{"package":"ccip","module":"rmn_remote","name":"uncurse_multiple","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"owner_cap","type":"OwnerCap"},{"name":"subjects","type":"vector>"}]}]` +const FunctionInfo = `[{"package":"ccip","module":"rmn_remote","name":"create_curser_cap","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"owner_cap","type":"OwnerCap"}]},{"package":"ccip","module":"rmn_remote","name":"curse","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"owner_cap","type":"OwnerCap"},{"name":"subject","type":"vector"}]},{"package":"ccip","module":"rmn_remote","name":"curse_multiple","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"owner_cap","type":"OwnerCap"},{"name":"subjects","type":"vector>"}]},{"package":"ccip","module":"rmn_remote","name":"curse_multiple_with_curser_cap","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"_curser_cap","type":"CurserCap"},{"name":"subjects","type":"vector>"}]},{"package":"ccip","module":"rmn_remote","name":"curse_with_curser_cap","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"curser_cap","type":"CurserCap"},{"name":"subject","type":"vector"}]},{"package":"ccip","module":"rmn_remote","name":"get_cursed_subjects","parameters":[{"name":"ref","type":"CCIPObjectRef"}]},{"package":"ccip","module":"rmn_remote","name":"get_local_chain_selector","parameters":[{"name":"ref","type":"CCIPObjectRef"}]},{"package":"ccip","module":"rmn_remote","name":"get_report_digest_header","parameters":null},{"package":"ccip","module":"rmn_remote","name":"get_versioned_config","parameters":[{"name":"ref","type":"CCIPObjectRef"}]},{"package":"ccip","module":"rmn_remote","name":"initialize","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"owner_cap","type":"OwnerCap"},{"name":"local_chain_selector","type":"u64"}]},{"package":"ccip","module":"rmn_remote","name":"is_cursed","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"subject","type":"vector"}]},{"package":"ccip","module":"rmn_remote","name":"is_cursed_global","parameters":[{"name":"ref","type":"CCIPObjectRef"}]},{"package":"ccip","module":"rmn_remote","name":"is_cursed_u128","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"subject_value","type":"u128"}]},{"package":"ccip","module":"rmn_remote","name":"set_config","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"owner_cap","type":"OwnerCap"},{"name":"rmn_home_contract_config_digest","type":"vector"},{"name":"signer_onchain_public_keys","type":"vector>"},{"name":"node_indexes","type":"vector"},{"name":"f_sign","type":"u64"}]},{"package":"ccip","module":"rmn_remote","name":"type_and_version","parameters":null},{"package":"ccip","module":"rmn_remote","name":"uncurse","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"owner_cap","type":"OwnerCap"},{"name":"subject","type":"vector"}]},{"package":"ccip","module":"rmn_remote","name":"uncurse_multiple","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"owner_cap","type":"OwnerCap"},{"name":"subjects","type":"vector>"}]}]` type IRmnRemote interface { TypeAndVersion(ctx context.Context, opts *bind.CallOpts) (*models.SuiTransactionBlockResponse, error) @@ -30,6 +30,9 @@ type IRmnRemote interface { GetReportDigestHeader(ctx context.Context, opts *bind.CallOpts) (*models.SuiTransactionBlockResponse, error) Curse(ctx context.Context, opts *bind.CallOpts, ref bind.Object, ownerCap bind.Object, subject []byte) (*models.SuiTransactionBlockResponse, error) CurseMultiple(ctx context.Context, opts *bind.CallOpts, ref bind.Object, ownerCap bind.Object, subjects [][]byte) (*models.SuiTransactionBlockResponse, error) + CurseWithCurserCap(ctx context.Context, opts *bind.CallOpts, ref bind.Object, curserCap bind.Object, subject []byte) (*models.SuiTransactionBlockResponse, error) + CurseMultipleWithCurserCap(ctx context.Context, opts *bind.CallOpts, ref bind.Object, curserCap bind.Object, subjects [][]byte) (*models.SuiTransactionBlockResponse, error) + CreateCurserCap(ctx context.Context, opts *bind.CallOpts, ref bind.Object, ownerCap bind.Object) (*models.SuiTransactionBlockResponse, error) Uncurse(ctx context.Context, opts *bind.CallOpts, ref bind.Object, ownerCap bind.Object, subject []byte) (*models.SuiTransactionBlockResponse, error) UncurseMultiple(ctx context.Context, opts *bind.CallOpts, ref bind.Object, ownerCap bind.Object, subjects [][]byte) (*models.SuiTransactionBlockResponse, error) GetCursedSubjects(ctx context.Context, opts *bind.CallOpts, ref bind.Object) (*models.SuiTransactionBlockResponse, error) @@ -41,6 +44,11 @@ type IRmnRemote interface { McmsCurseMultiple(ctx context.Context, opts *bind.CallOpts, ref bind.Object, registry bind.Object, params bind.Object) (*models.SuiTransactionBlockResponse, error) McmsUncurse(ctx context.Context, opts *bind.CallOpts, ref bind.Object, registry bind.Object, params bind.Object) (*models.SuiTransactionBlockResponse, error) McmsUncurseMultiple(ctx context.Context, opts *bind.CallOpts, ref bind.Object, registry bind.Object, params bind.Object) (*models.SuiTransactionBlockResponse, error) + McmsCurseWithCurserCap(ctx context.Context, opts *bind.CallOpts, ref bind.Object, registry bind.Object, params bind.Object) (*models.SuiTransactionBlockResponse, error) + McmsCurseMultipleWithCurserCap(ctx context.Context, opts *bind.CallOpts, ref bind.Object, registry bind.Object, params bind.Object) (*models.SuiTransactionBlockResponse, error) + McmsCreateCurserCap(ctx context.Context, opts *bind.CallOpts, ref bind.Object, registry bind.Object, params bind.Object) (*models.SuiTransactionBlockResponse, error) + McmsRegisterCurserCap(ctx context.Context, opts *bind.CallOpts, ref bind.Object, slowRegistry bind.Object, fastRegistry bind.Object, params bind.Object, curserCap bind.Object) (*models.SuiTransactionBlockResponse, error) + McmsMintAndRegisterCurserCap(ctx context.Context, opts *bind.CallOpts, ref bind.Object, slowRegistry bind.Object, fastRegistry bind.Object, params bind.Object) (*models.SuiTransactionBlockResponse, error) DevInspect() IRmnRemoteDevInspect Encoder() RmnRemoteEncoder Bound() bind.IBoundContract @@ -51,10 +59,12 @@ type IRmnRemoteDevInspect interface { GetVersionedConfig(ctx context.Context, opts *bind.CallOpts, ref bind.Object) ([]any, error) GetLocalChainSelector(ctx context.Context, opts *bind.CallOpts, ref bind.Object) (uint64, error) GetReportDigestHeader(ctx context.Context, opts *bind.CallOpts) ([]byte, error) + CreateCurserCap(ctx context.Context, opts *bind.CallOpts, ref bind.Object, ownerCap bind.Object) (bind.Object, error) GetCursedSubjects(ctx context.Context, opts *bind.CallOpts, ref bind.Object) ([][]byte, error) IsCursedGlobal(ctx context.Context, opts *bind.CallOpts, ref bind.Object) (bool, error) IsCursed(ctx context.Context, opts *bind.CallOpts, ref bind.Object, subject []byte) (bool, error) IsCursedU128(ctx context.Context, opts *bind.CallOpts, ref bind.Object, subjectValue *big.Int) (bool, error) + McmsCreateCurserCap(ctx context.Context, opts *bind.CallOpts, ref bind.Object, registry bind.Object, params bind.Object) (bind.Object, error) } type RmnRemoteEncoder interface { @@ -74,6 +84,12 @@ type RmnRemoteEncoder interface { CurseWithArgs(args ...any) (*bind.EncodedCall, error) CurseMultiple(ref bind.Object, ownerCap bind.Object, subjects [][]byte) (*bind.EncodedCall, error) CurseMultipleWithArgs(args ...any) (*bind.EncodedCall, error) + CurseWithCurserCap(ref bind.Object, curserCap bind.Object, subject []byte) (*bind.EncodedCall, error) + CurseWithCurserCapWithArgs(args ...any) (*bind.EncodedCall, error) + CurseMultipleWithCurserCap(ref bind.Object, curserCap bind.Object, subjects [][]byte) (*bind.EncodedCall, error) + CurseMultipleWithCurserCapWithArgs(args ...any) (*bind.EncodedCall, error) + CreateCurserCap(ref bind.Object, ownerCap bind.Object) (*bind.EncodedCall, error) + CreateCurserCapWithArgs(args ...any) (*bind.EncodedCall, error) Uncurse(ref bind.Object, ownerCap bind.Object, subject []byte) (*bind.EncodedCall, error) UncurseWithArgs(args ...any) (*bind.EncodedCall, error) UncurseMultiple(ref bind.Object, ownerCap bind.Object, subjects [][]byte) (*bind.EncodedCall, error) @@ -96,6 +112,16 @@ type RmnRemoteEncoder interface { McmsUncurseWithArgs(args ...any) (*bind.EncodedCall, error) McmsUncurseMultiple(ref bind.Object, registry bind.Object, params bind.Object) (*bind.EncodedCall, error) McmsUncurseMultipleWithArgs(args ...any) (*bind.EncodedCall, error) + McmsCurseWithCurserCap(ref bind.Object, registry bind.Object, params bind.Object) (*bind.EncodedCall, error) + McmsCurseWithCurserCapWithArgs(args ...any) (*bind.EncodedCall, error) + McmsCurseMultipleWithCurserCap(ref bind.Object, registry bind.Object, params bind.Object) (*bind.EncodedCall, error) + McmsCurseMultipleWithCurserCapWithArgs(args ...any) (*bind.EncodedCall, error) + McmsCreateCurserCap(ref bind.Object, registry bind.Object, params bind.Object) (*bind.EncodedCall, error) + McmsCreateCurserCapWithArgs(args ...any) (*bind.EncodedCall, error) + McmsRegisterCurserCap(ref bind.Object, slowRegistry bind.Object, fastRegistry bind.Object, params bind.Object, curserCap bind.Object) (*bind.EncodedCall, error) + McmsRegisterCurserCapWithArgs(args ...any) (*bind.EncodedCall, error) + McmsMintAndRegisterCurserCap(ref bind.Object, slowRegistry bind.Object, fastRegistry bind.Object, params bind.Object) (*bind.EncodedCall, error) + McmsMintAndRegisterCurserCapWithArgs(args ...any) (*bind.EncodedCall, error) } type RmnRemoteContract struct { @@ -170,6 +196,10 @@ type Uncursed struct { Subjects [][]byte `move:"vector>"` } +type CurserCap struct { + Id string `move:"sui::object::UID"` +} + func init() { bind.RegisterStructDecoder("ccip::rmn_remote::RMNRemoteState", func(data []byte) (interface{}, error) { var result RMNRemoteState @@ -273,6 +303,23 @@ func init() { } return results, nil }) + bind.RegisterStructDecoder("ccip::rmn_remote::CurserCap", func(data []byte) (interface{}, error) { + var result CurserCap + _, err := mystenbcs.Unmarshal(data, &result) + if err != nil { + return nil, err + } + return result, nil + }) + // Register vector decoder for CurserCap + bind.RegisterStructDecoder("vector", func(data []byte) (interface{}, error) { + var results []CurserCap + _, err := mystenbcs.Unmarshal(data, &results) + if err != nil { + return nil, err + } + return results, nil + }) } // TypeAndVersion executes the type_and_version Move function. @@ -355,6 +402,36 @@ func (c *RmnRemoteContract) CurseMultiple(ctx context.Context, opts *bind.CallOp return c.ExecuteTransaction(ctx, opts, encoded) } +// CurseWithCurserCap executes the curse_with_curser_cap Move function. +func (c *RmnRemoteContract) CurseWithCurserCap(ctx context.Context, opts *bind.CallOpts, ref bind.Object, curserCap bind.Object, subject []byte) (*models.SuiTransactionBlockResponse, error) { + encoded, err := c.rmnRemoteEncoder.CurseWithCurserCap(ref, curserCap, subject) + if err != nil { + return nil, fmt.Errorf("failed to encode function call: %w", err) + } + + return c.ExecuteTransaction(ctx, opts, encoded) +} + +// CurseMultipleWithCurserCap executes the curse_multiple_with_curser_cap Move function. +func (c *RmnRemoteContract) CurseMultipleWithCurserCap(ctx context.Context, opts *bind.CallOpts, ref bind.Object, curserCap bind.Object, subjects [][]byte) (*models.SuiTransactionBlockResponse, error) { + encoded, err := c.rmnRemoteEncoder.CurseMultipleWithCurserCap(ref, curserCap, subjects) + if err != nil { + return nil, fmt.Errorf("failed to encode function call: %w", err) + } + + return c.ExecuteTransaction(ctx, opts, encoded) +} + +// CreateCurserCap executes the create_curser_cap Move function. +func (c *RmnRemoteContract) CreateCurserCap(ctx context.Context, opts *bind.CallOpts, ref bind.Object, ownerCap bind.Object) (*models.SuiTransactionBlockResponse, error) { + encoded, err := c.rmnRemoteEncoder.CreateCurserCap(ref, ownerCap) + if err != nil { + return nil, fmt.Errorf("failed to encode function call: %w", err) + } + + return c.ExecuteTransaction(ctx, opts, encoded) +} + // Uncurse executes the uncurse Move function. func (c *RmnRemoteContract) Uncurse(ctx context.Context, opts *bind.CallOpts, ref bind.Object, ownerCap bind.Object, subject []byte) (*models.SuiTransactionBlockResponse, error) { encoded, err := c.rmnRemoteEncoder.Uncurse(ref, ownerCap, subject) @@ -465,6 +542,56 @@ func (c *RmnRemoteContract) McmsUncurseMultiple(ctx context.Context, opts *bind. return c.ExecuteTransaction(ctx, opts, encoded) } +// McmsCurseWithCurserCap executes the mcms_curse_with_curser_cap Move function. +func (c *RmnRemoteContract) McmsCurseWithCurserCap(ctx context.Context, opts *bind.CallOpts, ref bind.Object, registry bind.Object, params bind.Object) (*models.SuiTransactionBlockResponse, error) { + encoded, err := c.rmnRemoteEncoder.McmsCurseWithCurserCap(ref, registry, params) + if err != nil { + return nil, fmt.Errorf("failed to encode function call: %w", err) + } + + return c.ExecuteTransaction(ctx, opts, encoded) +} + +// McmsCurseMultipleWithCurserCap executes the mcms_curse_multiple_with_curser_cap Move function. +func (c *RmnRemoteContract) McmsCurseMultipleWithCurserCap(ctx context.Context, opts *bind.CallOpts, ref bind.Object, registry bind.Object, params bind.Object) (*models.SuiTransactionBlockResponse, error) { + encoded, err := c.rmnRemoteEncoder.McmsCurseMultipleWithCurserCap(ref, registry, params) + if err != nil { + return nil, fmt.Errorf("failed to encode function call: %w", err) + } + + return c.ExecuteTransaction(ctx, opts, encoded) +} + +// McmsCreateCurserCap executes the mcms_create_curser_cap Move function. +func (c *RmnRemoteContract) McmsCreateCurserCap(ctx context.Context, opts *bind.CallOpts, ref bind.Object, registry bind.Object, params bind.Object) (*models.SuiTransactionBlockResponse, error) { + encoded, err := c.rmnRemoteEncoder.McmsCreateCurserCap(ref, registry, params) + if err != nil { + return nil, fmt.Errorf("failed to encode function call: %w", err) + } + + return c.ExecuteTransaction(ctx, opts, encoded) +} + +// McmsRegisterCurserCap executes the mcms_register_curser_cap Move function. +func (c *RmnRemoteContract) McmsRegisterCurserCap(ctx context.Context, opts *bind.CallOpts, ref bind.Object, slowRegistry bind.Object, fastRegistry bind.Object, params bind.Object, curserCap bind.Object) (*models.SuiTransactionBlockResponse, error) { + encoded, err := c.rmnRemoteEncoder.McmsRegisterCurserCap(ref, slowRegistry, fastRegistry, params, curserCap) + if err != nil { + return nil, fmt.Errorf("failed to encode function call: %w", err) + } + + return c.ExecuteTransaction(ctx, opts, encoded) +} + +// McmsMintAndRegisterCurserCap executes the mcms_mint_and_register_curser_cap Move function. +func (c *RmnRemoteContract) McmsMintAndRegisterCurserCap(ctx context.Context, opts *bind.CallOpts, ref bind.Object, slowRegistry bind.Object, fastRegistry bind.Object, params bind.Object) (*models.SuiTransactionBlockResponse, error) { + encoded, err := c.rmnRemoteEncoder.McmsMintAndRegisterCurserCap(ref, slowRegistry, fastRegistry, params) + if err != nil { + return nil, fmt.Errorf("failed to encode function call: %w", err) + } + + return c.ExecuteTransaction(ctx, opts, encoded) +} + // TypeAndVersion executes the type_and_version Move function using DevInspect to get return values. // // Returns: 0x1::string::String @@ -545,6 +672,28 @@ func (d *RmnRemoteDevInspect) GetReportDigestHeader(ctx context.Context, opts *b return result, nil } +// CreateCurserCap executes the create_curser_cap Move function using DevInspect to get return values. +// +// Returns: CurserCap +func (d *RmnRemoteDevInspect) CreateCurserCap(ctx context.Context, opts *bind.CallOpts, ref bind.Object, ownerCap bind.Object) (bind.Object, error) { + encoded, err := d.contract.rmnRemoteEncoder.CreateCurserCap(ref, ownerCap) + if err != nil { + return bind.Object{}, fmt.Errorf("failed to encode function call: %w", err) + } + results, err := d.contract.Call(ctx, opts, encoded) + if err != nil { + return bind.Object{}, err + } + if len(results) == 0 { + return bind.Object{}, fmt.Errorf("no return value") + } + result, ok := results[0].(bind.Object) + if !ok { + return bind.Object{}, fmt.Errorf("unexpected return type: expected bind.Object, got %T", results[0]) + } + return result, nil +} + // GetCursedSubjects executes the get_cursed_subjects Move function using DevInspect to get return values. // // Returns: vector> @@ -633,6 +782,28 @@ func (d *RmnRemoteDevInspect) IsCursedU128(ctx context.Context, opts *bind.CallO return result, nil } +// McmsCreateCurserCap executes the mcms_create_curser_cap Move function using DevInspect to get return values. +// +// Returns: CurserCap +func (d *RmnRemoteDevInspect) McmsCreateCurserCap(ctx context.Context, opts *bind.CallOpts, ref bind.Object, registry bind.Object, params bind.Object) (bind.Object, error) { + encoded, err := d.contract.rmnRemoteEncoder.McmsCreateCurserCap(ref, registry, params) + if err != nil { + return bind.Object{}, fmt.Errorf("failed to encode function call: %w", err) + } + results, err := d.contract.Call(ctx, opts, encoded) + if err != nil { + return bind.Object{}, err + } + if len(results) == 0 { + return bind.Object{}, fmt.Errorf("no return value") + } + result, ok := results[0].(bind.Object) + if !ok { + return bind.Object{}, fmt.Errorf("unexpected return type: expected bind.Object, got %T", results[0]) + } + return result, nil +} + type rmnRemoteEncoder struct { *bind.BoundContract } @@ -884,6 +1055,103 @@ func (c rmnRemoteEncoder) CurseMultipleWithArgs(args ...any) (*bind.EncodedCall, return c.EncodeCallArgsWithGenerics("curse_multiple", typeArgsList, typeParamsList, expectedParams, args, nil) } +// CurseWithCurserCap encodes a call to the curse_with_curser_cap Move function. +func (c rmnRemoteEncoder) CurseWithCurserCap(ref bind.Object, curserCap bind.Object, subject []byte) (*bind.EncodedCall, error) { + typeArgsList := []string{} + typeParamsList := []string{} + return c.EncodeCallArgsWithGenerics("curse_with_curser_cap", typeArgsList, typeParamsList, []string{ + "&mut CCIPObjectRef", + "&CurserCap", + "vector", + }, []any{ + ref, + curserCap, + subject, + }, nil) +} + +// CurseWithCurserCapWithArgs encodes a call to the curse_with_curser_cap Move function using arbitrary arguments. +// This method allows passing both regular values and transaction.Argument values for PTB chaining. +func (c rmnRemoteEncoder) CurseWithCurserCapWithArgs(args ...any) (*bind.EncodedCall, error) { + expectedParams := []string{ + "&mut CCIPObjectRef", + "&CurserCap", + "vector", + } + + if len(args) != len(expectedParams) { + return nil, fmt.Errorf("expected %d arguments, got %d", len(expectedParams), len(args)) + } + typeArgsList := []string{} + typeParamsList := []string{} + return c.EncodeCallArgsWithGenerics("curse_with_curser_cap", typeArgsList, typeParamsList, expectedParams, args, nil) +} + +// CurseMultipleWithCurserCap encodes a call to the curse_multiple_with_curser_cap Move function. +func (c rmnRemoteEncoder) CurseMultipleWithCurserCap(ref bind.Object, curserCap bind.Object, subjects [][]byte) (*bind.EncodedCall, error) { + typeArgsList := []string{} + typeParamsList := []string{} + return c.EncodeCallArgsWithGenerics("curse_multiple_with_curser_cap", typeArgsList, typeParamsList, []string{ + "&mut CCIPObjectRef", + "&CurserCap", + "vector>", + }, []any{ + ref, + curserCap, + subjects, + }, nil) +} + +// CurseMultipleWithCurserCapWithArgs encodes a call to the curse_multiple_with_curser_cap Move function using arbitrary arguments. +// This method allows passing both regular values and transaction.Argument values for PTB chaining. +func (c rmnRemoteEncoder) CurseMultipleWithCurserCapWithArgs(args ...any) (*bind.EncodedCall, error) { + expectedParams := []string{ + "&mut CCIPObjectRef", + "&CurserCap", + "vector>", + } + + if len(args) != len(expectedParams) { + return nil, fmt.Errorf("expected %d arguments, got %d", len(expectedParams), len(args)) + } + typeArgsList := []string{} + typeParamsList := []string{} + return c.EncodeCallArgsWithGenerics("curse_multiple_with_curser_cap", typeArgsList, typeParamsList, expectedParams, args, nil) +} + +// CreateCurserCap encodes a call to the create_curser_cap Move function. +func (c rmnRemoteEncoder) CreateCurserCap(ref bind.Object, ownerCap bind.Object) (*bind.EncodedCall, error) { + typeArgsList := []string{} + typeParamsList := []string{} + return c.EncodeCallArgsWithGenerics("create_curser_cap", typeArgsList, typeParamsList, []string{ + "&mut CCIPObjectRef", + "&OwnerCap", + }, []any{ + ref, + ownerCap, + }, []string{ + "ccip::rmn_remote::CurserCap", + }) +} + +// CreateCurserCapWithArgs encodes a call to the create_curser_cap Move function using arbitrary arguments. +// This method allows passing both regular values and transaction.Argument values for PTB chaining. +func (c rmnRemoteEncoder) CreateCurserCapWithArgs(args ...any) (*bind.EncodedCall, error) { + expectedParams := []string{ + "&mut CCIPObjectRef", + "&OwnerCap", + } + + if len(args) != len(expectedParams) { + return nil, fmt.Errorf("expected %d arguments, got %d", len(expectedParams), len(args)) + } + typeArgsList := []string{} + typeParamsList := []string{} + return c.EncodeCallArgsWithGenerics("create_curser_cap", typeArgsList, typeParamsList, expectedParams, args, []string{ + "ccip::rmn_remote::CurserCap", + }) +} + // Uncurse encodes a call to the uncurse Move function. func (c rmnRemoteEncoder) Uncurse(ref bind.Object, ownerCap bind.Object, subject []byte) (*bind.EncodedCall, error) { typeArgsList := []string{} @@ -1233,3 +1501,176 @@ func (c rmnRemoteEncoder) McmsUncurseMultipleWithArgs(args ...any) (*bind.Encode typeParamsList := []string{} return c.EncodeCallArgsWithGenerics("mcms_uncurse_multiple", typeArgsList, typeParamsList, expectedParams, args, nil) } + +// McmsCurseWithCurserCap encodes a call to the mcms_curse_with_curser_cap Move function. +func (c rmnRemoteEncoder) McmsCurseWithCurserCap(ref bind.Object, registry bind.Object, params bind.Object) (*bind.EncodedCall, error) { + typeArgsList := []string{} + typeParamsList := []string{} + return c.EncodeCallArgsWithGenerics("mcms_curse_with_curser_cap", typeArgsList, typeParamsList, []string{ + "&mut CCIPObjectRef", + "&mut Registry", + "ExecutingCallbackParams", + }, []any{ + ref, + registry, + params, + }, nil) +} + +// McmsCurseWithCurserCapWithArgs encodes a call to the mcms_curse_with_curser_cap Move function using arbitrary arguments. +// This method allows passing both regular values and transaction.Argument values for PTB chaining. +func (c rmnRemoteEncoder) McmsCurseWithCurserCapWithArgs(args ...any) (*bind.EncodedCall, error) { + expectedParams := []string{ + "&mut CCIPObjectRef", + "&mut Registry", + "ExecutingCallbackParams", + } + + if len(args) != len(expectedParams) { + return nil, fmt.Errorf("expected %d arguments, got %d", len(expectedParams), len(args)) + } + typeArgsList := []string{} + typeParamsList := []string{} + return c.EncodeCallArgsWithGenerics("mcms_curse_with_curser_cap", typeArgsList, typeParamsList, expectedParams, args, nil) +} + +// McmsCurseMultipleWithCurserCap encodes a call to the mcms_curse_multiple_with_curser_cap Move function. +func (c rmnRemoteEncoder) McmsCurseMultipleWithCurserCap(ref bind.Object, registry bind.Object, params bind.Object) (*bind.EncodedCall, error) { + typeArgsList := []string{} + typeParamsList := []string{} + return c.EncodeCallArgsWithGenerics("mcms_curse_multiple_with_curser_cap", typeArgsList, typeParamsList, []string{ + "&mut CCIPObjectRef", + "&mut Registry", + "ExecutingCallbackParams", + }, []any{ + ref, + registry, + params, + }, nil) +} + +// McmsCurseMultipleWithCurserCapWithArgs encodes a call to the mcms_curse_multiple_with_curser_cap Move function using arbitrary arguments. +// This method allows passing both regular values and transaction.Argument values for PTB chaining. +func (c rmnRemoteEncoder) McmsCurseMultipleWithCurserCapWithArgs(args ...any) (*bind.EncodedCall, error) { + expectedParams := []string{ + "&mut CCIPObjectRef", + "&mut Registry", + "ExecutingCallbackParams", + } + + if len(args) != len(expectedParams) { + return nil, fmt.Errorf("expected %d arguments, got %d", len(expectedParams), len(args)) + } + typeArgsList := []string{} + typeParamsList := []string{} + return c.EncodeCallArgsWithGenerics("mcms_curse_multiple_with_curser_cap", typeArgsList, typeParamsList, expectedParams, args, nil) +} + +// McmsCreateCurserCap encodes a call to the mcms_create_curser_cap Move function. +func (c rmnRemoteEncoder) McmsCreateCurserCap(ref bind.Object, registry bind.Object, params bind.Object) (*bind.EncodedCall, error) { + typeArgsList := []string{} + typeParamsList := []string{} + return c.EncodeCallArgsWithGenerics("mcms_create_curser_cap", typeArgsList, typeParamsList, []string{ + "&mut CCIPObjectRef", + "&mut Registry", + "ExecutingCallbackParams", + }, []any{ + ref, + registry, + params, + }, []string{ + "ccip::rmn_remote::CurserCap", + }) +} + +// McmsCreateCurserCapWithArgs encodes a call to the mcms_create_curser_cap Move function using arbitrary arguments. +// This method allows passing both regular values and transaction.Argument values for PTB chaining. +func (c rmnRemoteEncoder) McmsCreateCurserCapWithArgs(args ...any) (*bind.EncodedCall, error) { + expectedParams := []string{ + "&mut CCIPObjectRef", + "&mut Registry", + "ExecutingCallbackParams", + } + + if len(args) != len(expectedParams) { + return nil, fmt.Errorf("expected %d arguments, got %d", len(expectedParams), len(args)) + } + typeArgsList := []string{} + typeParamsList := []string{} + return c.EncodeCallArgsWithGenerics("mcms_create_curser_cap", typeArgsList, typeParamsList, expectedParams, args, []string{ + "ccip::rmn_remote::CurserCap", + }) +} + +// McmsRegisterCurserCap encodes a call to the mcms_register_curser_cap Move function. +func (c rmnRemoteEncoder) McmsRegisterCurserCap(ref bind.Object, slowRegistry bind.Object, fastRegistry bind.Object, params bind.Object, curserCap bind.Object) (*bind.EncodedCall, error) { + typeArgsList := []string{} + typeParamsList := []string{} + return c.EncodeCallArgsWithGenerics("mcms_register_curser_cap", typeArgsList, typeParamsList, []string{ + "&mut CCIPObjectRef", + "&mut Registry", + "&mut Registry", + "ExecutingCallbackParams", + "ccip::rmn_remote::CurserCap", + }, []any{ + ref, + slowRegistry, + fastRegistry, + params, + curserCap, + }, nil) +} + +// McmsRegisterCurserCapWithArgs encodes a call to the mcms_register_curser_cap Move function using arbitrary arguments. +// This method allows passing both regular values and transaction.Argument values for PTB chaining. +func (c rmnRemoteEncoder) McmsRegisterCurserCapWithArgs(args ...any) (*bind.EncodedCall, error) { + expectedParams := []string{ + "&mut CCIPObjectRef", + "&mut Registry", + "&mut Registry", + "ExecutingCallbackParams", + "ccip::rmn_remote::CurserCap", + } + + if len(args) != len(expectedParams) { + return nil, fmt.Errorf("expected %d arguments, got %d", len(expectedParams), len(args)) + } + typeArgsList := []string{} + typeParamsList := []string{} + return c.EncodeCallArgsWithGenerics("mcms_register_curser_cap", typeArgsList, typeParamsList, expectedParams, args, nil) +} + +// McmsMintAndRegisterCurserCap encodes a call to the mcms_mint_and_register_curser_cap Move function. +func (c rmnRemoteEncoder) McmsMintAndRegisterCurserCap(ref bind.Object, slowRegistry bind.Object, fastRegistry bind.Object, params bind.Object) (*bind.EncodedCall, error) { + typeArgsList := []string{} + typeParamsList := []string{} + return c.EncodeCallArgsWithGenerics("mcms_mint_and_register_curser_cap", typeArgsList, typeParamsList, []string{ + "&mut CCIPObjectRef", + "&mut Registry", + "&mut Registry", + "ExecutingCallbackParams", + }, []any{ + ref, + slowRegistry, + fastRegistry, + params, + }, nil) +} + +// McmsMintAndRegisterCurserCapWithArgs encodes a call to the mcms_mint_and_register_curser_cap Move function using arbitrary arguments. +// This method allows passing both regular values and transaction.Argument values for PTB chaining. +func (c rmnRemoteEncoder) McmsMintAndRegisterCurserCapWithArgs(args ...any) (*bind.EncodedCall, error) { + expectedParams := []string{ + "&mut CCIPObjectRef", + "&mut Registry", + "&mut Registry", + "ExecutingCallbackParams", + } + + if len(args) != len(expectedParams) { + return nil, fmt.Errorf("expected %d arguments, got %d", len(expectedParams), len(args)) + } + typeArgsList := []string{} + typeParamsList := []string{} + return c.EncodeCallArgsWithGenerics("mcms_mint_and_register_curser_cap", typeArgsList, typeParamsList, expectedParams, args, nil) +} diff --git a/bindings/generated/ccip/ccip_offramp/offramp/offramp.go b/bindings/generated/ccip/ccip_offramp/offramp/offramp.go index 4eb23965a..c44d27d4f 100644 --- a/bindings/generated/ccip/ccip_offramp/offramp/offramp.go +++ b/bindings/generated/ccip/ccip_offramp/offramp/offramp.go @@ -19,7 +19,7 @@ var ( _ = big.NewInt ) -const FunctionInfo = `[{"package":"ccip_offramp","module":"offramp","name":"accept_ownership","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"state","type":"OffRampState"}]},{"package":"ccip_offramp","module":"offramp","name":"accept_ownership_from_object","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"state","type":"OffRampState"},{"name":"from","type":"sui::object::UID"}]},{"package":"ccip_offramp","module":"offramp","name":"add_package_id","parameters":[{"name":"state","type":"OffRampState"},{"name":"owner_cap","type":"OwnerCap"},{"name":"package_id","type":"address"}]},{"package":"ccip_offramp","module":"offramp","name":"apply_source_chain_config_updates","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"state","type":"OffRampState"},{"name":"owner_cap","type":"OwnerCap"},{"name":"source_chains_selector","type":"vector"},{"name":"source_chains_is_enabled","type":"vector"},{"name":"source_chains_is_rmn_verification_disabled","type":"vector"},{"name":"source_chains_on_ramp","type":"vector>"}]},{"package":"ccip_offramp","module":"offramp","name":"calculate_message_hash","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"message_id","type":"vector"},{"name":"source_chain_selector","type":"u64"},{"name":"dest_chain_selector","type":"u64"},{"name":"sequence_number","type":"u64"},{"name":"nonce","type":"u64"},{"name":"sender","type":"vector"},{"name":"receiver","type":"address"},{"name":"on_ramp","type":"vector"},{"name":"data","type":"vector"},{"name":"gas_limit","type":"u256"},{"name":"token_receiver","type":"address"},{"name":"source_pool_addresses","type":"vector>"},{"name":"dest_token_addresses","type":"vector
"},{"name":"dest_gas_amounts","type":"vector"},{"name":"extra_datas","type":"vector>"},{"name":"amounts","type":"vector"}]},{"package":"ccip_offramp","module":"offramp","name":"calculate_metadata_hash","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"source_chain_selector","type":"u64"},{"name":"dest_chain_selector","type":"u64"},{"name":"on_ramp","type":"vector"}]},{"package":"ccip_offramp","module":"offramp","name":"commit","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"state","type":"OffRampState"},{"name":"clock","type":"clock::Clock"},{"name":"report_context","type":"vector>"},{"name":"report","type":"vector"},{"name":"signatures","type":"vector>"}]},{"package":"ccip_offramp","module":"offramp","name":"config_signers","parameters":[{"name":"state","type":"OCRConfig"}]},{"package":"ccip_offramp","module":"offramp","name":"config_transmitters","parameters":[{"name":"state","type":"OCRConfig"}]},{"package":"ccip_offramp","module":"offramp","name":"execute_ownership_transfer","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"owner_cap","type":"OwnerCap"},{"name":"state","type":"OffRampState"},{"name":"to","type":"address"}]},{"package":"ccip_offramp","module":"offramp","name":"execute_ownership_transfer_to_mcms","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"owner_cap","type":"OwnerCap"},{"name":"state","type":"OffRampState"},{"name":"registry","type":"Registry"},{"name":"to","type":"address"}]},{"package":"ccip_offramp","module":"offramp","name":"finish_execute","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"state","type":"OffRampState"},{"name":"receiver_params","type":"osh::ReceiverParams"}]},{"package":"ccip_offramp","module":"offramp","name":"get_all_source_chain_configs","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"state","type":"OffRampState"}]},{"package":"ccip_offramp","module":"offramp","name":"get_ccip_package_id","parameters":null},{"package":"ccip_offramp","module":"offramp","name":"get_dynamic_config","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"state","type":"OffRampState"}]},{"package":"ccip_offramp","module":"offramp","name":"get_dynamic_config_fields","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"cfg","type":"DynamicConfig"}]},{"package":"ccip_offramp","module":"offramp","name":"get_execution_state","parameters":[{"name":"state","type":"OffRampState"},{"name":"source_chain_selector","type":"u64"},{"name":"sequence_number","type":"u64"}]},{"package":"ccip_offramp","module":"offramp","name":"get_merkle_root","parameters":[{"name":"state","type":"OffRampState"},{"name":"root","type":"vector"}]},{"package":"ccip_offramp","module":"offramp","name":"get_ocr3_base","parameters":[{"name":"state","type":"OffRampState"}]},{"package":"ccip_offramp","module":"offramp","name":"get_source_chain_config","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"state","type":"OffRampState"},{"name":"source_chain_selector","type":"u64"}]},{"package":"ccip_offramp","module":"offramp","name":"get_source_chain_config_fields","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"source_chain_config","type":"SourceChainConfig"}]},{"package":"ccip_offramp","module":"offramp","name":"get_static_config","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"state","type":"OffRampState"}]},{"package":"ccip_offramp","module":"offramp","name":"get_static_config_fields","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"cfg","type":"StaticConfig"}]},{"package":"ccip_offramp","module":"offramp","name":"has_pending_transfer","parameters":[{"name":"state","type":"OffRampState"}]},{"package":"ccip_offramp","module":"offramp","name":"init_execute","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"state","type":"OffRampState"},{"name":"clock","type":"clock::Clock"},{"name":"report_context","type":"vector>"},{"name":"report","type":"vector"}]},{"package":"ccip_offramp","module":"offramp","name":"initialize","parameters":[{"name":"state","type":"OffRampState"},{"name":"owner_cap","type":"OwnerCap"},{"name":"fee_quoter_cap","type":"FeeQuoterCap"},{"name":"dest_transfer_cap","type":"osh::DestTransferCap"},{"name":"chain_selector","type":"u64"},{"name":"permissionless_execution_threshold_seconds","type":"u32"},{"name":"source_chains_selectors","type":"vector"},{"name":"source_chains_is_enabled","type":"vector"},{"name":"source_chains_is_rmn_verification_disabled","type":"vector"},{"name":"source_chains_on_ramp","type":"vector>"}]},{"package":"ccip_offramp","module":"offramp","name":"manually_init_execute","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"state","type":"OffRampState"},{"name":"clock","type":"clock::Clock"},{"name":"report_bytes","type":"vector"}]},{"package":"ccip_offramp","module":"offramp","name":"owner","parameters":[{"name":"state","type":"OffRampState"}]},{"package":"ccip_offramp","module":"offramp","name":"pending_transfer_accepted","parameters":[{"name":"state","type":"OffRampState"}]},{"package":"ccip_offramp","module":"offramp","name":"pending_transfer_from","parameters":[{"name":"state","type":"OffRampState"}]},{"package":"ccip_offramp","module":"offramp","name":"pending_transfer_to","parameters":[{"name":"state","type":"OffRampState"}]},{"package":"ccip_offramp","module":"offramp","name":"remove_package_id","parameters":[{"name":"state","type":"OffRampState"},{"name":"owner_cap","type":"OwnerCap"},{"name":"package_id","type":"address"}]},{"package":"ccip_offramp","module":"offramp","name":"set_dynamic_config","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"state","type":"OffRampState"},{"name":"owner_cap","type":"OwnerCap"},{"name":"permissionless_execution_threshold_seconds","type":"u32"}]},{"package":"ccip_offramp","module":"offramp","name":"set_ocr3_config","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"state","type":"OffRampState"},{"name":"owner_cap","type":"OwnerCap"},{"name":"config_digest","type":"vector"},{"name":"ocr_plugin_type","type":"u8"},{"name":"big_f","type":"u8"},{"name":"is_signature_verification_enabled","type":"bool"},{"name":"signers","type":"vector>"},{"name":"transmitters","type":"vector
"}]},{"package":"ccip_offramp","module":"offramp","name":"transfer_ownership","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"state","type":"OffRampState"},{"name":"owner_cap","type":"OwnerCap"},{"name":"new_owner","type":"address"}]},{"package":"ccip_offramp","module":"offramp","name":"type_and_version","parameters":null}]` +const FunctionInfo = `[{"package":"ccip_offramp","module":"offramp","name":"accept_ownership","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"state","type":"OffRampState"}]},{"package":"ccip_offramp","module":"offramp","name":"accept_ownership_from_object","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"state","type":"OffRampState"},{"name":"from","type":"sui::object::UID"}]},{"package":"ccip_offramp","module":"offramp","name":"add_package_id","parameters":[{"name":"state","type":"OffRampState"},{"name":"owner_cap","type":"OwnerCap"},{"name":"package_id","type":"address"}]},{"package":"ccip_offramp","module":"offramp","name":"apply_source_chain_config_updates","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"state","type":"OffRampState"},{"name":"owner_cap","type":"OwnerCap"},{"name":"source_chains_selector","type":"vector"},{"name":"source_chains_is_enabled","type":"vector"},{"name":"source_chains_is_rmn_verification_disabled","type":"vector"},{"name":"source_chains_on_ramp","type":"vector>"}]},{"package":"ccip_offramp","module":"offramp","name":"calculate_message_hash","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"message_id","type":"vector"},{"name":"source_chain_selector","type":"u64"},{"name":"dest_chain_selector","type":"u64"},{"name":"sequence_number","type":"u64"},{"name":"nonce","type":"u64"},{"name":"sender","type":"vector"},{"name":"receiver","type":"address"},{"name":"on_ramp","type":"vector"},{"name":"data","type":"vector"},{"name":"gas_limit","type":"u256"},{"name":"token_receiver","type":"address"},{"name":"source_pool_addresses","type":"vector>"},{"name":"dest_token_addresses","type":"vector
"},{"name":"dest_gas_amounts","type":"vector"},{"name":"extra_datas","type":"vector>"},{"name":"amounts","type":"vector"}]},{"package":"ccip_offramp","module":"offramp","name":"calculate_metadata_hash","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"source_chain_selector","type":"u64"},{"name":"dest_chain_selector","type":"u64"},{"name":"on_ramp","type":"vector"}]},{"package":"ccip_offramp","module":"offramp","name":"commit","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"state","type":"OffRampState"},{"name":"clock","type":"clock::Clock"},{"name":"report_context","type":"vector>"},{"name":"report","type":"vector"},{"name":"signatures","type":"vector>"}]},{"package":"ccip_offramp","module":"offramp","name":"config_signers","parameters":[{"name":"state","type":"OCRConfig"}]},{"package":"ccip_offramp","module":"offramp","name":"config_transmitters","parameters":[{"name":"state","type":"OCRConfig"}]},{"package":"ccip_offramp","module":"offramp","name":"execute_ownership_transfer","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"owner_cap","type":"OwnerCap"},{"name":"state","type":"OffRampState"},{"name":"to","type":"address"}]},{"package":"ccip_offramp","module":"offramp","name":"execute_ownership_transfer_to_mcms","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"owner_cap","type":"OwnerCap"},{"name":"state","type":"OffRampState"},{"name":"registry","type":"Registry"},{"name":"to","type":"address"}]},{"package":"ccip_offramp","module":"offramp","name":"finish_execute","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"state","type":"OffRampState"},{"name":"receiver_params","type":"osh::ReceiverParams"}]},{"package":"ccip_offramp","module":"offramp","name":"finish_execute_v2","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"state","type":"OffRampState"},{"name":"receiver_params","type":"osh::ReceiverParamsV2"}]},{"package":"ccip_offramp","module":"offramp","name":"get_all_source_chain_configs","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"state","type":"OffRampState"}]},{"package":"ccip_offramp","module":"offramp","name":"get_ccip_package_id","parameters":null},{"package":"ccip_offramp","module":"offramp","name":"get_dynamic_config","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"state","type":"OffRampState"}]},{"package":"ccip_offramp","module":"offramp","name":"get_dynamic_config_fields","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"cfg","type":"DynamicConfig"}]},{"package":"ccip_offramp","module":"offramp","name":"get_execution_state","parameters":[{"name":"state","type":"OffRampState"},{"name":"source_chain_selector","type":"u64"},{"name":"sequence_number","type":"u64"}]},{"package":"ccip_offramp","module":"offramp","name":"get_merkle_root","parameters":[{"name":"state","type":"OffRampState"},{"name":"root","type":"vector"}]},{"package":"ccip_offramp","module":"offramp","name":"get_ocr3_base","parameters":[{"name":"state","type":"OffRampState"}]},{"package":"ccip_offramp","module":"offramp","name":"get_source_chain_config","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"state","type":"OffRampState"},{"name":"source_chain_selector","type":"u64"}]},{"package":"ccip_offramp","module":"offramp","name":"get_source_chain_config_fields","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"source_chain_config","type":"SourceChainConfig"}]},{"package":"ccip_offramp","module":"offramp","name":"get_static_config","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"state","type":"OffRampState"}]},{"package":"ccip_offramp","module":"offramp","name":"get_static_config_fields","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"cfg","type":"StaticConfig"}]},{"package":"ccip_offramp","module":"offramp","name":"has_pending_transfer","parameters":[{"name":"state","type":"OffRampState"}]},{"package":"ccip_offramp","module":"offramp","name":"init_execute","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"state","type":"OffRampState"},{"name":"clock","type":"clock::Clock"},{"name":"report_context","type":"vector>"},{"name":"report","type":"vector"}]},{"package":"ccip_offramp","module":"offramp","name":"init_execute_v2","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"state","type":"OffRampState"},{"name":"clock","type":"clock::Clock"},{"name":"report_context","type":"vector>"},{"name":"report","type":"vector"}]},{"package":"ccip_offramp","module":"offramp","name":"initialize","parameters":[{"name":"state","type":"OffRampState"},{"name":"owner_cap","type":"OwnerCap"},{"name":"fee_quoter_cap","type":"FeeQuoterCap"},{"name":"dest_transfer_cap","type":"osh::DestTransferCap"},{"name":"chain_selector","type":"u64"},{"name":"permissionless_execution_threshold_seconds","type":"u32"},{"name":"source_chains_selectors","type":"vector"},{"name":"source_chains_is_enabled","type":"vector"},{"name":"source_chains_is_rmn_verification_disabled","type":"vector"},{"name":"source_chains_on_ramp","type":"vector>"}]},{"package":"ccip_offramp","module":"offramp","name":"manually_init_execute","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"state","type":"OffRampState"},{"name":"clock","type":"clock::Clock"},{"name":"report_bytes","type":"vector"}]},{"package":"ccip_offramp","module":"offramp","name":"manually_init_execute_v2","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"state","type":"OffRampState"},{"name":"clock","type":"clock::Clock"},{"name":"report_bytes","type":"vector"}]},{"package":"ccip_offramp","module":"offramp","name":"owner","parameters":[{"name":"state","type":"OffRampState"}]},{"package":"ccip_offramp","module":"offramp","name":"pending_transfer_accepted","parameters":[{"name":"state","type":"OffRampState"}]},{"package":"ccip_offramp","module":"offramp","name":"pending_transfer_from","parameters":[{"name":"state","type":"OffRampState"}]},{"package":"ccip_offramp","module":"offramp","name":"pending_transfer_to","parameters":[{"name":"state","type":"OffRampState"}]},{"package":"ccip_offramp","module":"offramp","name":"remove_package_id","parameters":[{"name":"state","type":"OffRampState"},{"name":"owner_cap","type":"OwnerCap"},{"name":"package_id","type":"address"}]},{"package":"ccip_offramp","module":"offramp","name":"set_dynamic_config","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"state","type":"OffRampState"},{"name":"owner_cap","type":"OwnerCap"},{"name":"permissionless_execution_threshold_seconds","type":"u32"}]},{"package":"ccip_offramp","module":"offramp","name":"set_ocr3_config","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"state","type":"OffRampState"},{"name":"owner_cap","type":"OwnerCap"},{"name":"config_digest","type":"vector"},{"name":"ocr_plugin_type","type":"u8"},{"name":"big_f","type":"u8"},{"name":"is_signature_verification_enabled","type":"bool"},{"name":"signers","type":"vector>"},{"name":"transmitters","type":"vector
"}]},{"package":"ccip_offramp","module":"offramp","name":"transfer_ownership","parameters":[{"name":"ref","type":"CCIPObjectRef"},{"name":"state","type":"OffRampState"},{"name":"owner_cap","type":"OwnerCap"},{"name":"new_owner","type":"address"}]},{"package":"ccip_offramp","module":"offramp","name":"type_and_version","parameters":null}]` type IOfframp interface { TypeAndVersion(ctx context.Context, opts *bind.CallOpts) (*models.SuiTransactionBlockResponse, error) @@ -69,6 +69,9 @@ type IOfframp interface { McmsExecuteOwnershipTransfer(ctx context.Context, opts *bind.CallOpts, ref bind.Object, state bind.Object, registry bind.Object, deployerState bind.Object, params bind.Object) (*models.SuiTransactionBlockResponse, error) McmsAddAllowedModules(ctx context.Context, opts *bind.CallOpts, registry bind.Object, params bind.Object) (*models.SuiTransactionBlockResponse, error) McmsRemoveAllowedModules(ctx context.Context, opts *bind.CallOpts, registry bind.Object, params bind.Object) (*models.SuiTransactionBlockResponse, error) + InitExecuteV2(ctx context.Context, opts *bind.CallOpts, ref bind.Object, state bind.Object, clock bind.Object, reportContext [][]byte, report []byte) (*models.SuiTransactionBlockResponse, error) + ManuallyInitExecuteV2(ctx context.Context, opts *bind.CallOpts, ref bind.Object, state bind.Object, clock bind.Object, reportBytes []byte) (*models.SuiTransactionBlockResponse, error) + FinishExecuteV2(ctx context.Context, opts *bind.CallOpts, ref bind.Object, state bind.Object, receiverParams bind.Object) (*models.SuiTransactionBlockResponse, error) DevInspect() IOfframpDevInspect Encoder() OfframpEncoder Bound() bind.IBoundContract @@ -98,6 +101,8 @@ type IOfframpDevInspect interface { PendingTransferFrom(ctx context.Context, opts *bind.CallOpts, state bind.Object) (*string, error) PendingTransferTo(ctx context.Context, opts *bind.CallOpts, state bind.Object) (*string, error) PendingTransferAccepted(ctx context.Context, opts *bind.CallOpts, state bind.Object) (*bool, error) + InitExecuteV2(ctx context.Context, opts *bind.CallOpts, ref bind.Object, state bind.Object, clock bind.Object, reportContext [][]byte, report []byte) (bind.Object, error) + ManuallyInitExecuteV2(ctx context.Context, opts *bind.CallOpts, ref bind.Object, state bind.Object, clock bind.Object, reportBytes []byte) (bind.Object, error) } type OfframpEncoder interface { @@ -195,6 +200,12 @@ type OfframpEncoder interface { McmsAddAllowedModulesWithArgs(args ...any) (*bind.EncodedCall, error) McmsRemoveAllowedModules(registry bind.Object, params bind.Object) (*bind.EncodedCall, error) McmsRemoveAllowedModulesWithArgs(args ...any) (*bind.EncodedCall, error) + InitExecuteV2(ref bind.Object, state bind.Object, clock bind.Object, reportContext [][]byte, report []byte) (*bind.EncodedCall, error) + InitExecuteV2WithArgs(args ...any) (*bind.EncodedCall, error) + ManuallyInitExecuteV2(ref bind.Object, state bind.Object, clock bind.Object, reportBytes []byte) (*bind.EncodedCall, error) + ManuallyInitExecuteV2WithArgs(args ...any) (*bind.EncodedCall, error) + FinishExecuteV2(ref bind.Object, state bind.Object, receiverParams bind.Object) (*bind.EncodedCall, error) + FinishExecuteV2WithArgs(args ...any) (*bind.EncodedCall, error) } type OfframpContract struct { @@ -388,6 +399,24 @@ type McmsCallback struct { type McmsAcceptOwnershipProof struct { } +type Any2SuiRampMessageV2 struct { + Header RampMessageHeader `move:"RampMessageHeader"` + Sender []byte `move:"vector"` + Data []byte `move:"vector"` + Receiver string `move:"address"` + GasLimit *big.Int `move:"u256"` + TokenReceiver string `move:"address"` + ReceiverObjectIds []string `move:"vector
"` + TokenAmounts []Any2SuiTokenTransfer `move:"vector"` +} + +type ExecutionReportV2 struct { + SourceChainSelector uint64 `move:"u64"` + Message Any2SuiRampMessageV2 `move:"Any2SuiRampMessageV2"` + OffchainTokenData [][]byte `move:"vector>"` + Proofs [][]byte `move:"vector>"` +} + type bcsOffRampState struct { Id string PackageIds [][32]byte @@ -626,6 +655,62 @@ func convertSourceChainConfigSetFromBCS(bcs bcsSourceChainConfigSet) (SourceChai }, nil } +type bcsAny2SuiRampMessageV2 struct { + Header RampMessageHeader + Sender []byte + Data []byte + Receiver [32]byte + GasLimit [32]byte + TokenReceiver [32]byte + ReceiverObjectIds [][32]byte + TokenAmounts []Any2SuiTokenTransfer +} + +func convertAny2SuiRampMessageV2FromBCS(bcs bcsAny2SuiRampMessageV2) (Any2SuiRampMessageV2, error) { + GasLimitField, err := bind.DecodeU256Value(bcs.GasLimit) + if err != nil { + return Any2SuiRampMessageV2{}, fmt.Errorf("failed to decode u256 field GasLimit: %w", err) + } + + return Any2SuiRampMessageV2{ + Header: bcs.Header, + Sender: bcs.Sender, + Data: bcs.Data, + Receiver: fmt.Sprintf("0x%x", bcs.Receiver), + GasLimit: GasLimitField, + TokenReceiver: fmt.Sprintf("0x%x", bcs.TokenReceiver), + ReceiverObjectIds: func() []string { + addrs := make([]string, len(bcs.ReceiverObjectIds)) + for i, addr := range bcs.ReceiverObjectIds { + addrs[i] = fmt.Sprintf("0x%x", addr) + } + return addrs + }(), + TokenAmounts: bcs.TokenAmounts, + }, nil +} + +type bcsExecutionReportV2 struct { + SourceChainSelector uint64 + Message bcsAny2SuiRampMessageV2 + OffchainTokenData [][]byte + Proofs [][]byte +} + +func convertExecutionReportV2FromBCS(bcs bcsExecutionReportV2) (ExecutionReportV2, error) { + MessageField, err := convertAny2SuiRampMessageV2FromBCS(bcs.Message) + if err != nil { + return ExecutionReportV2{}, fmt.Errorf("failed to convert nested struct Message: %w", err) + } + + return ExecutionReportV2{ + SourceChainSelector: bcs.SourceChainSelector, + Message: MessageField, + OffchainTokenData: bcs.OffchainTokenData, + Proofs: bcs.Proofs, + }, nil +} + func init() { bind.RegisterStructDecoder("ccip_offramp::offramp::OffRampState", func(data []byte) (interface{}, error) { var temp bcsOffRampState @@ -1220,6 +1305,68 @@ func init() { } return results, nil }) + bind.RegisterStructDecoder("ccip_offramp::offramp::Any2SuiRampMessageV2", func(data []byte) (interface{}, error) { + var temp bcsAny2SuiRampMessageV2 + _, err := mystenbcs.Unmarshal(data, &temp) + if err != nil { + return nil, err + } + + result, err := convertAny2SuiRampMessageV2FromBCS(temp) + if err != nil { + return nil, err + } + return result, nil + }) + // Register vector decoder for Any2SuiRampMessageV2 + bind.RegisterStructDecoder("vector", func(data []byte) (interface{}, error) { + var temps []bcsAny2SuiRampMessageV2 + _, err := mystenbcs.Unmarshal(data, &temps) + if err != nil { + return nil, err + } + + results := make([]Any2SuiRampMessageV2, len(temps)) + for i, temp := range temps { + result, err := convertAny2SuiRampMessageV2FromBCS(temp) + if err != nil { + return nil, fmt.Errorf("failed to convert element %d: %w", i, err) + } + results[i] = result + } + return results, nil + }) + bind.RegisterStructDecoder("ccip_offramp::offramp::ExecutionReportV2", func(data []byte) (interface{}, error) { + var temp bcsExecutionReportV2 + _, err := mystenbcs.Unmarshal(data, &temp) + if err != nil { + return nil, err + } + + result, err := convertExecutionReportV2FromBCS(temp) + if err != nil { + return nil, err + } + return result, nil + }) + // Register vector decoder for ExecutionReportV2 + bind.RegisterStructDecoder("vector", func(data []byte) (interface{}, error) { + var temps []bcsExecutionReportV2 + _, err := mystenbcs.Unmarshal(data, &temps) + if err != nil { + return nil, err + } + + results := make([]ExecutionReportV2, len(temps)) + for i, temp := range temps { + result, err := convertExecutionReportV2FromBCS(temp) + if err != nil { + return nil, fmt.Errorf("failed to convert element %d: %w", i, err) + } + results[i] = result + } + return results, nil + }) } // TypeAndVersion executes the type_and_version Move function. @@ -1692,6 +1839,36 @@ func (c *OfframpContract) McmsRemoveAllowedModules(ctx context.Context, opts *bi return c.ExecuteTransaction(ctx, opts, encoded) } +// InitExecuteV2 executes the init_execute_v2 Move function. +func (c *OfframpContract) InitExecuteV2(ctx context.Context, opts *bind.CallOpts, ref bind.Object, state bind.Object, clock bind.Object, reportContext [][]byte, report []byte) (*models.SuiTransactionBlockResponse, error) { + encoded, err := c.offrampEncoder.InitExecuteV2(ref, state, clock, reportContext, report) + if err != nil { + return nil, fmt.Errorf("failed to encode function call: %w", err) + } + + return c.ExecuteTransaction(ctx, opts, encoded) +} + +// ManuallyInitExecuteV2 executes the manually_init_execute_v2 Move function. +func (c *OfframpContract) ManuallyInitExecuteV2(ctx context.Context, opts *bind.CallOpts, ref bind.Object, state bind.Object, clock bind.Object, reportBytes []byte) (*models.SuiTransactionBlockResponse, error) { + encoded, err := c.offrampEncoder.ManuallyInitExecuteV2(ref, state, clock, reportBytes) + if err != nil { + return nil, fmt.Errorf("failed to encode function call: %w", err) + } + + return c.ExecuteTransaction(ctx, opts, encoded) +} + +// FinishExecuteV2 executes the finish_execute_v2 Move function. +func (c *OfframpContract) FinishExecuteV2(ctx context.Context, opts *bind.CallOpts, ref bind.Object, state bind.Object, receiverParams bind.Object) (*models.SuiTransactionBlockResponse, error) { + encoded, err := c.offrampEncoder.FinishExecuteV2(ref, state, receiverParams) + if err != nil { + return nil, fmt.Errorf("failed to encode function call: %w", err) + } + + return c.ExecuteTransaction(ctx, opts, encoded) +} + // TypeAndVersion executes the type_and_version Move function using DevInspect to get return values. // // Returns: 0x1::string::String @@ -2171,6 +2348,50 @@ func (d *OfframpDevInspect) PendingTransferAccepted(ctx context.Context, opts *b return result, nil } +// InitExecuteV2 executes the init_execute_v2 Move function using DevInspect to get return values. +// +// Returns: osh::ReceiverParamsV2 +func (d *OfframpDevInspect) InitExecuteV2(ctx context.Context, opts *bind.CallOpts, ref bind.Object, state bind.Object, clock bind.Object, reportContext [][]byte, report []byte) (bind.Object, error) { + encoded, err := d.contract.offrampEncoder.InitExecuteV2(ref, state, clock, reportContext, report) + if err != nil { + return bind.Object{}, fmt.Errorf("failed to encode function call: %w", err) + } + results, err := d.contract.Call(ctx, opts, encoded) + if err != nil { + return bind.Object{}, err + } + if len(results) == 0 { + return bind.Object{}, fmt.Errorf("no return value") + } + result, ok := results[0].(bind.Object) + if !ok { + return bind.Object{}, fmt.Errorf("unexpected return type: expected bind.Object, got %T", results[0]) + } + return result, nil +} + +// ManuallyInitExecuteV2 executes the manually_init_execute_v2 Move function using DevInspect to get return values. +// +// Returns: osh::ReceiverParamsV2 +func (d *OfframpDevInspect) ManuallyInitExecuteV2(ctx context.Context, opts *bind.CallOpts, ref bind.Object, state bind.Object, clock bind.Object, reportBytes []byte) (bind.Object, error) { + encoded, err := d.contract.offrampEncoder.ManuallyInitExecuteV2(ref, state, clock, reportBytes) + if err != nil { + return bind.Object{}, fmt.Errorf("failed to encode function call: %w", err) + } + results, err := d.contract.Call(ctx, opts, encoded) + if err != nil { + return bind.Object{}, err + } + if len(results) == 0 { + return bind.Object{}, fmt.Errorf("no return value") + } + result, ok := results[0].(bind.Object) + if !ok { + return bind.Object{}, fmt.Errorf("unexpected return type: expected bind.Object, got %T", results[0]) + } + return result, nil +} + type offrampEncoder struct { *bind.BoundContract } @@ -3839,3 +4060,116 @@ func (c offrampEncoder) McmsRemoveAllowedModulesWithArgs(args ...any) (*bind.Enc typeParamsList := []string{} return c.EncodeCallArgsWithGenerics("mcms_remove_allowed_modules", typeArgsList, typeParamsList, expectedParams, args, nil) } + +// InitExecuteV2 encodes a call to the init_execute_v2 Move function. +func (c offrampEncoder) InitExecuteV2(ref bind.Object, state bind.Object, clock bind.Object, reportContext [][]byte, report []byte) (*bind.EncodedCall, error) { + typeArgsList := []string{} + typeParamsList := []string{} + return c.EncodeCallArgsWithGenerics("init_execute_v2", typeArgsList, typeParamsList, []string{ + "&CCIPObjectRef", + "&mut OffRampState", + "&clock::Clock", + "vector>", + "vector", + }, []any{ + ref, + state, + clock, + reportContext, + report, + }, []string{ + "osh::ReceiverParamsV2", + }) +} + +// InitExecuteV2WithArgs encodes a call to the init_execute_v2 Move function using arbitrary arguments. +// This method allows passing both regular values and transaction.Argument values for PTB chaining. +func (c offrampEncoder) InitExecuteV2WithArgs(args ...any) (*bind.EncodedCall, error) { + expectedParams := []string{ + "&CCIPObjectRef", + "&mut OffRampState", + "&clock::Clock", + "vector>", + "vector", + } + + if len(args) != len(expectedParams) { + return nil, fmt.Errorf("expected %d arguments, got %d", len(expectedParams), len(args)) + } + typeArgsList := []string{} + typeParamsList := []string{} + return c.EncodeCallArgsWithGenerics("init_execute_v2", typeArgsList, typeParamsList, expectedParams, args, []string{ + "osh::ReceiverParamsV2", + }) +} + +// ManuallyInitExecuteV2 encodes a call to the manually_init_execute_v2 Move function. +func (c offrampEncoder) ManuallyInitExecuteV2(ref bind.Object, state bind.Object, clock bind.Object, reportBytes []byte) (*bind.EncodedCall, error) { + typeArgsList := []string{} + typeParamsList := []string{} + return c.EncodeCallArgsWithGenerics("manually_init_execute_v2", typeArgsList, typeParamsList, []string{ + "&CCIPObjectRef", + "&mut OffRampState", + "&clock::Clock", + "vector", + }, []any{ + ref, + state, + clock, + reportBytes, + }, []string{ + "osh::ReceiverParamsV2", + }) +} + +// ManuallyInitExecuteV2WithArgs encodes a call to the manually_init_execute_v2 Move function using arbitrary arguments. +// This method allows passing both regular values and transaction.Argument values for PTB chaining. +func (c offrampEncoder) ManuallyInitExecuteV2WithArgs(args ...any) (*bind.EncodedCall, error) { + expectedParams := []string{ + "&CCIPObjectRef", + "&mut OffRampState", + "&clock::Clock", + "vector", + } + + if len(args) != len(expectedParams) { + return nil, fmt.Errorf("expected %d arguments, got %d", len(expectedParams), len(args)) + } + typeArgsList := []string{} + typeParamsList := []string{} + return c.EncodeCallArgsWithGenerics("manually_init_execute_v2", typeArgsList, typeParamsList, expectedParams, args, []string{ + "osh::ReceiverParamsV2", + }) +} + +// FinishExecuteV2 encodes a call to the finish_execute_v2 Move function. +func (c offrampEncoder) FinishExecuteV2(ref bind.Object, state bind.Object, receiverParams bind.Object) (*bind.EncodedCall, error) { + typeArgsList := []string{} + typeParamsList := []string{} + return c.EncodeCallArgsWithGenerics("finish_execute_v2", typeArgsList, typeParamsList, []string{ + "&CCIPObjectRef", + "&mut OffRampState", + "osh::ReceiverParamsV2", + }, []any{ + ref, + state, + receiverParams, + }, nil) +} + +// FinishExecuteV2WithArgs encodes a call to the finish_execute_v2 Move function using arbitrary arguments. +// This method allows passing both regular values and transaction.Argument values for PTB chaining. +func (c offrampEncoder) FinishExecuteV2WithArgs(args ...any) (*bind.EncodedCall, error) { + expectedParams := []string{ + "&CCIPObjectRef", + "&mut OffRampState", + "osh::ReceiverParamsV2", + } + + if len(args) != len(expectedParams) { + return nil, fmt.Errorf("expected %d arguments, got %d", len(expectedParams), len(args)) + } + typeArgsList := []string{} + typeParamsList := []string{} + return c.EncodeCallArgsWithGenerics("finish_execute_v2", typeArgsList, typeParamsList, expectedParams, args, nil) +} diff --git a/contracts/ccip/ccip/sources/client.move b/contracts/ccip/ccip/sources/client.move index 6fe3c3648..323bec760 100644 --- a/contracts/ccip/ccip/sources/client.move +++ b/contracts/ccip/ccip/sources/client.move @@ -163,3 +163,77 @@ public fun get_amount(input: &Any2SuiTokenAmount): u256 { public fun get_token_and_amount(input: &Any2SuiTokenAmount): (address, u256) { (input.token, input.amount) } + +// ================================================================ +// | V2 Types | +// ================================================================ + +public struct Any2SuiMessageV2 { + message_id: vector, + source_chain_selector: u64, + sender: vector, + data: vector, + message_receiver: address, + token_receiver: address, + receiver_object_ids: vector
, + dest_token_amounts: vector, +} + +public(package) fun new_any2sui_message_v2( + message_id: vector, + source_chain_selector: u64, + sender: vector, + data: vector, + message_receiver: address, + token_receiver: address, + receiver_object_ids: vector
, + dest_token_amounts: vector, +): Any2SuiMessageV2 { + Any2SuiMessageV2 { + message_id, + source_chain_selector, + sender, + data, + message_receiver, + token_receiver, + receiver_object_ids, + dest_token_amounts, + } +} + +public(package) fun consume_any2sui_message_v2( + message: Any2SuiMessageV2, + receiver_package_id: address, +): (vector, u64, vector, vector, address, address, vector
, vector) { + let Any2SuiMessageV2 { + message_id, + source_chain_selector, + sender, + data, + message_receiver, + token_receiver, + receiver_object_ids, + dest_token_amounts, + } = message; + assert!(message_receiver == receiver_package_id, EMessageReceiverMismatch); + + ( + message_id, + source_chain_selector, + sender, + data, + message_receiver, + token_receiver, + receiver_object_ids, + dest_token_amounts, + ) +} + +public fun get_receiver_object_ids(message: &Any2SuiMessageV2): &vector
{ + &message.receiver_object_ids +} + +public fun assert_receiver_object(message: &Any2SuiMessageV2, index: u64, object: &T) { + let expected_id = message.receiver_object_ids[index]; + assert!(object::id_address(object) == expected_id, EMessageReceiverMismatch); +} diff --git a/contracts/ccip/ccip/sources/offramp_state_helper.move b/contracts/ccip/ccip/sources/offramp_state_helper.move index dbd2ac86e..dc5eb0ad2 100644 --- a/contracts/ccip/ccip/sources/offramp_state_helper.move +++ b/contracts/ccip/ccip/sources/offramp_state_helper.move @@ -277,6 +277,196 @@ public fun deconstruct_receiver_params(_: &DestTransferCap, receiver_params: Rec message_op.destroy_none(); } +// ================================================================ +// | V2 Types and Functions | +// ================================================================ + +const EReceiverObjectMismatch: u64 = 11; + +public struct ReceiverParamsV2 { + token_transfer: Option, + message: Option, + source_chain_selector: u64, + receipt: Option, +} + +public fun new_any2sui_message_v2( + _: &DestTransferCap, + message_id: vector, + source_chain_selector: u64, + sender: vector, + data: vector, + message_receiver: address, + token_receiver: address, + receiver_object_ids: vector
, + token_addresses: vector
, + token_amounts: vector, +): client::Any2SuiMessageV2 { + client::new_any2sui_message_v2( + message_id, + source_chain_selector, + sender, + data, + message_receiver, + token_receiver, + receiver_object_ids, + client::new_dest_token_amounts(token_addresses, token_amounts), + ) +} + +public fun create_receiver_params_v2(_: &DestTransferCap, source_chain_selector: u64): ReceiverParamsV2 { + ReceiverParamsV2 { + token_transfer: option::none(), + message: option::none(), + source_chain_selector, + receipt: option::none(), + } +} + +public fun add_dest_token_transfer_v2( + _: &DestTransferCap, + receiver_params: &mut ReceiverParamsV2, + token_receiver: address, + remote_chain_selector: u64, + source_amount: u256, + dest_token_address: address, + dest_token_pool_package_id: address, + source_pool_address: vector, + source_pool_data: vector, + offchain_data: vector, +) { + assert!(receiver_params.token_transfer.is_none(), ETokenTransferAlreadyExists); + + receiver_params + .token_transfer + .fill(DestTokenTransfer { + token_receiver, + remote_chain_selector, + source_amount, + dest_token_address, + dest_token_pool_package_id, + source_pool_address, + source_pool_data, + offchain_token_data: offchain_data, + }); +} + +public fun populate_message_v2( + _: &DestTransferCap, + receiver_params: &mut ReceiverParamsV2, + any2sui_message: client::Any2SuiMessageV2, +) { + assert!(receiver_params.message.is_none(), EMessageAlreadyExists); + receiver_params.message.fill(any2sui_message); +} + +/// Extracts the V2 message from ReceiverParams. Enforces that the declared `used_object_ids` +/// match the source-committed `receiver_object_ids` in the message. This is the protocol-level +/// object binding enforcement point. +public fun extract_any2sui_message_v2( + receiver_params: &mut ReceiverParamsV2, + used_object_ids: vector
, +): client::Any2SuiMessageV2 { + assert!(receiver_params.message.is_some(), ENoMessageToExtract); + let message = receiver_params.message.extract(); + assert!(used_object_ids == *client::get_receiver_object_ids(&message), EReceiverObjectMismatch); + message +} + +public fun consume_any2sui_message_v2( + ref: &CCIPObjectRef, + message: client::Any2SuiMessageV2, + _: TypeProof, +): (vector, u64, vector, vector, address, address, vector
, vector) { + let proof_tn = type_name::with_defining_ids(); + let address_str = type_name::address_string(&proof_tn); + let receiver_package_id = address::from_ascii_bytes(&ascii::into_bytes(address_str)); + + let receiver_config = receiver_registry::get_receiver_config(ref, receiver_package_id); + let (_, proof_typename) = receiver_registry::get_receiver_config_fields(receiver_config); + assert!(proof_typename == proof_tn.into_string(), ETypeProofMismatch); + + client::consume_any2sui_message_v2(message, receiver_package_id) +} + +public fun get_dest_token_transfer_data_v2( + receiver_params: &ReceiverParamsV2, +): (address, u64, u256, address, address, vector, vector, vector) { + assert!(receiver_params.token_transfer.is_some(), ETokenTransferDoesNotExist); + + let token_transfer = receiver_params.token_transfer.borrow(); + ( + token_transfer.token_receiver, + token_transfer.remote_chain_selector, + token_transfer.source_amount, + token_transfer.dest_token_address, + token_transfer.dest_token_pool_package_id, + token_transfer.source_pool_address, + token_transfer.source_pool_data, + token_transfer.offchain_token_data, + ) +} + +public fun complete_token_transfer_v2( + ref: &CCIPObjectRef, + receiver_params: &mut ReceiverParamsV2, + _: TypeProof, +) { + let dest_token_transfer = receiver_params.token_transfer.borrow(); + let token_receiver = dest_token_transfer.token_receiver; + let dest_token_address = dest_token_transfer.dest_token_address; + let (_, _, _, _, _, type_proof, _, _) = registry::get_token_config_data( + ref, + dest_token_address, + ); + + let proof_tn = type_name::with_defining_ids(); + let proof_tn_str = type_name::into_string(proof_tn); + assert!(type_proof == proof_tn_str, ETypeProofMismatch); + + let receipt = CompletedDestTokenTransfer { + token_receiver, + dest_token_address, + }; + + assert!(receiver_params.receipt.is_none(), ETokenTransferAlreadyCompleted); + receiver_params.receipt.fill(receipt); +} + +public fun deconstruct_receiver_params_v2(_: &DestTransferCap, receiver_params: ReceiverParamsV2) { + let ReceiverParamsV2 { + token_transfer: mut token_transfer_op, + message: message_op, + source_chain_selector: _, + receipt: mut receipt_op, + } = receiver_params; + + assert!( + token_transfer_op.is_none() && receipt_op.is_none() || (token_transfer_op.is_some() && receipt_op.is_some()), + EWrongReceiptAndTokenTransfer, + ); + if (token_transfer_op.is_some()) { + let token_transfer = token_transfer_op.extract(); + let receipt = receipt_op.extract(); + let CompletedDestTokenTransfer { + token_receiver, + dest_token_address, + } = receipt; + + assert!( + token_receiver == token_transfer.token_receiver && + dest_token_address == token_transfer.dest_token_address, + ETokenTransferMismatch, + ); + }; + + token_transfer_op.destroy_none(); + receipt_op.destroy_none(); + + assert!(message_op.is_none(), ECCIPReceiveFailed); + message_op.destroy_none(); +} + // =========================== Test Functions =========================== // #[test_only] @@ -312,3 +502,31 @@ public fun deconstruct_receiver_params_with_message_for_test( message_op.destroy_none(); r.destroy_none(); } + +#[test_only] +public fun deconstruct_receiver_params_v2_with_message_for_test( + _: &DestTransferCap, + receiver_package_id: address, + receiver_params: ReceiverParamsV2, + message: client::Any2SuiMessageV2, +) { + let ReceiverParamsV2 { + token_transfer: _, + message: message_op, + source_chain_selector: _, + receipt: mut r, + } = receiver_params; + + if (r.is_some()) { + let completed_transfer = r.extract(); + let CompletedDestTokenTransfer { + token_receiver: _, + dest_token_address: _, + } = completed_transfer; + }; + + message_op.destroy_none(); + r.destroy_none(); + + client::consume_any2sui_message_v2(message, receiver_package_id); +} diff --git a/contracts/ccip/ccip/sources/rmn_remote.move b/contracts/ccip/ccip/sources/rmn_remote.move index 49e5a710a..01490ccdd 100644 --- a/contracts/ccip/ccip/sources/rmn_remote.move +++ b/contracts/ccip/ccip/sources/rmn_remote.move @@ -1,7 +1,7 @@ module ccip::rmn_remote; use ccip::eth_abi; -use ccip::ownable::OwnerCap; +use ccip::ownable::{Self, OwnerCap}; use ccip::state_object::{Self, CCIPObjectRef}; use ccip::upgrade_registry::verify_function_allowed; use mcms::bcs_stream; @@ -48,6 +48,15 @@ public struct Uncursed has copy, drop { subjects: vector>, } +/// Capability granting curse-only authority. Minted by the CCIP owner via +/// `create_curser_cap` and intended to be registered as the package cap in a +/// secondary MCMS instance whose only allowed module on `@ccip` is `rmn_remote`. +/// Possession of `&CurserCap` is the authority for `curse_with_curser_cap` and +/// `curse_multiple_with_curser_cap`; uncurse is not reachable through this cap. +public struct CurserCap has key, store { + id: UID, +} + const EAlreadyInitialized: u64 = 1; const EAlreadyCursed: u64 = 2; const EDuplicateSigner: u64 = 3; @@ -222,7 +231,13 @@ public fun curse_multiple( assert!(object::id(owner_cap) == state_object::owner_cap_id(ref), EInvalidOwnerCap); let state = state_object::borrow_mut(ref); + insert_cursed_subjects(state, subjects); +} +/// Shared state mutation for every curse path. Keeps OwnerCap-based and +/// CurserCap-based callers on identical semantics so the two auth paths cannot +/// drift apart on validation, insertion, or event emission. +fun insert_cursed_subjects(state: &mut RMNRemoteState, subjects: vector>) { subjects.do_ref!(|subject| { let subject: vector = *subject; assert!(subject.length() == 16, EInvalidSubjectLength); @@ -232,6 +247,58 @@ public fun curse_multiple( event::emit(Cursed { subjects }); } +/// Curse a single subject via possession of a `CurserCap`. Intended for use by +/// a secondary MCMS instance whose Registry holds a `CurserCap` for `@ccip`. +public fun curse_with_curser_cap( + ref: &mut CCIPObjectRef, + curser_cap: &CurserCap, + subject: vector, +) { + verify_function_allowed( + ref, + string::utf8(b"rmn_remote"), + string::utf8(b"curse_with_curser_cap"), + VERSION, + ); + curse_multiple_with_curser_cap(ref, curser_cap, vector[subject]); +} + +/// Curse multiple subjects via possession of a `CurserCap`. +public fun curse_multiple_with_curser_cap( + ref: &mut CCIPObjectRef, + _curser_cap: &CurserCap, + subjects: vector>, +) { + verify_function_allowed( + ref, + string::utf8(b"rmn_remote"), + string::utf8(b"curse_multiple_with_curser_cap"), + VERSION, + ); + + let state = state_object::borrow_mut(ref); + insert_cursed_subjects(state, subjects); +} + +/// Mint a new `CurserCap`. Owner-only. The fresh cap can be registered as the +/// package cap on a secondary MCMS Registry to enable curse-only governance +/// without granting that instance any other CCIP authority. +public fun create_curser_cap( + ref: &mut CCIPObjectRef, + owner_cap: &OwnerCap, + ctx: &mut TxContext, +): CurserCap { + verify_function_allowed( + ref, + string::utf8(b"rmn_remote"), + string::utf8(b"create_curser_cap"), + VERSION, + ); + assert!(object::id(owner_cap) == state_object::owner_cap_id(ref), EInvalidOwnerCap); + + CurserCap { id: object::new(ctx) } +} + public fun uncurse(ref: &mut CCIPObjectRef, owner_cap: &OwnerCap, subject: vector) { verify_function_allowed( ref, @@ -477,6 +544,234 @@ public fun mcms_uncurse_multiple( uncurse_multiple(ref, owner_cap, subjects); } +// ================================================================ +// | Fast Curse MCMS Entrypoints | +// ================================================================ +// +// These callbacks are intended to be invoked from a SECONDARY MCMS instance +// whose `Registry` holds a `CurserCap` (not `OwnerCap`) for `@ccip` with +// `allowed_modules = [b"rmn_remote"]`. They use `state_object::McmsCallback` +// as the proof type because the CCIP `Publisher` was claimed by the +// `STATE_OBJECT` OTW; `sui::package::Publisher::from_module` requires the +// proof type's defining module to match the Publisher's `module_name` field. +// The two MCMS instances are separate shared `Registry` objects, so reusing +// the proof type does not create a collision: the slow instance holds +// `(state_object::McmsCallback, OwnerCap)` for `@ccip` and the fast instance +// holds `(state_object::McmsCallback, CurserCap)` for `@ccip`. + +/// Fast-path callback. Releases the `CurserCap` from the fast Registry and +/// curses a single subject. The Registry's `allowed_modules` allowlist plus +/// the absence of any `uncurse`-bearing callback in this entrypoint family +/// constrains the fast MCMS to curse operations only. +public fun mcms_curse_with_curser_cap( + ref: &mut CCIPObjectRef, + registry: &mut Registry, + params: ExecutingCallbackParams, +) { + let (curser_cap, function, data) = mcms_registry::get_callback_params_with_caps< + state_object::McmsCallback, + CurserCap, + >( + registry, + state_object::mcms_callback(), + params, + ); + assert!(function == string::utf8(b"curse_with_curser_cap"), EInvalidFunction); + + let mut stream = bcs_stream::new(data); + bcs_stream::validate_obj_addrs( + vector[object::id_address(ref), object::id_address(curser_cap)], + &mut stream, + ); + + let subject = bcs_stream::deserialize_vector_u8(&mut stream); + bcs_stream::assert_is_consumed(&stream); + + curse_with_curser_cap(ref, curser_cap, subject); +} + +public fun mcms_curse_multiple_with_curser_cap( + ref: &mut CCIPObjectRef, + registry: &mut Registry, + params: ExecutingCallbackParams, +) { + let (curser_cap, function, data) = mcms_registry::get_callback_params_with_caps< + state_object::McmsCallback, + CurserCap, + >( + registry, + state_object::mcms_callback(), + params, + ); + assert!(function == string::utf8(b"curse_multiple_with_curser_cap"), EInvalidFunction); + + let mut stream = bcs_stream::new(data); + bcs_stream::validate_obj_addrs( + vector[object::id_address(ref), object::id_address(curser_cap)], + &mut stream, + ); + + let subjects = bcs_stream::deserialize_vector!( + &mut stream, + |stream| { bcs_stream::deserialize_vector_u8(stream) }, + ); + bcs_stream::assert_is_consumed(&stream); + + curse_multiple_with_curser_cap(ref, curser_cap, subjects); +} + +// ================================================================ +// | Slow-MCMS Bootstrap Wrappers for Fast Curse | +// ================================================================ +// +// These callbacks are invoked by the slow MCMS (which holds CCIP `OwnerCap`) +// to mint a `CurserCap` and register it in the fast MCMS Registry. The slow +// proposal can batch a mint op then a register op (`mcms_create_curser_cap` +// → `mcms_register_curser_cap`), or use the single combined op +// (`mcms_mint_and_register_curser_cap`). +// +// TODO: consolidate the two-op flow into the single combined flow once the +// PTB-chain ergonomics and external proposal tooling are agreed; the combined +// path is strictly safer because no transient `CurserCap` exists between txs. + +/// Slow-MCMS wrapper that mints a `CurserCap`. Returns the cap by value so a +/// PTB can chain it into `mcms_register_curser_cap` in the same batch. +public fun mcms_create_curser_cap( + ref: &mut CCIPObjectRef, + registry: &mut Registry, + params: ExecutingCallbackParams, + ctx: &mut TxContext, +): CurserCap { + let (owner_cap, function, data) = mcms_registry::get_callback_params_with_caps< + state_object::McmsCallback, + OwnerCap, + >( + registry, + state_object::mcms_callback(), + params, + ); + assert!(function == string::utf8(b"create_curser_cap"), EInvalidFunction); + + let mut stream = bcs_stream::new(data); + bcs_stream::validate_obj_addrs( + vector[object::id_address(ref), object::id_address(owner_cap)], + &mut stream, + ); + bcs_stream::assert_is_consumed(&stream); + + create_curser_cap(ref, owner_cap, ctx) +} + +/// Slow-MCMS wrapper that registers a `CurserCap` as the package cap on a +/// fast MCMS Registry with `allowed_modules = [b"rmn_remote"]`. Built from +/// the OwnerCap's stored `Publisher` so the proof type binding is rooted in +/// the same module that claimed the CCIP Publisher (`state_object`). +public fun mcms_register_curser_cap( + ref: &mut CCIPObjectRef, + slow_registry: &mut Registry, + fast_registry: &mut Registry, + params: ExecutingCallbackParams, + curser_cap: CurserCap, + ctx: &mut TxContext, +) { + let (owner_cap, function, data) = mcms_registry::get_callback_params_with_caps< + state_object::McmsCallback, + OwnerCap, + >( + slow_registry, + state_object::mcms_callback(), + params, + ); + assert!(function == string::utf8(b"register_curser_cap"), EInvalidFunction); + + let mut stream = bcs_stream::new(data); + bcs_stream::validate_obj_addrs( + vector[ + object::id_address(ref), + object::id_address(owner_cap), + object::id_address(fast_registry), + ], + &mut stream, + ); + bcs_stream::assert_is_consumed(&stream); + + verify_function_allowed( + ref, + string::utf8(b"rmn_remote"), + string::utf8(b"register_curser_cap"), + VERSION, + ); + + let publisher_wrapper = mcms_registry::create_publisher_wrapper( + ownable::borrow_publisher(owner_cap), + state_object::mcms_callback(), + ); + + mcms_registry::register_entrypoint( + fast_registry, + publisher_wrapper, + state_object::mcms_callback(), + curser_cap, + vector[b"rmn_remote"], + ctx, + ); +} + +/// Slow-MCMS wrapper that mints a `CurserCap` and atomically registers it in +/// the fast MCMS Registry. Preferred over the two-op variant: no PTB caller +/// can substitute a different `CurserCap` between mint and register. +public fun mcms_mint_and_register_curser_cap( + ref: &mut CCIPObjectRef, + slow_registry: &mut Registry, + fast_registry: &mut Registry, + params: ExecutingCallbackParams, + ctx: &mut TxContext, +) { + let (owner_cap, function, data) = mcms_registry::get_callback_params_with_caps< + state_object::McmsCallback, + OwnerCap, + >( + slow_registry, + state_object::mcms_callback(), + params, + ); + assert!(function == string::utf8(b"mint_and_register_curser_cap"), EInvalidFunction); + + let mut stream = bcs_stream::new(data); + bcs_stream::validate_obj_addrs( + vector[ + object::id_address(ref), + object::id_address(owner_cap), + object::id_address(fast_registry), + ], + &mut stream, + ); + bcs_stream::assert_is_consumed(&stream); + + verify_function_allowed( + ref, + string::utf8(b"rmn_remote"), + string::utf8(b"mint_and_register_curser_cap"), + VERSION, + ); + + let publisher_wrapper = mcms_registry::create_publisher_wrapper( + ownable::borrow_publisher(owner_cap), + state_object::mcms_callback(), + ); + + let curser_cap = create_curser_cap(ref, owner_cap, ctx); + + mcms_registry::register_entrypoint( + fast_registry, + publisher_wrapper, + state_object::mcms_callback(), + curser_cap, + vector[b"rmn_remote"], + ctx, + ); +} + #[test_only] public fun get_config(config: &Config): (vector, vector, u64) { (config.rmn_home_contract_config_digest, config.signers, config.f_sign) diff --git a/contracts/ccip/ccip/tests/offramp_state_helper_v2_tests.move b/contracts/ccip/ccip/tests/offramp_state_helper_v2_tests.move new file mode 100644 index 000000000..dbf7ebc5d --- /dev/null +++ b/contracts/ccip/ccip/tests/offramp_state_helper_v2_tests.move @@ -0,0 +1,231 @@ +#[test_only] +module ccip::offramp_state_helper_v2_tests; + +use ccip::client; +use ccip::offramp_state_helper::{Self as osh, DestTransferCap}; +use ccip::ownable::OwnerCap; +use ccip::receiver_registry; +use ccip::state_object::{Self, CCIPObjectRef}; +use ccip::upgrade_registry; +use sui::test_scenario::{Self as ts}; + +const OWNER: address = @0x1000; +const SOURCE_CHAIN: u64 = 2000; +const RECEIVER_PKG: address = @0xABCD; + +#[test] +public fun test_v2_extract_with_correct_object_ids() { + let mut sc = ts::begin(OWNER); + state_object::test_init(sc.ctx()); + osh::test_init(sc.ctx()); + sc.next_tx(OWNER); + + let owner_cap = ts::take_from_sender(&sc); + let mut ref_obj = ts::take_shared(&sc); + let dest_cap = ts::take_from_sender(&sc); + + upgrade_registry::initialize(&mut ref_obj, &owner_cap, sc.ctx()); + receiver_registry::initialize(&mut ref_obj, &owner_cap, sc.ctx()); + + let object_id_a = @0x1111; + let object_id_b = @0x2222; + + let mut receiver_params = osh::create_receiver_params_v2(&dest_cap, SOURCE_CHAIN); + + let msg = osh::new_any2sui_message_v2( + &dest_cap, + b"message_id_32_bytes_padding_ok!!", + SOURCE_CHAIN, + b"sender", + b"payload", + RECEIVER_PKG, + @0x0, + vector[object_id_a, object_id_b], + vector[], + vector[], + ); + + osh::populate_message_v2(&dest_cap, &mut receiver_params, msg); + + // Extract with CORRECT object IDs — should succeed + let extracted = osh::extract_any2sui_message_v2( + &mut receiver_params, + vector[object_id_a, object_id_b], + ); + + // Consume via test helper + osh::deconstruct_receiver_params_v2_with_message_for_test(&dest_cap, RECEIVER_PKG, receiver_params, extracted); + + ts::return_to_sender(&sc, owner_cap); + ts::return_to_sender(&sc, dest_cap); + ts::return_shared(ref_obj); + ts::end(sc); +} + +#[test] +#[expected_failure(abort_code = osh::EReceiverObjectMismatch)] +public fun test_v2_extract_with_wrong_object_ids_aborts() { + let mut sc = ts::begin(OWNER); + state_object::test_init(sc.ctx()); + osh::test_init(sc.ctx()); + sc.next_tx(OWNER); + + let owner_cap = ts::take_from_sender(&sc); + let mut ref_obj = ts::take_shared(&sc); + let dest_cap = ts::take_from_sender(&sc); + + upgrade_registry::initialize(&mut ref_obj, &owner_cap, sc.ctx()); + receiver_registry::initialize(&mut ref_obj, &owner_cap, sc.ctx()); + + let mut receiver_params = osh::create_receiver_params_v2(&dest_cap, SOURCE_CHAIN); + + let msg = osh::new_any2sui_message_v2( + &dest_cap, + b"message_id_32_bytes_padding_ok!!", + SOURCE_CHAIN, + b"sender", + b"payload", + RECEIVER_PKG, + @0x0, + vector[@0x1111], // committed: object A + vector[], + vector[], + ); + + osh::populate_message_v2(&dest_cap, &mut receiver_params, msg); + + // Extract with WRONG object IDs — should abort with EReceiverObjectMismatch + let _extracted = osh::extract_any2sui_message_v2( + &mut receiver_params, + vector[@0x9999], // attacker substitutes different object + ); + + // unreachable — cleanup for compiler + abort 0 +} + +#[test] +public fun test_v2_extract_with_empty_object_ids() { + let mut sc = ts::begin(OWNER); + state_object::test_init(sc.ctx()); + osh::test_init(sc.ctx()); + sc.next_tx(OWNER); + + let owner_cap = ts::take_from_sender(&sc); + let mut ref_obj = ts::take_shared(&sc); + let dest_cap = ts::take_from_sender(&sc); + + upgrade_registry::initialize(&mut ref_obj, &owner_cap, sc.ctx()); + receiver_registry::initialize(&mut ref_obj, &owner_cap, sc.ctx()); + + let mut receiver_params = osh::create_receiver_params_v2(&dest_cap, SOURCE_CHAIN); + + let msg = osh::new_any2sui_message_v2( + &dest_cap, + b"message_id_32_bytes_padding_ok!!", + SOURCE_CHAIN, + b"sender", + b"payload", + RECEIVER_PKG, + @0x0, + vector[], // stateless receiver: no objects committed + vector[], + vector[], + ); + + osh::populate_message_v2(&dest_cap, &mut receiver_params, msg); + + // Extract with empty used_object_ids — should succeed + let extracted = osh::extract_any2sui_message_v2( + &mut receiver_params, + vector[], + ); + + osh::deconstruct_receiver_params_v2_with_message_for_test(&dest_cap, RECEIVER_PKG, receiver_params, extracted); + + ts::return_to_sender(&sc, owner_cap); + ts::return_to_sender(&sc, dest_cap); + ts::return_shared(ref_obj); + ts::end(sc); +} + +#[test] +public fun test_v2_no_message_populated_deconstruct_succeeds() { + let mut sc = ts::begin(OWNER); + state_object::test_init(sc.ctx()); + osh::test_init(sc.ctx()); + sc.next_tx(OWNER); + + let owner_cap = ts::take_from_sender(&sc); + let mut ref_obj = ts::take_shared(&sc); + let dest_cap = ts::take_from_sender(&sc); + + upgrade_registry::initialize(&mut ref_obj, &owner_cap, sc.ctx()); + + // Token-only or unregistered receiver: no message populated + let receiver_params = osh::create_receiver_params_v2(&dest_cap, SOURCE_CHAIN); + osh::deconstruct_receiver_params_v2(&dest_cap, receiver_params); + + ts::return_to_sender(&sc, owner_cap); + ts::return_to_sender(&sc, dest_cap); + ts::return_shared(ref_obj); + ts::end(sc); +} + +#[test] +#[expected_failure(abort_code = osh::ECCIPReceiveFailed)] +public fun test_v2_message_not_consumed_deconstruct_aborts() { + let mut sc = ts::begin(OWNER); + state_object::test_init(sc.ctx()); + osh::test_init(sc.ctx()); + sc.next_tx(OWNER); + + let owner_cap = ts::take_from_sender(&sc); + let mut ref_obj = ts::take_shared(&sc); + let dest_cap = ts::take_from_sender(&sc); + + upgrade_registry::initialize(&mut ref_obj, &owner_cap, sc.ctx()); + receiver_registry::initialize(&mut ref_obj, &owner_cap, sc.ctx()); + + let mut receiver_params = osh::create_receiver_params_v2(&dest_cap, SOURCE_CHAIN); + + let msg = osh::new_any2sui_message_v2( + &dest_cap, + b"message_id_32_bytes_padding_ok!!", + SOURCE_CHAIN, + b"sender", + b"payload", + RECEIVER_PKG, + @0x0, + vector[@0x1111], + vector[], + vector[], + ); + + osh::populate_message_v2(&dest_cap, &mut receiver_params, msg); + + // Do NOT extract the message — simulates skipping ccip_receive + // deconstruct should abort because message is still populated + osh::deconstruct_receiver_params_v2(&dest_cap, receiver_params); + + abort 0 // unreachable +} + +#[test] +public fun test_v2_receiver_object_ids_stored_correctly() { + let msg = client::new_any2sui_message_v2( + b"message_id_32_bytes_padding_ok!!", + SOURCE_CHAIN, + b"sender", + b"data", + RECEIVER_PKG, + @0x0, + vector[@0x1111, @0x2222, @0x3333], + vector[], + ); + + assert!(*client::get_receiver_object_ids(&msg) == vector[@0x1111, @0x2222, @0x3333]); + + // Consume to satisfy no-drop + client::consume_any2sui_message_v2(msg, RECEIVER_PKG); +} diff --git a/contracts/ccip/ccip/tests/rmn_remote_tests.move b/contracts/ccip/ccip/tests/rmn_remote_tests.move index 8bf4f4282..608cae275 100644 --- a/contracts/ccip/ccip/tests/rmn_remote_tests.move +++ b/contracts/ccip/ccip/tests/rmn_remote_tests.move @@ -3,9 +3,15 @@ module ccip::rmn_remote_test; use ccip::ownable::OwnerCap; -use ccip::rmn_remote::{Self, RMNRemoteState}; +use ccip::rmn_remote::{Self, RMNRemoteState, CurserCap}; use ccip::state_object::{Self, CCIPObjectRef}; use ccip::upgrade_registry; +use mcms::mcms_account; +use mcms::mcms_deployer; +use mcms::mcms_registry::{Self, Registry}; +use std::string; +use std::unit_test; +use sui::bcs; use sui::test_scenario::{Self, Scenario}; // === Constants === @@ -550,3 +556,491 @@ public fun test_curse_function_not_allowed() { tear_down_test(scenario, owner_cap, ref); } + +// ================================================================ +// | Fast Curse via CurserCap Tests | +// ================================================================ + +// === Direct cap-path tests (no MCMS Registry involved) === + +#[test] +public fun test_create_curser_cap_succeeds() { + let (mut scenario, owner_cap, mut ref) = set_up_test(); + let ctx = scenario.ctx(); + + initialize_rmn_remote(&mut ref, &owner_cap, TEST_CHAIN_SELECTOR, ctx); + + let curser_cap = rmn_remote::create_curser_cap(&mut ref, &owner_cap, ctx); + unit_test::destroy(curser_cap); + + tear_down_test(scenario, owner_cap, ref); +} + +#[test] +public fun test_curse_with_curser_cap_succeeds() { + let (mut scenario, owner_cap, mut ref) = set_up_test(); + let ctx = scenario.ctx(); + + initialize_rmn_remote(&mut ref, &owner_cap, TEST_CHAIN_SELECTOR, ctx); + let curser_cap = rmn_remote::create_curser_cap(&mut ref, &owner_cap, ctx); + + rmn_remote::curse_with_curser_cap(&mut ref, &curser_cap, SUBJECT_1); + assert!(rmn_remote::is_cursed(&ref, SUBJECT_1)); + + let cursed = rmn_remote::get_cursed_subjects(&ref); + assert!(cursed.length() == 1); + + unit_test::destroy(curser_cap); + tear_down_test(scenario, owner_cap, ref); +} + +#[test] +public fun test_curse_multiple_with_curser_cap_succeeds() { + let (mut scenario, owner_cap, mut ref) = set_up_test(); + let ctx = scenario.ctx(); + + initialize_rmn_remote(&mut ref, &owner_cap, TEST_CHAIN_SELECTOR, ctx); + let curser_cap = rmn_remote::create_curser_cap(&mut ref, &owner_cap, ctx); + + rmn_remote::curse_multiple_with_curser_cap( + &mut ref, + &curser_cap, + vector[SUBJECT_1, SUBJECT_2], + ); + assert!(rmn_remote::is_cursed(&ref, SUBJECT_1)); + assert!(rmn_remote::is_cursed(&ref, SUBJECT_2)); + + unit_test::destroy(curser_cap); + tear_down_test(scenario, owner_cap, ref); +} + +#[test] +#[expected_failure(abort_code = rmn_remote::EInvalidSubjectLength)] +public fun test_curse_with_curser_cap_invalid_subject_length() { + let (mut scenario, owner_cap, mut ref) = set_up_test(); + let ctx = scenario.ctx(); + + initialize_rmn_remote(&mut ref, &owner_cap, TEST_CHAIN_SELECTOR, ctx); + let curser_cap = rmn_remote::create_curser_cap(&mut ref, &owner_cap, ctx); + + rmn_remote::curse_with_curser_cap(&mut ref, &curser_cap, INVALID_SHORT_SUBJECT); + + unit_test::destroy(curser_cap); + tear_down_test(scenario, owner_cap, ref); +} + +#[test] +#[expected_failure(abort_code = rmn_remote::EAlreadyCursed)] +public fun test_curse_with_curser_cap_already_cursed() { + let (mut scenario, owner_cap, mut ref) = set_up_test(); + let ctx = scenario.ctx(); + + initialize_rmn_remote(&mut ref, &owner_cap, TEST_CHAIN_SELECTOR, ctx); + let curser_cap = rmn_remote::create_curser_cap(&mut ref, &owner_cap, ctx); + + rmn_remote::curse_with_curser_cap(&mut ref, &curser_cap, SUBJECT_1); + rmn_remote::curse_with_curser_cap(&mut ref, &curser_cap, SUBJECT_1); + + unit_test::destroy(curser_cap); + tear_down_test(scenario, owner_cap, ref); +} + +#[test] +#[expected_failure(abort_code = upgrade_registry::EFunctionNotAllowed)] +public fun test_curse_with_curser_cap_function_not_allowed() { + let (mut scenario, owner_cap, mut ref) = set_up_test(); + let ctx = scenario.ctx(); + + initialize_rmn_remote(&mut ref, &owner_cap, TEST_CHAIN_SELECTOR, ctx); + let curser_cap = rmn_remote::create_curser_cap(&mut ref, &owner_cap, ctx); + + upgrade_registry::block_function( + &mut ref, + &owner_cap, + string::utf8(b"rmn_remote"), + string::utf8(b"curse_with_curser_cap"), + 1, + ctx, + ); + + rmn_remote::curse_with_curser_cap(&mut ref, &curser_cap, SUBJECT_1); + + unit_test::destroy(curser_cap); + tear_down_test(scenario, owner_cap, ref); +} + +#[test] +#[expected_failure(abort_code = upgrade_registry::EFunctionNotAllowed)] +public fun test_create_curser_cap_function_not_allowed() { + let (mut scenario, owner_cap, mut ref) = set_up_test(); + let ctx = scenario.ctx(); + + initialize_rmn_remote(&mut ref, &owner_cap, TEST_CHAIN_SELECTOR, ctx); + + upgrade_registry::block_function( + &mut ref, + &owner_cap, + string::utf8(b"rmn_remote"), + string::utf8(b"create_curser_cap"), + 1, + ctx, + ); + + let curser_cap = rmn_remote::create_curser_cap(&mut ref, &owner_cap, ctx); + unit_test::destroy(curser_cap); + + tear_down_test(scenario, owner_cap, ref); +} + +// === MCMS callback tests === + +const BATCH_ID_1: vector = x"0000000000000000000000000000000000000000000000000000000000000001"; +const BATCH_ID_2: vector = x"0000000000000000000000000000000000000000000000000000000000000002"; +const BATCH_ID_3: vector = x"0000000000000000000000000000000000000000000000000000000000000003"; +const BATCH_ID_4: vector = x"0000000000000000000000000000000000000000000000000000000000000004"; + +/// Initializes CCIP and a single MCMS Registry (used here as the FAST Registry) +/// pre-populated with a fresh `CurserCap` for `@ccip` and +/// `allowed_modules = [b"rmn_remote"]`. Used to exercise the fast-path +/// callbacks (`mcms_curse_with_curser_cap` etc.) end-to-end. +fun setup_fast_registry_with_cap(): (Scenario, OwnerCap, CCIPObjectRef, Registry, ID) { + let mut scenario = test_scenario::begin(ADMIN_ADDRESS); + { + let ctx = scenario.ctx(); + mcms_account::test_init(ctx); + mcms_registry::test_init(ctx); + mcms_deployer::test_init(ctx); + state_object::test_init(ctx); + }; + scenario.next_tx(ADMIN_ADDRESS); + + let mut fast_registry = test_scenario::take_shared(&scenario); + let mut ref = test_scenario::take_shared(&scenario); + let owner_cap = test_scenario::take_from_sender(&scenario); + + upgrade_registry::initialize(&mut ref, &owner_cap, scenario.ctx()); + rmn_remote::initialize(&mut ref, &owner_cap, TEST_CHAIN_SELECTOR, scenario.ctx()); + + let curser_cap = rmn_remote::create_curser_cap(&mut ref, &owner_cap, scenario.ctx()); + let curser_cap_id = object::id_address(&curser_cap); + + let publisher_wrapper = mcms_registry::create_publisher_wrapper( + ccip::ownable::borrow_publisher(&owner_cap), + state_object::test_create_mcms_callback(), + ); + mcms_registry::register_entrypoint( + &mut fast_registry, + publisher_wrapper, + state_object::test_create_mcms_callback(), + curser_cap, + vector[b"rmn_remote"], + scenario.ctx(), + ); + + scenario.next_tx(ADMIN_ADDRESS); + + (scenario, owner_cap, ref, fast_registry, sui::object::id_from_address(curser_cap_id)) +} + +fun tear_down_fast_registry( + scenario: Scenario, + owner_cap: OwnerCap, + ref: CCIPObjectRef, + fast_registry: Registry, +) { + test_scenario::return_to_sender(&scenario, owner_cap); + test_scenario::return_shared(ref); + test_scenario::return_shared(fast_registry); + test_scenario::end(scenario); +} + +#[test] +public fun test_mcms_curse_with_curser_cap_succeeds() { + let (mut scenario, owner_cap, mut ref, mut fast_registry, curser_cap_id) = + setup_fast_registry_with_cap(); + + let mut data = vector::empty(); + data.append(bcs::to_bytes(&object::id_address(&ref))); + data.append(bcs::to_bytes(&sui::object::id_to_address(&curser_cap_id))); + data.append(bcs::to_bytes(&SUBJECT_1)); + + let params = mcms_registry::test_create_executing_callback_params( + @ccip, + string::utf8(b"rmn_remote"), + string::utf8(b"curse_with_curser_cap"), + data, + BATCH_ID_1, + 0, + 1, + ); + + rmn_remote::mcms_curse_with_curser_cap(&mut ref, &mut fast_registry, params); + + assert!(rmn_remote::is_cursed(&ref, SUBJECT_1)); + + tear_down_fast_registry(scenario, owner_cap, ref, fast_registry); +} + +#[test] +public fun test_mcms_curse_multiple_with_curser_cap_succeeds() { + let (mut scenario, owner_cap, mut ref, mut fast_registry, curser_cap_id) = + setup_fast_registry_with_cap(); + + let subjects = vector[SUBJECT_1, SUBJECT_2]; + + let mut data = vector::empty(); + data.append(bcs::to_bytes(&object::id_address(&ref))); + data.append(bcs::to_bytes(&sui::object::id_to_address(&curser_cap_id))); + data.append(bcs::to_bytes(&subjects)); + + let params = mcms_registry::test_create_executing_callback_params( + @ccip, + string::utf8(b"rmn_remote"), + string::utf8(b"curse_multiple_with_curser_cap"), + data, + BATCH_ID_2, + 0, + 1, + ); + + rmn_remote::mcms_curse_multiple_with_curser_cap(&mut ref, &mut fast_registry, params); + + assert!(rmn_remote::is_cursed(&ref, SUBJECT_1)); + assert!(rmn_remote::is_cursed(&ref, SUBJECT_2)); + + tear_down_fast_registry(scenario, owner_cap, ref, fast_registry); +} + +#[test] +#[expected_failure(abort_code = rmn_remote::EInvalidFunction)] +public fun test_mcms_curse_with_curser_cap_wrong_function_name() { + let (mut scenario, owner_cap, mut ref, mut fast_registry, curser_cap_id) = + setup_fast_registry_with_cap(); + + let mut data = vector::empty(); + data.append(bcs::to_bytes(&object::id_address(&ref))); + data.append(bcs::to_bytes(&sui::object::id_to_address(&curser_cap_id))); + data.append(bcs::to_bytes(&SUBJECT_1)); + + let params = mcms_registry::test_create_executing_callback_params( + @ccip, + string::utf8(b"rmn_remote"), + string::utf8(b"uncurse"), + data, + BATCH_ID_3, + 0, + 1, + ); + + rmn_remote::mcms_curse_with_curser_cap(&mut ref, &mut fast_registry, params); + + tear_down_fast_registry(scenario, owner_cap, ref, fast_registry); +} + +// === Slow-MCMS bootstrap tests (two Registries) === + +const FAST_RECIPIENT: address = @0xF45CA; + +/// Initializes CCIP, transfers `OwnerCap` into a SLOW MCMS Registry, then +/// shares a second empty Registry to act as the FAST instance. +fun setup_slow_and_fast_registries(): (Scenario, CCIPObjectRef, Registry /* slow */, Registry /* fast */) { + let mut scenario = test_scenario::begin(ADMIN_ADDRESS); + { + let ctx = scenario.ctx(); + mcms_account::test_init(ctx); + mcms_registry::test_init(ctx); + mcms_deployer::test_init(ctx); + state_object::test_init(ctx); + }; + scenario.next_tx(ADMIN_ADDRESS); + + let mut slow_registry = test_scenario::take_shared(&scenario); + let mut ref = test_scenario::take_shared(&scenario); + let owner_cap = test_scenario::take_from_sender(&scenario); + + upgrade_registry::initialize(&mut ref, &owner_cap, scenario.ctx()); + rmn_remote::initialize(&mut ref, &owner_cap, TEST_CHAIN_SELECTOR, scenario.ctx()); + + state_object::transfer_ownership( + &mut ref, + &owner_cap, + mcms_registry::get_multisig_address(), + scenario.ctx(), + ); + scenario.next_tx(mcms_registry::get_multisig_address()); + state_object::accept_ownership(&mut ref, scenario.ctx()); + + state_object::execute_ownership_transfer_to_mcms( + &mut ref, + owner_cap, + &mut slow_registry, + @mcms, + scenario.ctx(), + ); + + scenario.next_tx(ADMIN_ADDRESS); + { + let ctx = scenario.ctx(); + mcms_registry::test_init(ctx); + }; + scenario.next_tx(ADMIN_ADDRESS); + + let fast_registry = test_scenario::take_shared(&scenario); + + (scenario, ref, slow_registry, fast_registry) +} + +fun tear_down_slow_and_fast( + scenario: Scenario, + ref: CCIPObjectRef, + slow_registry: Registry, + fast_registry: Registry, +) { + test_scenario::return_shared(ref); + test_scenario::return_shared(slow_registry); + test_scenario::return_shared(fast_registry); + test_scenario::end(scenario); +} + +#[test] +public fun test_mcms_create_curser_cap_succeeds() { + let (mut scenario, mut ref, mut slow_registry, fast_registry) = setup_slow_and_fast_registries(); + + let owner_cap_address = test_owner_cap_address_in_registry(&slow_registry); + + let mut data = vector::empty(); + data.append(bcs::to_bytes(&object::id_address(&ref))); + data.append(bcs::to_bytes(&owner_cap_address)); + + let params = mcms_registry::test_create_executing_callback_params( + @ccip, + string::utf8(b"rmn_remote"), + string::utf8(b"create_curser_cap"), + data, + BATCH_ID_1, + 0, + 1, + ); + + let curser_cap = rmn_remote::mcms_create_curser_cap( + &mut ref, + &mut slow_registry, + params, + scenario.ctx(), + ); + + sui::transfer::public_transfer(curser_cap, FAST_RECIPIENT); + + tear_down_slow_and_fast(scenario, ref, slow_registry, fast_registry); +} + +#[test] +public fun test_mcms_register_curser_cap_succeeds() { + let (mut scenario, mut ref, mut slow_registry, mut fast_registry) = + setup_slow_and_fast_registries(); + + let owner_cap_address = test_owner_cap_address_in_registry(&slow_registry); + + // Op 0: mcms_create_curser_cap + let mut create_data = vector::empty(); + create_data.append(bcs::to_bytes(&object::id_address(&ref))); + create_data.append(bcs::to_bytes(&owner_cap_address)); + + let create_params = mcms_registry::test_create_executing_callback_params( + @ccip, + string::utf8(b"rmn_remote"), + string::utf8(b"create_curser_cap"), + create_data, + BATCH_ID_2, + 0, + 2, + ); + + let curser_cap = rmn_remote::mcms_create_curser_cap( + &mut ref, + &mut slow_registry, + create_params, + scenario.ctx(), + ); + + // Op 1: mcms_register_curser_cap + let mut register_data = vector::empty(); + register_data.append(bcs::to_bytes(&object::id_address(&ref))); + register_data.append(bcs::to_bytes(&owner_cap_address)); + register_data.append(bcs::to_bytes(&object::id_address(&fast_registry))); + + let register_params = mcms_registry::test_create_executing_callback_params( + @ccip, + string::utf8(b"rmn_remote"), + string::utf8(b"register_curser_cap"), + register_data, + BATCH_ID_2, + 1, + 2, + ); + + rmn_remote::mcms_register_curser_cap( + &mut ref, + &mut slow_registry, + &mut fast_registry, + register_params, + curser_cap, + scenario.ctx(), + ); + + let allowed = mcms_registry::get_allowed_modules( + &fast_registry, + sui::address::to_ascii_string(@ccip), + ); + assert!(allowed == vector[b"rmn_remote"]); + + tear_down_slow_and_fast(scenario, ref, slow_registry, fast_registry); +} + +#[test] +public fun test_mcms_mint_and_register_curser_cap_succeeds() { + let (mut scenario, mut ref, mut slow_registry, mut fast_registry) = + setup_slow_and_fast_registries(); + + let owner_cap_address = test_owner_cap_address_in_registry(&slow_registry); + + let mut data = vector::empty(); + data.append(bcs::to_bytes(&object::id_address(&ref))); + data.append(bcs::to_bytes(&owner_cap_address)); + data.append(bcs::to_bytes(&object::id_address(&fast_registry))); + + let params = mcms_registry::test_create_executing_callback_params( + @ccip, + string::utf8(b"rmn_remote"), + string::utf8(b"mint_and_register_curser_cap"), + data, + BATCH_ID_4, + 0, + 1, + ); + + rmn_remote::mcms_mint_and_register_curser_cap( + &mut ref, + &mut slow_registry, + &mut fast_registry, + params, + scenario.ctx(), + ); + + let allowed = mcms_registry::get_allowed_modules( + &fast_registry, + sui::address::to_ascii_string(@ccip), + ); + assert!(allowed == vector[b"rmn_remote"]); + + tear_down_slow_and_fast(scenario, ref, slow_registry, fast_registry); +} + +/// Helper to read the address of the `OwnerCap` stored inside a slow MCMS +/// Registry, so callers can populate `validate_obj_addrs` data correctly. +fun test_owner_cap_address_in_registry(registry: &Registry): address { + mcms_registry::test_get_cap_address( + registry, + sui::address::to_ascii_string(@ccip), + ) +} diff --git a/contracts/ccip/ccip_broken_receiver/Move.lock b/contracts/ccip/ccip_broken_receiver/Move.lock new file mode 100644 index 000000000..276ab11b5 --- /dev/null +++ b/contracts/ccip/ccip_broken_receiver/Move.lock @@ -0,0 +1,35 @@ +# Generated by move; do not edit +# This file should be checked in. + +[move] +version = 4 + +[pinned.testnet.MoveStdlib] +source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/move-stdlib", rev = "c2428b3aaf9c24270b609001e56d96cb10c76d28" } +use_environment = "testnet" +manifest_digest = "C4FE4C91DE74CBF223B2E380AE40F592177D21870DC2D7EB6227D2D694E05363" +deps = {} + +[pinned.testnet.Sui] +source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "c2428b3aaf9c24270b609001e56d96cb10c76d28" } +use_environment = "testnet" +manifest_digest = "7AFB66695545775FBFBB2D3078ADFD084244D5002392E837FDE21D9EA1C6D01C" +deps = { MoveStdlib = "MoveStdlib" } + +[pinned.testnet.ccip] +source = { local = "../ccip" } +use_environment = "testnet" +manifest_digest = "B41543A1928002B1ABE6F16D69A3C1BE6BCA4D7A4DB7D2F9048274EFE11ED724" +deps = { mcms = "mcms", std = "MoveStdlib", sui = "Sui" } + +[pinned.testnet.ccip_broken_receiver] +source = { root = true } +use_environment = "testnet" +manifest_digest = "C7C879F0A4269E2362771F19494E1AD03A480D84F6E8221262E457CF21A2249B" +deps = { ccip = "ccip", std = "MoveStdlib", sui = "Sui" } + +[pinned.testnet.mcms] +source = { local = "../../mcms/mcms" } +use_environment = "testnet" +manifest_digest = "5745706258F61D6CE210904B3E6AE87A73CE9D31A6F93BE4718C442529332A87" +deps = { std = "MoveStdlib", sui = "Sui" } diff --git a/contracts/ccip/ccip_broken_receiver/Move.toml b/contracts/ccip/ccip_broken_receiver/Move.toml new file mode 100644 index 000000000..acf6474d7 --- /dev/null +++ b/contracts/ccip/ccip_broken_receiver/Move.toml @@ -0,0 +1,6 @@ +[package] +name = "ccip_broken_receiver" +edition = "2024" + +[dependencies] +ccip = { local = "../ccip" } diff --git a/contracts/ccip/ccip_broken_receiver/sources/broken_receiver.move b/contracts/ccip/ccip_broken_receiver/sources/broken_receiver.move new file mode 100644 index 000000000..19e368d92 --- /dev/null +++ b/contracts/ccip/ccip_broken_receiver/sources/broken_receiver.move @@ -0,0 +1,56 @@ +// THIS CONTRACT IS ONLY FOR TESTING PURPOSES. +// It exposes a ccip_receive with a generic type parameter to produce a normalized ABI +// containing {"Vector": {"TypeParameter": 0}}, which triggers the poison ABI path +// in the relayer's DecodeParameters/decodeParam. +module ccip_broken_receiver::broken_receiver; + +use ccip::client; +use ccip::offramp_state_helper as osh; +use ccip::publisher_wrapper; +use ccip::receiver_registry; +use ccip::state_object::CCIPObjectRef; +use std::string::{Self, String}; +use sui::dynamic_field as df; +use sui::package::{Self, Publisher}; + +public struct BROKEN_RECEIVER has drop {} + +public struct OwnerCap has key, store { + id: UID, +} + +public struct BrokenReceiverProof has drop {} + +public struct PublisherKey has copy, drop, store {} + +public fun type_and_version(): String { + string::utf8(b"BrokenReceiver 1.0.0-test") +} + +fun init(otw: BROKEN_RECEIVER, ctx: &mut TxContext) { + let mut owner_cap = OwnerCap { + id: object::new(ctx), + }; + + let publisher = package::claim(otw, ctx); + df::add(&mut owner_cap.id, PublisherKey {}, publisher); + + transfer::transfer(owner_cap, ctx.sender()); +} + +public fun register_receiver(owner_cap: &OwnerCap, ref: &mut CCIPObjectRef) { + let publisher: &Publisher = df::borrow(&owner_cap.id, PublisherKey {}); + let publisher_wrapper = publisher_wrapper::create(publisher, BrokenReceiverProof {}); + receiver_registry::register_receiver(ref, publisher_wrapper, BrokenReceiverProof {}); +} + +/// This function has a generic type parameter T, producing a normalized ABI with +/// {"Vector": {"TypeParameter": 0}} that the relayer cannot decode. +public fun ccip_receive( + _items: vector, + _expected_message_id: vector, + ref: &CCIPObjectRef, + message: client::Any2SuiMessage, +) { + osh::consume_any2sui_message(ref, message, BrokenReceiverProof {}); +} diff --git a/contracts/ccip/ccip_offramp/sources/offramp.move b/contracts/ccip/ccip_offramp/sources/offramp.move index 141bfc9b6..90ac49121 100644 --- a/contracts/ccip/ccip_offramp/sources/offramp.move +++ b/contracts/ccip/ccip_offramp/sources/offramp.move @@ -1763,9 +1763,425 @@ public fun mcms_remove_allowed_modules( mcms_registry::remove_allowed_modules(registry, McmsCallback {}, module_names, ctx); } +// ================================================================ +// | V2 Execution (Object Binding) | +// ================================================================ + +public struct Any2SuiRampMessageV2 has drop { + header: RampMessageHeader, + sender: vector, + data: vector, + receiver: address, + gas_limit: u256, + token_receiver: address, + receiver_object_ids: vector
, + token_amounts: vector, +} + +public struct ExecutionReportV2 has drop { + source_chain_selector: u64, + message: Any2SuiRampMessageV2, + offchain_token_data: vector>, + proofs: vector>, +} + +public fun init_execute_v2( + ref: &CCIPObjectRef, + state: &mut OffRampState, + clock: &clock::Clock, + report_context: vector>, + report: vector, + ctx: &mut TxContext, +): osh::ReceiverParamsV2 { + verify_function_allowed( + ref, + string::utf8(b"offramp"), + string::utf8(b"init_execute_v2"), + VERSION, + ); + assert!(report_context.length() == 2, EInvalidReportContextLength); + let reports = deserialize_execution_report_v2(report); + + ocr3_base::transmit( + &state.ocr3_base_state, + ctx.sender(), + ocr3_base::ocr_plugin_type_execution(), + report_context, + report, + vector[], + ctx, + ); + + pre_execute_single_report_v2(ref, state, clock, reports, false) +} + +public fun manually_init_execute_v2( + ref: &CCIPObjectRef, + state: &mut OffRampState, + clock: &clock::Clock, + report_bytes: vector, +): osh::ReceiverParamsV2 { + verify_function_allowed( + ref, + string::utf8(b"offramp"), + string::utf8(b"manually_init_execute_v2"), + VERSION, + ); + let reports = deserialize_execution_report_v2(report_bytes); + + pre_execute_single_report_v2(ref, state, clock, reports, true) +} + +public fun finish_execute_v2( + ref: &CCIPObjectRef, + state: &mut OffRampState, + receiver_params: osh::ReceiverParamsV2, +) { + verify_function_allowed( + ref, + string::utf8(b"offramp"), + string::utf8(b"finish_execute_v2"), + VERSION, + ); + assert!(state.dest_transfer_cap.is_some(), EDestTransferCapNotSet); + osh::deconstruct_receiver_params_v2(state.dest_transfer_cap.borrow(), receiver_params); +} + +fun deserialize_execution_report_v2(report_bytes: vector): ExecutionReportV2 { + let mut stream = bcs_stream::new(report_bytes); + let source_chain_selector = bcs_stream::deserialize_u64(&mut stream); + + let message_id = bcs_stream::deserialize_fixed_vector_u8(&mut stream, 32); + let header_source_chain_selector = bcs_stream::deserialize_u64(&mut stream); + let dest_chain_selector = bcs_stream::deserialize_u64(&mut stream); + let sequence_number = bcs_stream::deserialize_u64(&mut stream); + let nonce = bcs_stream::deserialize_u64(&mut stream); + + let header = RampMessageHeader { + message_id, + source_chain_selector: header_source_chain_selector, + dest_chain_selector, + sequence_number, + nonce, + }; + + assert!(source_chain_selector == header_source_chain_selector, ESourceChainSelectorMismatch); + + let sender = bcs_stream::deserialize_vector_u8(&mut stream); + let data = bcs_stream::deserialize_vector_u8(&mut stream); + let receiver = bcs_stream::deserialize_address(&mut stream); + let gas_limit = bcs_stream::deserialize_u256(&mut stream); + let token_receiver = bcs_stream::deserialize_address(&mut stream); + + let receiver_object_ids = bcs_stream::deserialize_vector!( + &mut stream, + |stream| bcs_stream::deserialize_address(stream), + ); + + let token_amounts = bcs_stream::deserialize_vector!(&mut stream, |stream| { + let source_pool_address = bcs_stream::deserialize_vector_u8(stream); + let dest_token_address = bcs_stream::deserialize_address(stream); + let dest_gas_amount = bcs_stream::deserialize_u32(stream); + let extra_data = bcs_stream::deserialize_vector_u8(stream); + let amount = bcs_stream::deserialize_u256(stream); + + Any2SuiTokenTransfer { + source_pool_address, + dest_token_address, + dest_gas_amount, + extra_data, + amount, + } + }); + + let message = Any2SuiRampMessageV2 { + header, + sender, + data, + receiver, + gas_limit, + token_receiver, + receiver_object_ids, + token_amounts, + }; + + let offchain_token_data = bcs_stream::deserialize_vector!( + &mut stream, + |stream| bcs_stream::deserialize_vector_u8(stream), + ); + + let proofs = bcs_stream::deserialize_vector!( + &mut stream, + |stream| { bcs_stream::deserialize_fixed_vector_u8(stream, 32) }, + ); + + bcs_stream::assert_is_consumed(&stream); + + ExecutionReportV2 { source_chain_selector, message, offchain_token_data, proofs } +} + +#[allow(implicit_const_copy)] +fun pre_execute_single_report_v2( + ref: &CCIPObjectRef, + state: &mut OffRampState, + clock: &clock::Clock, + execution_report: ExecutionReportV2, + manual_execution: bool, +): osh::ReceiverParamsV2 { + let source_chain_selector = execution_report.source_chain_selector; + assert!(state.dest_transfer_cap.is_some(), EDestTransferCapNotSet); + + if (rmn_remote::is_cursed_u128(ref, source_chain_selector as u128)) { + assert!(!manual_execution, ECursedByRmn); + + event::emit(SkippedReportExecution { source_chain_selector }); + + return osh::create_receiver_params_v2(state.dest_transfer_cap.borrow(), source_chain_selector) + }; + + assert_source_chain_enabled(state, source_chain_selector); + + assert!( + execution_report.message.header.dest_chain_selector == state.chain_selector, + EDestChainSelectorMismatch, + ); + + let source_chain_config = state.source_chain_configs[&source_chain_selector]; + let metadata_hash = calculate_metadata_hash( + ref, + source_chain_selector, + state.chain_selector, + source_chain_config.on_ramp, + ); + + let hashed_leaf = calculate_message_hash_v2( + &execution_report.message, + metadata_hash, + ); + + let root = merkle_proof::merkle_root(hashed_leaf, execution_report.proofs); + + let is_old_commit_report = is_committed_root(state, clock, root); + + if (manual_execution) { + assert!(is_old_commit_report, EManualExecutionNotYetEnabled); + }; + + let source_chain_execution_states = state.execution_states.borrow_mut(source_chain_selector); + + let message = &execution_report.message; + let sequence_number = message.header.sequence_number; + if (!source_chain_execution_states.contains(sequence_number)) { + source_chain_execution_states.add(sequence_number, EXECUTION_STATE_UNTOUCHED); + }; + let execution_state_ref = source_chain_execution_states.borrow_mut(sequence_number); + + if (*execution_state_ref != EXECUTION_STATE_UNTOUCHED) { + event::emit(SkippedAlreadyExecuted { source_chain_selector, sequence_number }); + + return osh::create_receiver_params_v2(state.dest_transfer_cap.borrow(), source_chain_selector) + }; + + assert!(message.header.nonce == 0, EMustBeOutOfOrderExec); + + let number_of_tokens_in_msg = message.token_amounts.length(); + assert!(number_of_tokens_in_msg <= TOKEN_TRANSFER_LIMIT, ETokenTransferLimitExceeded); + assert!( + number_of_tokens_in_msg == execution_report.offchain_token_data.length(), + ETokenDataMismatch, + ); + assert!( + message.token_receiver == @0x0 && number_of_tokens_in_msg == 0 || + (message.token_receiver != @0x0 && number_of_tokens_in_msg > 0), + EInvalidTokenReceiver, + ); + + let mut receiver_params = osh::create_receiver_params_v2( + state.dest_transfer_cap.borrow(), + source_chain_selector, + ); + + let mut token_addresses = vector[]; + let mut token_amounts = vector[]; + + if (number_of_tokens_in_msg == TOKEN_TRANSFER_LIMIT) { + let token_pool_address: address = token_admin_registry::get_pool( + ref, + message.token_amounts[0].dest_token_address, + ); + assert!(token_pool_address != @0x0, EUnsupportedToken); + + osh::add_dest_token_transfer_v2( + state.dest_transfer_cap.borrow(), + &mut receiver_params, + message.token_receiver, + source_chain_selector, + message.token_amounts[0].amount, + message.token_amounts[0].dest_token_address, + token_pool_address, + message.token_amounts[0].source_pool_address, + message.token_amounts[0].extra_data, + execution_report.offchain_token_data[0], + ); + token_addresses.push_back(message.token_amounts[0].dest_token_address); + token_amounts.push_back(message.token_amounts[0].amount); + }; + + let has_valid_message_receiver = + (!message.data.is_empty() || message.gas_limit != 0) && receiver_registry::is_registered_receiver(ref, message.receiver); + + if (has_valid_message_receiver) { + let any2sui_message = osh::new_any2sui_message_v2( + state.dest_transfer_cap.borrow(), + message.header.message_id, + message.header.source_chain_selector, + message.sender, + message.data, + message.receiver, + message.token_receiver, + message.receiver_object_ids, + token_addresses, + token_amounts, + ); + + osh::populate_message_v2( + state.dest_transfer_cap.borrow(), + &mut receiver_params, + any2sui_message, + ); + }; + + *execution_state_ref = EXECUTION_STATE_SUCCESS; + + event::emit(ExecutionStateChanged { + source_chain_selector, + sequence_number, + message_id: message.header.message_id, + message_hash: hashed_leaf, + state: EXECUTION_STATE_SUCCESS, + }); + + receiver_params +} + +fun calculate_message_hash_v2( + message: &Any2SuiRampMessageV2, + metadata_hash: vector, +): vector { + let mut outer_hash = vector[]; + eth_abi::encode_right_padded_bytes32(&mut outer_hash, merkle_proof::leaf_domain_separator()); + eth_abi::encode_right_padded_bytes32(&mut outer_hash, metadata_hash); + + let mut inner_hash = vector[]; + eth_abi::encode_right_padded_bytes32(&mut inner_hash, message.header.message_id); + eth_abi::encode_address(&mut inner_hash, message.receiver); + eth_abi::encode_u64(&mut inner_hash, message.header.sequence_number); + eth_abi::encode_u256(&mut inner_hash, message.gas_limit); + eth_abi::encode_address(&mut inner_hash, message.token_receiver); + eth_abi::encode_u64(&mut inner_hash, message.header.nonce); + eth_abi::encode_right_padded_bytes32(&mut outer_hash, hash::keccak256(&inner_hash)); + + eth_abi::encode_right_padded_bytes32(&mut outer_hash, hash::keccak256(&message.sender)); + eth_abi::encode_right_padded_bytes32(&mut outer_hash, hash::keccak256(&message.data)); + + let mut token_hash = vector[]; + eth_abi::encode_u256( + &mut token_hash, + message.token_amounts.length() as u256, + ); + message.token_amounts.do_ref!(|token_transfer| { + let token_transfer: &Any2SuiTokenTransfer = token_transfer; + eth_abi::encode_bytes(&mut token_hash, token_transfer.source_pool_address); + eth_abi::encode_address(&mut token_hash, token_transfer.dest_token_address); + eth_abi::encode_u32(&mut token_hash, token_transfer.dest_gas_amount); + eth_abi::encode_bytes(&mut token_hash, token_transfer.extra_data); + eth_abi::encode_u256(&mut token_hash, token_transfer.amount); + }); + eth_abi::encode_right_padded_bytes32(&mut outer_hash, hash::keccak256(&token_hash)); + + let mut object_ids_hash = vector[]; + eth_abi::encode_u256( + &mut object_ids_hash, + message.receiver_object_ids.length() as u256, + ); + message.receiver_object_ids.do_ref!(|id| { + eth_abi::encode_address(&mut object_ids_hash, *id); + }); + eth_abi::encode_right_padded_bytes32(&mut outer_hash, hash::keccak256(&object_ids_hash)); + + hash::keccak256(&outer_hash) +} + // ============================== Test Functions ============================== // #[test_only] public fun test_init(ctx: &mut TxContext) { init(OFFRAMP {}, ctx); } + +#[test_only] +public fun test_calculate_message_hash_v2( + message_id: vector, + source_chain_selector: u64, + dest_chain_selector: u64, + sequence_number: u64, + nonce: u64, + sender: vector, + receiver: address, + on_ramp: vector, + data: vector, + gas_limit: u256, + token_receiver: address, + receiver_object_ids: vector
, + source_pool_addresses: vector>, + dest_token_addresses: vector
, + dest_gas_amounts: vector, + extra_datas: vector>, + amounts: vector, +): vector { + let source_pool_addresses_len = source_pool_addresses.length(); + + let mut token_amounts = vector[]; + let mut i = 0; + while (i < source_pool_addresses_len) { + token_amounts.push_back(Any2SuiTokenTransfer { + source_pool_address: source_pool_addresses[i], + dest_token_address: dest_token_addresses[i], + dest_gas_amount: dest_gas_amounts[i], + extra_data: extra_datas[i], + amount: amounts[i], + }); + i = i + 1; + }; + + let message = Any2SuiRampMessageV2 { + header: RampMessageHeader { + message_id, + source_chain_selector, + dest_chain_selector, + sequence_number, + nonce, + }, + sender, + data, + receiver, + gas_limit, + token_receiver, + receiver_object_ids, + token_amounts, + }; + + let metadata_hash = sui::hash::keccak256(&{ + let mut packed = vector[]; + eth_abi::encode_right_padded_bytes32( + &mut packed, + sui::hash::keccak256(&b"Any2SuiMessageHashV1"), + ); + eth_abi::encode_u64(&mut packed, source_chain_selector); + eth_abi::encode_u64(&mut packed, dest_chain_selector); + eth_abi::encode_right_padded_bytes32(&mut packed, sui::hash::keccak256(&on_ramp)); + packed + }); + + calculate_message_hash_v2(&message, metadata_hash) +} diff --git a/contracts/ccip/ccip_offramp/tests/offramp_test.move b/contracts/ccip/ccip_offramp/tests/offramp_test.move index 82236584c..8c1f6a593 100644 --- a/contracts/ccip/ccip_offramp/tests/offramp_test.move +++ b/contracts/ccip/ccip_offramp/tests/offramp_test.move @@ -1027,3 +1027,70 @@ public fun test_calculate_metadata_hash() { transfer::public_transfer(fee_quoter_cap, OWNER); transfer::public_transfer(dest_transfer_cap, OWNER); } + +#[test] +public fun test_calculate_message_hash_v2_parity() { + // Uses the same inputs as Go TestMessageHasherV2_Deterministic in hasher_test.go. + // The expected hash here is the ground truth that the Go test must match. + let message_id = x"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; + let source_chain_selector = 1000u64; + let dest_chain_selector = 2000u64; + let sequence_number = 1u64; + let nonce = 0u64; + let sender = x"8765432109fedcba8765432109fedcba87654321"; + let receiver = @0x1234; + let on_ramp = b"onramp"; + let data = b"test payload"; + let gas_limit = 200000u256; + let token_receiver = @0x5678; + let receiver_object_ids = vector[@0xaabbcc]; + + let hash_v2 = offramp::test_calculate_message_hash_v2( + message_id, + source_chain_selector, + dest_chain_selector, + sequence_number, + nonce, + sender, + receiver, + on_ramp, + data, + gas_limit, + token_receiver, + receiver_object_ids, + vector>[], + vector
[], + vector[], + vector>[], + vector[], + ); + + assert!(hash_v2.length() == 32, 0); + + // Cross-language parity: this value must match Go computeMessageDataHashV2 with same inputs. + let expected_hash_v2 = x"1463b1b58f28f74dd73d4447da139d065051ddbb292549847a8c315d19148fc1"; + assert!(hash_v2 == expected_hash_v2, 2); + + // Verify different receiver_object_ids produce a different hash + let hash_v2_different = offramp::test_calculate_message_hash_v2( + message_id, + source_chain_selector, + dest_chain_selector, + sequence_number, + nonce, + sender, + receiver, + on_ramp, + data, + gas_limit, + token_receiver, + vector[@0xddeeff], + vector>[], + vector
[], + vector[], + vector>[], + vector[], + ); + + assert!(hash_v2 != hash_v2_different, 1); +} diff --git a/contracts/contracts.go b/contracts/contracts.go index 3c10d4efb..59d6e40b9 100644 --- a/contracts/contracts.go +++ b/contracts/contracts.go @@ -13,6 +13,7 @@ type Package string const ( // CCIP CCIP = Package("ccip") + CCIPBrokenReceiver = Package("ccip_broken_receiver") CCIPDummyReceiver = Package("ccip_dummy_receiver") CCIPOfframp = Package("ccip_offramp") CCIPOnramp = Package("ccip_onramp") @@ -41,6 +42,7 @@ const ( var Contracts map[Package]string = map[Package]string{ // CCIP CCIP: filepath.Join("ccip", "ccip"), + CCIPBrokenReceiver: filepath.Join("ccip", "ccip_broken_receiver"), CCIPDummyReceiver: filepath.Join("ccip", "ccip_dummy_receiver"), CCIPBnM: filepath.Join("ccip", "ccip_burn_mint_token"), CCIPOfframp: filepath.Join("ccip", "ccip_offramp"), diff --git a/deployment/changesets/cs_deploy_broken_receiver.go b/deployment/changesets/cs_deploy_broken_receiver.go new file mode 100644 index 000000000..00694368e --- /dev/null +++ b/deployment/changesets/cs_deploy_broken_receiver.go @@ -0,0 +1,70 @@ +package changesets + +import ( + "fmt" + + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + "github.com/smartcontractkit/chainlink-deployments-framework/operations" + "github.com/smartcontractkit/chainlink-sui/bindings/bind" + "github.com/smartcontractkit/chainlink-sui/deployment" + sui_ops "github.com/smartcontractkit/chainlink-sui/deployment/ops" + ccipops "github.com/smartcontractkit/chainlink-sui/deployment/ops/ccip" +) + +type DeployBrokenReceiverConfig struct { + SuiChainSelector uint64 + McmsOwner string +} + +var _ cldf.ChangeSetV2[DeployBrokenReceiverConfig] = DeployBrokenReceiver{} + +// DeployBrokenReceiver deploys the test-only CCIP broken receiver package, whose +// generic ccip_receive produces the poison normalized ABI from report 71024. +type DeployBrokenReceiver struct{} + +// Apply implements deployment.ChangeSetV2. +func (d DeployBrokenReceiver) Apply(e cldf.Environment, config DeployBrokenReceiverConfig) (cldf.ChangesetOutput, error) { + state, err := deployment.LoadOnchainStatesui(e) + if err != nil { + return cldf.ChangesetOutput{}, err + } + + ab := cldf.NewMemoryAddressBook() + seqReports := make([]operations.Report[any, any], 0) + + suiChain := e.BlockChains.SuiChains()[config.SuiChainSelector] + + deps := sui_ops.OpTxDeps{ + Client: suiChain.Client, + Signer: suiChain.Signer, + GetCallOpts: func() *bind.CallOpts { + b := uint64(400_000_000) + return &bind.CallOpts{ + WaitForExecution: true, + GasBudget: &b, + } + }, + SuiRPC: suiChain.URL, + } + + DeployBrokenReceiverOp, err := operations.ExecuteOperation(e.OperationsBundle, ccipops.DeployCCIPBrokenReceiverOp, deps, ccipops.DeployBrokenReceiverInput{ + CCIPPackageId: state[config.SuiChainSelector].CCIPAddress, + McmsPackageId: state[config.SuiChainSelector].MCMSPackageID, + McmsOwner: config.McmsOwner, + }) + if err != nil { + return cldf.ChangesetOutput{}, fmt.Errorf("failed to deploy broken receiver for Sui chain %d: %w", config.SuiChainSelector, err) + } + + seqReports = append(seqReports, []operations.Report[any, any]{DeployBrokenReceiverOp.ToGenericReport()}...) + + return cldf.ChangesetOutput{ + AddressBook: ab, + Reports: seqReports, + }, nil +} + +// VerifyPreconditions implements deployment.ChangeSetV2. +func (d DeployBrokenReceiver) VerifyPreconditions(e cldf.Environment, config DeployBrokenReceiverConfig) error { + return nil +} diff --git a/deployment/changesets/cs_register_broken_receiver.go b/deployment/changesets/cs_register_broken_receiver.go new file mode 100644 index 000000000..d6e73cd73 --- /dev/null +++ b/deployment/changesets/cs_register_broken_receiver.go @@ -0,0 +1,64 @@ +package changesets + +import ( + "fmt" + + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + "github.com/smartcontractkit/chainlink-deployments-framework/operations" + "github.com/smartcontractkit/chainlink-sui/bindings/bind" + sui_ops "github.com/smartcontractkit/chainlink-sui/deployment/ops" + ccipops "github.com/smartcontractkit/chainlink-sui/deployment/ops/ccip" +) + +type RegisterBrokenReceiverConfig struct { + SuiChainSelector uint64 + OwnerCapObjectId string + CCIPObjectRefObjectId string + BrokenReceiverPackageId string +} + +var _ cldf.ChangeSetV2[RegisterBrokenReceiverConfig] = RegisterBrokenReceiver{} + +type RegisterBrokenReceiver struct{} + +// Apply implements deployment.ChangeSetV2. +func (d RegisterBrokenReceiver) Apply(e cldf.Environment, config RegisterBrokenReceiverConfig) (cldf.ChangesetOutput, error) { + ab := cldf.NewMemoryAddressBook() + seqReports := make([]operations.Report[any, any], 0) + + suiChain := e.BlockChains.SuiChains()[config.SuiChainSelector] + + deps := sui_ops.OpTxDeps{ + Client: suiChain.Client, + Signer: suiChain.Signer, + GetCallOpts: func() *bind.CallOpts { + b := uint64(400_000_000) + return &bind.CallOpts{ + WaitForExecution: true, + GasBudget: &b, + } + }, + SuiRPC: suiChain.URL, + } + + RegisterBrokenReceiverOp, err := operations.ExecuteOperation(e.OperationsBundle, ccipops.RegisterBrokenReceiverOp, deps, ccipops.RegisterBrokenReceiverInput{ + OwnerCapObjectId: config.OwnerCapObjectId, + CCIPObjectRefObjectId: config.CCIPObjectRefObjectId, + BrokenReceiverPackageId: config.BrokenReceiverPackageId, + }) + if err != nil { + return cldf.ChangesetOutput{}, fmt.Errorf("failed to register broken receiver for Sui chain %d: %w", config.SuiChainSelector, err) + } + + seqReports = append(seqReports, []operations.Report[any, any]{RegisterBrokenReceiverOp.ToGenericReport()}...) + + return cldf.ChangesetOutput{ + AddressBook: ab, + Reports: seqReports, + }, nil +} + +// VerifyPreconditions implements deployment.ChangeSetV2. +func (d RegisterBrokenReceiver) VerifyPreconditions(e cldf.Environment, config RegisterBrokenReceiverConfig) error { + return nil +} diff --git a/deployment/ops/ccip/op_deploy_broken_receiver.go b/deployment/ops/ccip/op_deploy_broken_receiver.go new file mode 100644 index 000000000..e0df67ebd --- /dev/null +++ b/deployment/ops/ccip/op_deploy_broken_receiver.go @@ -0,0 +1,86 @@ +package ccipops + +import ( + "fmt" + + "github.com/Masterminds/semver/v3" + + cld_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" + + "github.com/smartcontractkit/chainlink-sui/bindings/bind" + "github.com/smartcontractkit/chainlink-sui/contracts" + sui_ops "github.com/smartcontractkit/chainlink-sui/deployment/ops" +) + +// DeployBrokenReceiverObjects holds the objects produced by deploying the broken +// receiver. Unlike the dummy receiver, the broken receiver's init only creates an +// OwnerCap (it has no shared receiver-state object). +type DeployBrokenReceiverObjects struct { + OwnerCapObjectId string +} + +type DeployBrokenReceiverInput struct { + CCIPPackageId string + McmsPackageId string + McmsOwner string +} + +var deployBrokenReceiverHandler = func(b cld_ops.Bundle, deps sui_ops.OpTxDeps, input DeployBrokenReceiverInput) (output sui_ops.OpTxResult[DeployBrokenReceiverObjects], err error) { + opts := deps.GetCallOpts() + opts.Signer = deps.Signer + + signerAddr, err := opts.Signer.GetAddress() + if err != nil { + return sui_ops.OpTxResult[DeployBrokenReceiverObjects]{}, err + } + + // Compile the broken receiver package. Its ccip_receive signature produces a + // normalized ABI containing {"Vector": {"TypeParameter": 0}}, the poison shape + // from report 71024. + artifact, err := bind.CompilePackage(contracts.CCIPBrokenReceiver, map[string]string{ + "ccip": input.CCIPPackageId, + "ccip_broken_receiver": "0x0", + "mcms": input.McmsPackageId, + "mcms_owner": input.McmsOwner, + + "signer": signerAddr, + }, false, deps.SuiRPC) + if err != nil { + return sui_ops.OpTxResult[DeployBrokenReceiverObjects]{}, fmt.Errorf("failed to compile broken receiver package: %w", err) + } + + // Publish the package + packageId, tx, err := bind.PublishPackage( + b.GetContext(), + opts, + deps.Client, + bind.PublishRequest{ + CompiledModules: artifact.Modules, + Dependencies: artifact.Dependencies, + }, + ) + if err != nil { + return sui_ops.OpTxResult[DeployBrokenReceiverObjects]{}, fmt.Errorf("failed to publish broken receiver package: %w", err) + } + + // The init function creates and transfers an OwnerCap to the sender. + ownerCapObjectId, err := bind.FindObjectIdFromPublishTx(*tx, "broken_receiver", "OwnerCap") + if err != nil { + return sui_ops.OpTxResult[DeployBrokenReceiverObjects]{}, fmt.Errorf("failed to find OwnerCap object ID in publish tx: %w", err) + } + + return sui_ops.OpTxResult[DeployBrokenReceiverObjects]{ + Digest: tx.Digest, + PackageId: packageId, + Objects: DeployBrokenReceiverObjects{ + OwnerCapObjectId: ownerCapObjectId, + }, + }, nil +} + +var DeployCCIPBrokenReceiverOp = cld_ops.NewOperation( + sui_ops.NewSuiOperationName("ccip-broken-receiver", "package", "deploy"), + semver.MustParse("0.1.0"), + "Deploys the CCIP broken receiver package (testing only; generic ccip_receive)", + deployBrokenReceiverHandler, +) diff --git a/deployment/ops/ccip/op_register_broken_receiver.go b/deployment/ops/ccip/op_register_broken_receiver.go new file mode 100644 index 000000000..602d6cca6 --- /dev/null +++ b/deployment/ops/ccip/op_register_broken_receiver.go @@ -0,0 +1,76 @@ +package ccipops + +import ( + "fmt" + + "github.com/Masterminds/semver/v3" + + cld_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" + + "github.com/smartcontractkit/chainlink-sui/bindings/bind" + sui_ops "github.com/smartcontractkit/chainlink-sui/deployment/ops" +) + +type RegisterBrokenReceiverInput struct { + OwnerCapObjectId string + CCIPObjectRefObjectId string + BrokenReceiverPackageId string +} + +type RegisterBrokenReceiverObjects struct { + // No specific objects are returned from registration +} + +// registerBrokenReceiverHandler registers the broken receiver with the OffRamp +// receiver registry. There is no generated Go binding for the broken receiver +// (it is a test-only contract), so we build the register_receiver call directly +// against a bound contract. +var registerBrokenReceiverHandler = func(b cld_ops.Bundle, deps sui_ops.OpTxDeps, input RegisterBrokenReceiverInput) (output sui_ops.OpTxResult[RegisterBrokenReceiverObjects], err error) { + contract, err := bind.NewBoundContract(input.BrokenReceiverPackageId, "ccip_broken_receiver", "broken_receiver", deps.Client) + if err != nil { + return sui_ops.OpTxResult[RegisterBrokenReceiverObjects]{}, fmt.Errorf("failed to create broken receiver bound contract: %w", err) + } + + opts := deps.GetCallOpts() + opts.Signer = deps.Signer + + b.Logger.Debugw("Registering broken receiver", "input", input) + + // broken_receiver::register_receiver(owner_cap: &OwnerCap, ref: &mut CCIPObjectRef) + encoded, err := contract.EncodeCallArgsWithGenerics( + "register_receiver", + []string{}, // typeArgs + []string{}, // typeParams + []string{"&OwnerCap", "&mut CCIPObjectRef"}, + []any{ + bind.Object{Id: input.OwnerCapObjectId}, + bind.Object{Id: input.CCIPObjectRefObjectId}, + }, + nil, + ) + if err != nil { + return sui_ops.OpTxResult[RegisterBrokenReceiverObjects]{}, fmt.Errorf("failed to encode broken receiver register_receiver call: %w", err) + } + + tx, err := contract.ExecuteTransaction(b.GetContext(), opts, encoded) + if err != nil { + return sui_ops.OpTxResult[RegisterBrokenReceiverObjects]{}, fmt.Errorf("failed to execute broken receiver registration: %w", err) + } + + b.Logger.Infow("Broken receiver registered", + "brokenReceiverPackageId", input.BrokenReceiverPackageId, + ) + + return sui_ops.OpTxResult[RegisterBrokenReceiverObjects]{ + Digest: tx.Digest, + PackageId: input.BrokenReceiverPackageId, + Objects: RegisterBrokenReceiverObjects{}, + }, nil +} + +var RegisterBrokenReceiverOp = cld_ops.NewOperation( + sui_ops.NewSuiOperationName("ccip-broken-receiver", "broken_receiver", "register_receiver"), + semver.MustParse("0.1.0"), + "Registers the CCIP broken receiver with the receiver registry (testing only)", + registerBrokenReceiverHandler, +) diff --git a/integration-tests/offramp/ccip_receivers_test.go b/integration-tests/offramp/ccip_receivers_test.go new file mode 100644 index 000000000..aec8ad015 --- /dev/null +++ b/integration-tests/offramp/ccip_receivers_test.go @@ -0,0 +1,213 @@ +//go:build integration + +package offramp_test + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/block-vision/sui-go-sdk/models" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-sui/bindings/bind" + "github.com/smartcontractkit/chainlink-sui/bindings/packages/ccip" + "github.com/smartcontractkit/chainlink-sui/bindings/packages/mcms" + "github.com/smartcontractkit/chainlink-sui/contracts" + "github.com/smartcontractkit/chainlink-sui/relayer/chainwriter/ptb/offramp" + rel "github.com/smartcontractkit/chainlink-sui/relayer/signer" + "github.com/smartcontractkit/chainlink-sui/relayer/testutils" +) + +// TestBrokenReceiverABI_NoPanic verifies that the relayer's DecodeParameters handles +// a registered receiver with generic type parameters (TypeParameter in normalized ABI) +// without panicking. This is the core fix for report 71024. +func TestBrokenReceiverABI_NoPanic(t *testing.T) { + lggr := logger.Test(t) + gasBudget := int64(1_000_000_000) + + // Start dedicated Sui node + cmd, err := testutils.StartSuiNode(testutils.CLI) + require.NoError(t, err) + t.Cleanup(func() { + if cmd.Process != nil { + if perr := cmd.Process.Kill(); perr != nil { + t.Logf("Failed to kill Sui node process: %v", perr) + } + } + }) + + time.Sleep(3 * time.Second) + + // Setup signer and fund account + keystoreInstance, accountAddress, publicKeyBytes := testutils.SetupTestSigner(t, context.Background(), lggr, gasBudget) + lggr.Infow("Using account", "address", accountAddress) + + for range 3 { + fundErr := testutils.FundWithFaucet(lggr, "localnet", accountAddress) + require.NoError(t, fundErr) + } + + // Setup client + ptbClient, _, _ := testutils.SetupClients(t, testutils.LocalUrl, keystoreInstance, lggr, gasBudget) + suiClient := ptbClient.GetClient() + + signer := keystoreInstance.GetSuiSigner(context.Background(), fmt.Sprintf("%064x", publicKeyBytes)) + privateKeySigner := rel.NewPrivateKeySigner(signer.PriKey) + + gasBudgetU64 := uint64(gasBudget) + opts := &bind.CallOpts{ + Signer: privateKeySigner, + WaitForExecution: true, + GasBudget: &gasBudgetU64, + } + + mcmsPkg, _, err := mcms.PublishMCMS(context.Background(), opts, suiClient, testutils.LocalUrl) + require.NoError(t, err, "failed to publish MCMS") + mcmsPackageId := mcmsPkg.Address() + + ccipPkg, _, err := ccip.PublishCCIP(context.Background(), opts, suiClient, mcmsPackageId, "0x2", testutils.LocalUrl) + require.NoError(t, err, "failed to publish CCIP") + ccipPackageId := ccipPkg.Address() + + signerAddr, err := privateKeySigner.GetAddress() + require.NoError(t, err) + + brokenReceiverArtifact, err := bind.CompilePackage(contracts.CCIPBrokenReceiver, map[string]string{ + "ccip": ccipPackageId, + "ccip_broken_receiver": "0x0", + "mcms": mcmsPackageId, + "mcms_owner": "0x2", + "signer": signerAddr, + }, false, testutils.LocalUrl) + require.NoError(t, err, "failed to compile broken receiver") + + brokenReceiverPackageId, _, err := bind.PublishPackage(context.Background(), opts, suiClient, bind.PublishRequest{ + CompiledModules: brokenReceiverArtifact.Modules, + Dependencies: brokenReceiverArtifact.Dependencies, + }) + require.NoError(t, err, "failed to publish broken receiver") + lggr.Infow("Published broken receiver", "packageId", brokenReceiverPackageId) + + // Fetch the normalized module — this is the data the relayer would get from chain + normalizedModule, err := suiClient.SuiGetNormalizedMoveModule(context.Background(), models.GetNormalizedMoveModuleRequest{ + Package: brokenReceiverPackageId, + ModuleName: "broken_receiver", + }) + require.NoError(t, err, "failed to get normalized module") + + functionSig, ok := normalizedModule.ExposedFunctions["ccip_receive"] + require.True(t, ok, "ccip_receive not found in normalized module") + + funcMap, ok := functionSig.(map[string]any) + require.True(t, ok, "function signature is not a map") + + params, ok := funcMap["parameters"].([]any) + require.True(t, ok, "parameters field is not an array") + require.Greater(t, len(params), 0) + + lggr.Infow("Broken receiver normalized ABI", "parameters", params) + + // The critical assertion: DecodeParameters must not panic and must return an error. + // Before the fix this panicked with: + // interface conversion: interface {} is float64, not map[string]interface {} + assert.NotPanics(t, func() { + result, decodeErr := offramp.DecodeParameters(lggr, funcMap, "parameters") + assert.Error(t, decodeErr, "DecodeParameters should return error for TypeParameter ABI") + assert.Nil(t, result) + assert.Contains(t, decodeErr.Error(), "TypeParameter") + }) +} + +// TestValidReceiverABI_DecodesSuccessfully verifies that the fix does not break +// legitimate receivers. The dummy receiver's ccip_receive has a concrete signature +// (no generics) and DecodeParameters must succeed on its normalized ABI. +func TestValidReceiverABI_DecodesSuccessfully(t *testing.T) { + lggr := logger.Test(t) + gasBudget := int64(1_000_000_000) + + // Start dedicated Sui node + cmd, err := testutils.StartSuiNode(testutils.CLI) + require.NoError(t, err) + t.Cleanup(func() { + if cmd.Process != nil { + if perr := cmd.Process.Kill(); perr != nil { + t.Logf("Failed to kill Sui node process: %v", perr) + } + } + }) + + time.Sleep(3 * time.Second) + + keystoreInstance, accountAddress, publicKeyBytes := testutils.SetupTestSigner(t, context.Background(), lggr, gasBudget) + lggr.Infow("Using account", "address", accountAddress) + + for range 3 { + fundErr := testutils.FundWithFaucet(lggr, "localnet", accountAddress) + require.NoError(t, fundErr) + } + + ptbClient, _, _ := testutils.SetupClients(t, testutils.LocalUrl, keystoreInstance, lggr, gasBudget) + suiClient := ptbClient.GetClient() + + signer := keystoreInstance.GetSuiSigner(context.Background(), fmt.Sprintf("%064x", publicKeyBytes)) + privateKeySigner := rel.NewPrivateKeySigner(signer.PriKey) + + gasBudgetU64 := uint64(gasBudget) + opts := &bind.CallOpts{ + Signer: privateKeySigner, + WaitForExecution: true, + GasBudget: &gasBudgetU64, + } + + mcmsPkg, _, err := mcms.PublishMCMS(context.Background(), opts, suiClient, testutils.LocalUrl) + require.NoError(t, err, "failed to publish MCMS") + mcmsPackageId := mcmsPkg.Address() + + ccipPkg, _, err := ccip.PublishCCIP(context.Background(), opts, suiClient, mcmsPackageId, "0x2", testutils.LocalUrl) + require.NoError(t, err, "failed to publish CCIP") + ccipPackageId := ccipPkg.Address() + + signerAddr, err := privateKeySigner.GetAddress() + require.NoError(t, err) + + dummyReceiverArtifact, err := bind.CompilePackage(contracts.CCIPDummyReceiver, map[string]string{ + "ccip": ccipPackageId, + "ccip_dummy_receiver": "0x0", + "mcms": mcmsPackageId, + "mcms_owner": "0x2", + "signer": signerAddr, + }, false, testutils.LocalUrl) + require.NoError(t, err, "failed to compile dummy receiver") + + dummyReceiverPackageId, _, err := bind.PublishPackage(context.Background(), opts, suiClient, bind.PublishRequest{ + CompiledModules: dummyReceiverArtifact.Modules, + Dependencies: dummyReceiverArtifact.Dependencies, + }) + require.NoError(t, err, "failed to publish dummy receiver") + lggr.Infow("Published dummy receiver", "packageId", dummyReceiverPackageId) + + // Fetch the normalized module + normalizedModule, err := suiClient.SuiGetNormalizedMoveModule(context.Background(), models.GetNormalizedMoveModuleRequest{ + Package: dummyReceiverPackageId, + ModuleName: "dummy_receiver", + }) + require.NoError(t, err) + + functionSig, ok := normalizedModule.ExposedFunctions["ccip_receive"] + require.True(t, ok, "ccip_receive not found") + + funcMap, ok := functionSig.(map[string]any) + require.True(t, ok) + + // DecodeParameters must succeed for valid receivers + result, err := offramp.DecodeParameters(lggr, funcMap, "parameters") + require.NoError(t, err, "DecodeParameters should succeed for dummy receiver ABI") + require.NotNil(t, result) + require.Greater(t, len(result), 0, "should decode at least one parameter type") + + lggr.Infow("Dummy receiver decoded parameter types", "paramTypes", result) +} diff --git a/relayer/chainreader/chainreader_util/hasher.go b/relayer/chainreader/chainreader_util/hasher.go index f35d456e0..6bf37d4ab 100644 --- a/relayer/chainreader/chainreader_util/hasher.go +++ b/relayer/chainreader/chainreader_util/hasher.go @@ -276,6 +276,182 @@ func encodeBytes(b []byte) []byte { return result } +// MessageHasherV2 computes the V2 leaf hash which includes receiver_object_ids. +// This matches calculate_message_hash_v2 in offramp.move. +type MessageHasherV2 struct { + lggr logger.Logger +} + +func NewMessageHasherV2(lggr logger.Logger) *MessageHasherV2 { + return &MessageHasherV2{lggr: lggr} +} + +func (h *MessageHasherV2) Hash(ctx context.Context, report *codec.ExecutionReport, onRampAddress []byte, receiverObjectIds [][32]byte) ([32]byte, error) { + rampTokenAmounts := make([]any2SuiTokenTransfer, len(report.Message.TokenAmounts)) + for i, rta := range report.Message.TokenAmounts { + var destTokenAddress [32]byte + destTokenBytes, err := hex.DecodeString("0x" + string(rta.DestTokenAddress)) + if err != nil { + return [32]byte{}, fmt.Errorf("failed to decode dest token address: %w", err) + } + if len(destTokenBytes) != 32 { + return [32]byte{}, fmt.Errorf("invalid dest token address length: expected 32, got %d", len(destTokenBytes)) + } + copy(destTokenAddress[:], destTokenBytes) + + rampTokenAmounts[i] = any2SuiTokenTransfer{ + SourcePoolAddress: rta.SourcePoolAddress, + DestTokenAddress: destTokenAddress, + DestGasAmount: rta.DestGasAmount, + ExtraData: rta.ExtraData, + Amount: rta.Amount, + } + } + + metaDataHash, err := computeMetadataHash( + report.SourceChainSelector, + report.Message.Header.DestChainSelector, + onRampAddress, + ) + if err != nil { + return [32]byte{}, fmt.Errorf("compute metadata hash: %w", err) + } + + var messageID [32]byte + copy(messageID[:], report.Message.Header.MessageID) + + var receiverAddress [32]byte + receiverBytes, err := hex.DecodeString("0x" + string(report.Message.Receiver)) + if err != nil { + return [32]byte{}, fmt.Errorf("failed to decode receiver address: %w", err) + } + if len(receiverBytes) != 32 { + return [32]byte{}, fmt.Errorf("invalid receiver address length: expected 32, got %d", len(receiverBytes)) + } + copy(receiverAddress[:], receiverBytes) + + msgHash, err := computeMessageDataHashV2( + metaDataHash, + messageID, + receiverAddress, + report.Message.Header.SequenceNumber, + report.Message.GasLimit, + [32]byte(report.Message.TokenReceiver), + report.Message.Header.Nonce, + report.Message.Sender, + report.Message.Data, + rampTokenAmounts, + receiverObjectIds, + ) + if err != nil { + return [32]byte{}, fmt.Errorf("compute message hash v2: %w", err) + } + + return msgHash, nil +} + +func computeMessageDataHashV2( + metadataHash [32]byte, + messageID [32]byte, + receiver [32]byte, + sequenceNumber uint64, + gasLimit *big.Int, + tokenReceiver [32]byte, + nonce uint64, + sender []byte, + data []byte, + tokenAmounts []any2SuiTokenTransfer, + receiverObjectIds [][32]byte, +) ([32]byte, error) { + uint64Type, err := abi.NewType("uint64", "", nil) + if err != nil { + return [32]byte{}, fmt.Errorf("failed to create uint64 ABI type: %w", err) + } + + uint256Type, err := abi.NewType("uint256", "", nil) + if err != nil { + return [32]byte{}, fmt.Errorf("failed to create uint256 ABI type: %w", err) + } + + bytes32Type, err := abi.NewType("bytes32", "", nil) + if err != nil { + return [32]byte{}, fmt.Errorf("failed to create bytes32 ABI type: %w", err) + } + + headerArgs := abi.Arguments{ + {Type: bytes32Type}, + {Type: bytes32Type}, + {Type: uint64Type}, + {Type: uint256Type}, + {Type: bytes32Type}, + {Type: uint64Type}, + } + headerEncoded, err := headerArgs.Pack(messageID, receiver, sequenceNumber, gasLimit, tokenReceiver, nonce) + if err != nil { + return [32]byte{}, err + } + headerHash := crypto.Keccak256Hash(headerEncoded) + + senderHash := crypto.Keccak256Hash(sender) + dataHash := crypto.Keccak256Hash(data) + + var tokenHashData []byte + encodedLen, err := encodeUint256(big.NewInt(int64(len(tokenAmounts)))) + if err != nil { + return [32]byte{}, fmt.Errorf("failed to encode token length: %w", err) + } + tokenHashData = append(tokenHashData, encodedLen...) + for _, token := range tokenAmounts { + tokenHashData = append(tokenHashData, encodeBytes(token.SourcePoolAddress)...) + tokenHashData = append(tokenHashData, token.DestTokenAddress[:]...) + tokenHashData = append(tokenHashData, encodeUint32(token.DestGasAmount)...) + tokenHashData = append(tokenHashData, encodeBytes(token.ExtraData)...) + encodedAmount, err := encodeUint256(token.Amount) + if err != nil { + return [32]byte{}, fmt.Errorf("failed to encode token amount: %w", err) + } + tokenHashData = append(tokenHashData, encodedAmount...) + } + tokenAmountsHash := crypto.Keccak256Hash(tokenHashData) + + // V2: compute objectIdsHash = keccak256(encode_u256(len) || encode_address(id[0]) || ...) + var objectIdsHashData []byte + encodedObjLen, err := encodeUint256(big.NewInt(int64(len(receiverObjectIds)))) + if err != nil { + return [32]byte{}, fmt.Errorf("failed to encode object ids length: %w", err) + } + objectIdsHashData = append(objectIdsHashData, encodedObjLen...) + for _, id := range receiverObjectIds { + objectIdsHashData = append(objectIdsHashData, id[:]...) + } + objectIdsHash := crypto.Keccak256Hash(objectIdsHashData) + + finalArgs := abi.Arguments{ + {Type: bytes32Type}, // LEAF_DOMAIN_SEPARATOR + {Type: bytes32Type}, // metadataHash + {Type: bytes32Type}, // headerHash + {Type: bytes32Type}, // senderHash + {Type: bytes32Type}, // dataHash + {Type: bytes32Type}, // tokenAmountsHash + {Type: bytes32Type}, // objectIdsHash (V2 addition) + } + + finalEncoded, err := finalArgs.Pack( + leafDomainSeparator, + metadataHash, + headerHash, + senderHash, + dataHash, + tokenAmountsHash, + objectIdsHash, + ) + if err != nil { + return [32]byte{}, err + } + + return crypto.Keccak256Hash(finalEncoded), nil +} + func keccak256Fixed(in []byte) [32]byte { hash := sha3.NewLegacyKeccak256() // Note this Keccak256 cannot error https://github.com/golang/crypto/blob/master/sha3/sha3.go#L126 diff --git a/relayer/chainreader/chainreader_util/hasher_test.go b/relayer/chainreader/chainreader_util/hasher_test.go index 07abf25a1..b0c9bb1e4 100644 --- a/relayer/chainreader/chainreader_util/hasher_test.go +++ b/relayer/chainreader/chainreader_util/hasher_test.go @@ -238,6 +238,125 @@ func TestMessageHasherV1_MessageHash(t *testing.T) { }) } +func TestMessageHasherV2_DifferentObjectIds_DifferentHash(t *testing.T) { + messageID := hexTo32Bytes(t, "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef") + receiver := hexTo32Bytes(t, "0000000000000000000000000000000000000000000000000000000000001234") + sender, err := hex.DecodeString("8765432109fedcba8765432109fedcba87654321") + require.NoError(t, err) + tokenReceiver := hexTo32Bytes(t, "0000000000000000000000000000000000000000000000000000000000000000") + data := []byte("sample message data") + gasLimit := big.NewInt(500000) + sequenceNumber := uint64(42) + nonce := uint64(0) + + metadataHash, err := computeMetadataHash(uint64(123456789), uint64(987654321), []byte("source-onramp-address")) + require.NoError(t, err) + + objectIdA := hexTo32Bytes(t, "0000000000000000000000000000000000000000000000000000000000001111") + objectIdB := hexTo32Bytes(t, "0000000000000000000000000000000000000000000000000000000000002222") + + t.Run("hash with object IDs [A] differs from hash with object IDs [B]", func(t *testing.T) { + hashA, err := computeMessageDataHashV2( + metadataHash, messageID, receiver, sequenceNumber, gasLimit, tokenReceiver, nonce, + sender, data, []any2SuiTokenTransfer{}, [][32]byte{objectIdA}, + ) + require.NoError(t, err) + + hashB, err := computeMessageDataHashV2( + metadataHash, messageID, receiver, sequenceNumber, gasLimit, tokenReceiver, nonce, + sender, data, []any2SuiTokenTransfer{}, [][32]byte{objectIdB}, + ) + require.NoError(t, err) + + assert.NotEqual(t, hashA, hashB) + }) + + t.Run("hash with empty object IDs differs from hash with [A]", func(t *testing.T) { + hashEmpty, err := computeMessageDataHashV2( + metadataHash, messageID, receiver, sequenceNumber, gasLimit, tokenReceiver, nonce, + sender, data, []any2SuiTokenTransfer{}, [][32]byte{}, + ) + require.NoError(t, err) + + hashWithA, err := computeMessageDataHashV2( + metadataHash, messageID, receiver, sequenceNumber, gasLimit, tokenReceiver, nonce, + sender, data, []any2SuiTokenTransfer{}, [][32]byte{objectIdA}, + ) + require.NoError(t, err) + + assert.NotEqual(t, hashEmpty, hashWithA) + }) + + t.Run("hash with [A,B] differs from [B,A] (order matters)", func(t *testing.T) { + hashAB, err := computeMessageDataHashV2( + metadataHash, messageID, receiver, sequenceNumber, gasLimit, tokenReceiver, nonce, + sender, data, []any2SuiTokenTransfer{}, [][32]byte{objectIdA, objectIdB}, + ) + require.NoError(t, err) + + hashBA, err := computeMessageDataHashV2( + metadataHash, messageID, receiver, sequenceNumber, gasLimit, tokenReceiver, nonce, + sender, data, []any2SuiTokenTransfer{}, [][32]byte{objectIdB, objectIdA}, + ) + require.NoError(t, err) + + assert.NotEqual(t, hashAB, hashBA) + }) + + t.Run("V2 hash with empty object IDs differs from V1 hash (different structure)", func(t *testing.T) { + hashV1, err := computeMessageDataHash( + metadataHash, messageID, receiver, sequenceNumber, gasLimit, tokenReceiver, nonce, + sender, data, []any2SuiTokenTransfer{}, + ) + require.NoError(t, err) + + hashV2Empty, err := computeMessageDataHashV2( + metadataHash, messageID, receiver, sequenceNumber, gasLimit, tokenReceiver, nonce, + sender, data, []any2SuiTokenTransfer{}, [][32]byte{}, + ) + require.NoError(t, err) + + assert.NotEqual(t, hashV1, hashV2Empty, + "V2 hash must differ from V1 even with empty objectIds because V2 includes the objectIdsHash term") + }) +} + +func TestMessageHasherV2_Deterministic(t *testing.T) { + messageID := hexTo32Bytes(t, "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef") + receiver := hexTo32Bytes(t, "0000000000000000000000000000000000000000000000000000000000001234") + sender, err := hex.DecodeString("8765432109fedcba8765432109fedcba87654321") + require.NoError(t, err) + tokenReceiver := hexTo32Bytes(t, "0000000000000000000000000000000000000000000000000000000000005678") + data := []byte("test payload") + gasLimit := big.NewInt(200000) + + metadataHash, err := computeMetadataHash(uint64(1000), uint64(2000), []byte("onramp")) + require.NoError(t, err) + + objectId := hexTo32Bytes(t, "0000000000000000000000000000000000000000000000000000000000aabbcc") + + hash1, err := computeMessageDataHashV2( + metadataHash, messageID, receiver, uint64(1), gasLimit, tokenReceiver, uint64(0), + sender, data, []any2SuiTokenTransfer{}, [][32]byte{objectId}, + ) + require.NoError(t, err) + + hash2, err := computeMessageDataHashV2( + metadataHash, messageID, receiver, uint64(1), gasLimit, tokenReceiver, uint64(0), + sender, data, []any2SuiTokenTransfer{}, [][32]byte{objectId}, + ) + require.NoError(t, err) + + assert.Equal(t, hash1, hash2, "same inputs must produce same hash") + + // Cross-language parity: this expected value matches the Move test + // test_calculate_message_hash_v2_parity in offramp_test.move + expectedHashHex := "1463b1b58f28f74dd73d4447da139d065051ddbb292549847a8c315d19148fc1" + actualHashHex := hex.EncodeToString(hash1[:]) + assert.Equal(t, expectedHashHex, actualHashHex, + "Go V2 hash must match Move calculate_message_hash_v2 for identical inputs") +} + // Helper function to convert hex string to [32]byte array func hexTo32Bytes(t *testing.T, hexStr string) [32]byte { bytes, err := hex.DecodeString(hexStr) diff --git a/relayer/chainwriter/ptb/offramp/execute.go b/relayer/chainwriter/ptb/offramp/execute.go index 56636229c..3267325f2 100644 --- a/relayer/chainwriter/ptb/offramp/execute.go +++ b/relayer/chainwriter/ptb/offramp/execute.go @@ -8,6 +8,7 @@ import ( "context" "encoding/hex" "fmt" + "math/big" "strings" "github.com/block-vision/sui-go-sdk/models" @@ -56,7 +57,7 @@ func DecodeOffRampExecCallArgs(args map[string]any) (*SuiOffRampExecCallArgs, er return offrampArgs, nil } -// BuildOffRampExecutePTB builds the PTB for the OffRampExecute operation +// BuildOffRampExecutePTB builds the PTB for the OffRampExecute operation (V1). func BuildOffRampExecutePTB( ctx context.Context, lggr logger.Logger, @@ -190,6 +191,127 @@ func BuildOffRampExecutePTB( return nil } +// BuildOffRampExecutePTBV2 builds the PTB for the OffRampExecute operation using V2 functions +// that enforce receiver object binding at the protocol level. The V2 leaf hash includes +// receiver_object_ids, and extract_any2sui_message_v2 verifies them before message delivery. +func BuildOffRampExecutePTBV2( + ctx context.Context, + lggr logger.Logger, + ptbClient client.SuiPTBClient, + ptb *transaction.Transaction, + args config.Arguments, + signerAddress string, + addressMappings OffRampAddressMappings, +) (err error) { + sdkClient := ptbClient.GetClient() + offrampArgs, err := DecodeOffRampExecCallArgs(args.Args) + if err != nil { + return fmt.Errorf("failed to decode args for offramp execute PTB v2: %w", err) + } + + coinMetadataAddresses := make([]string, 0) + messages := make([]ccipocr3.Message, 0) + + for _, report := range offrampArgs.Info.AbstractReports { + for _, message := range report.Messages { + messages = append(messages, message) + for _, tokenAmount := range message.TokenAmounts { + destTokenAddress := "0x" + hex.EncodeToString(tokenAmount.DestTokenAddress) + lggr.Debugw("found token metadata address", "address", destTokenAddress) + coinMetadataAddresses = append(coinMetadataAddresses, destTokenAddress) + } + } + } + + devInspectSigner := signer.NewDevInspectSigner(signerAddress) + callOpts := &bind.CallOpts{ + Signer: devInspectSigner, + WaitForExecution: true, + } + + latestOfframpPackageId, err := ptbClient.GetLatestPackageId(ctx, addressMappings.OffRampPackageId, "offramp") + if err != nil { + return err + } + + latestCcipPackageId, err := ptbClient.GetLatestPackageId(ctx, addressMappings.CcipPackageId, "state_object") + if err != nil { + return err + } + + addressMappings.OffRampPackageId = latestOfframpPackageId + addressMappings.CcipPackageId = latestCcipPackageId + + offrampPkg, err := offramp.NewOfframp(addressMappings.OffRampPackageId, sdkClient) + if err != nil { + return err + } + offrampContract := offrampPkg.Offramp().(*module_offramp.OfframpContract) + offrampEncoder := offrampContract.Encoder() + + encodedInitExecute, err := offrampEncoder.InitExecuteV2( + bind.Object{Id: addressMappings.CcipObjectRef}, + bind.Object{Id: addressMappings.OffRampState}, + bind.Object{Id: addressMappings.ClockObject}, + [][]byte{ + offrampArgs.ReportContext[0][:], + offrampArgs.ReportContext[1][:], + }, + offrampArgs.Report, + ) + if err != nil { + return fmt.Errorf("failed to encode move call (init_execute_v2) using bindings: %w", err) + } + + initExecuteResult, err := offrampContract.AppendPTB(ctx, callOpts, ptb, encodedInitExecute) + if err != nil { + return fmt.Errorf("failed to build PTB (init_execute_v2) using bindings: %w", err) + } + + tokenPoolCommandsResults, err := ProcessTokenPools( + ctx, + lggr, + ptbClient, + ptb, + &addressMappings, + callOpts, + coinMetadataAddresses, + initExecuteResult, + ) + if err != nil { + return err + } + + lggr.Debugw("finished processing token pool calls", "tokenPoolCalls", tokenPoolCommandsResults) + + _, err = ProcessReceiversV2( + ctx, + lggr, + ptbClient, + ptb, + messages, + &addressMappings, + callOpts, + initExecuteResult, + offrampArgs.ExtraData.ExtraArgsDecoded, + ) + if err != nil { + return err + } + + encodedFinishExecute, err := offrampEncoder.FinishExecuteV2WithArgs(bind.Object{Id: addressMappings.CcipObjectRef}, bind.Object{Id: addressMappings.OffRampState}, initExecuteResult) + if err != nil { + return fmt.Errorf("failed to encode move call (finish_execute_v2) using bindings: %w", err) + } + + _, err = offrampContract.AppendPTB(ctx, callOpts, ptb, encodedFinishExecute) + if err != nil { + return fmt.Errorf("failed to build PTB (finish_execute_v2) using bindings: %w", err) + } + + return nil +} + func ProcessTokenPools( ctx context.Context, lggr logger.Logger, @@ -343,7 +465,6 @@ func ProcessReceivers( ) ([]transaction.Argument, error) { sdkClient := ptbClient.GetClient() - // Create a receiver binding interface to filter out non-registered receivers receiverRegistryPkg, err := receiver_registry.NewReceiverRegistry(addressMappings.CcipPackageId, sdkClient) if err != nil { return nil, err @@ -351,40 +472,47 @@ func ProcessReceivers( receiverRegistryDevInspect := receiverRegistryPkg.DevInspect() receiverCommandsResults := make([]transaction.Argument, 0) - // Generate receiver call commands for _, message := range messages { - // If there is no receiver, skip this message if len(message.Receiver) == 0 || message.Receiver == nil { lggr.Errorw("unexpected nil or zero length receiver, skipping message in offramp execution...", "message", message) continue } - // Check if receiver is a zero address (0x0....0 // 32 bytes of 0) if bytes.Equal(message.Receiver, codec.AccountZero) { lggr.Debugw("receiver is zero address, skipping message in offramp execution...", "message", message) continue } - // Parse the receiver address into a hex string + // Mirror on-chain gating: skip receiver call when on-chain would not populate the message. + // On-chain: has_valid_message_receiver = (!data.is_empty() || gas_limit != 0) && is_registered_receiver + if !needsAppDelivery(message, extraArgs) { + lggr.Debugw("message has no data and zero gas limit, skipping receiver call", + "receiver", hex.EncodeToString(message.Receiver)) + continue + } + receiverPackageId := "0x" + hex.EncodeToString(message.Receiver) isRegistered, err := receiverRegistryDevInspect.IsRegisteredReceiver(ctx, callOpts, bind.Object{Id: addressMappings.CcipObjectRef}, receiverPackageId) if err != nil { return nil, fmt.Errorf("failed to check if receiver is registered in offramp execution: %w", err) } - // If the receiver is not registered, fail the entire execution if !isRegistered { - return nil, fmt.Errorf("receiver is not registered in offramp execution: %s", message.Receiver) + lggr.Warnw("receiver not registered, skipping receiver call (on-chain will not populate message)", + "receiver", receiverPackageId) + continue } receiverConfig, err := receiverRegistryDevInspect.GetReceiverConfig(ctx, callOpts, bind.Object{Id: addressMappings.CcipObjectRef}, receiverPackageId) if err != nil { + // RPC/network error — propagate so the caller can retry later. return nil, fmt.Errorf("failed to get receiver config in offramp execution: %w", err) } receiverNormalizedModule, err := ptbClient.GetNormalizedModule(ctx, receiverPackageId, receiverConfig.ModuleName) if err != nil { - return nil, fmt.Errorf("failed to get normalized module for token pool: %w", err) + // RPC/network error — propagate so the caller can retry later. + return nil, fmt.Errorf("failed to get normalized module for receiver: %w", err) } receiverCommandResult, err := AppendPTBCommandForReceiver( @@ -403,7 +531,18 @@ func ProcessReceivers( extraArgs, ) if err != nil { - return nil, err + // Permanent failure (e.g. unsupported ABI with TypeParameter/generics). + // Skip the receiver leg and let the PTB be submitted without it. + // On-chain, populate_message will have set ReceiverParams.message to Some, + // so finish_execute → deconstruct_receiver_params will abort with ECCIPReceiveFailed. + // The transactions indexer observes this failed PTB and creates a synthetic + // ExecutionStateChanged(FAILURE) event, causing the DON to stop retrying. + // The message remains UNTOUCHED on-chain (atomic rollback) and available + // for manually_init_execute once the receiver is fixed or unregistered. + lggr.Errorw("skipping receiver command due to permanent build failure; PTB will fail on-chain", + "receiver", receiverPackageId, + "error", err) + continue } receiverCommandsResults = append(receiverCommandsResults, *receiverCommandResult) } @@ -411,6 +550,49 @@ func ProcessReceivers( return receiverCommandsResults, nil } +func needsAppDelivery(message ccipocr3.Message, extraArgs map[string]any) bool { + if len(message.Data) > 0 { + return true + } + if val, ok := extraArgs["gasLimit"]; ok { + switch gl := val.(type) { + case *big.Int: + return gl != nil && gl.Sign() > 0 + case uint64: + return gl > 0 + } + } + return false +} + +// extractReceiverObjectIdStrings parses receiver_object_ids from the extraArgs map +// into hex-prefixed address strings. Returns an empty slice if the key is missing or +// contains no valid entries. +func extractReceiverObjectIdStrings(extraArgs map[string]any) []string { + raw, ok := extraArgs["receiverObjectIds"] + if !ok { + return []string{} + } + + var result []string + switch vals := raw.(type) { + case [][]byte: + for _, v := range vals { + result = append(result, "0x"+hex.EncodeToString(v)) + } + case []any: + for _, v := range vals { + if b, ok := v.([]byte); ok { + result = append(result, "0x"+hex.EncodeToString(b)) + } + } + } + if result == nil { + return []string{} + } + return result +} + func AppendPTBCommandForReceiver( ctx context.Context, lggr logger.Logger, @@ -461,12 +643,12 @@ func AppendPTBCommandForReceiver( nil, ) if err != nil { - return nil, fmt.Errorf("failed to encode get_token_param_data call: %w", err) + return nil, fmt.Errorf("failed to encode extract_any2sui_message call: %w", err) } extractedAny2SuiMessageResult, err := offrampStateHelperContract.AppendPTB(ctx, callOpts, ptb, encodedAny2SuiExtractCall) if err != nil { - return nil, fmt.Errorf("failed to build PTB (get_token_param_data) using bindings: %w", err) + return nil, fmt.Errorf("failed to build PTB (extract_any2sui_message) using bindings: %w", err) } typeArgsList = []string{} @@ -486,38 +668,214 @@ func AppendPTBCommandForReceiver( // Figure out the parameter types from the normalized module of the token pool paramTypes, err = DecodeParameters(lggr, functionSignature.(map[string]any), "parameters") if err != nil { - return nil, fmt.Errorf("failed to decode parameters for token pool function: %w", err) + return nil, fmt.Errorf("failed to decode parameters for receiver function: %w", err) } lggr.Debugw("calling receiver", "paramTypes", paramTypes, "paramValues", paramValues) - // Append extra args to the paramValues for the receiver call. - receiverObjectIds, ok := extraArgs["receiverObjectIds"] - if !ok { - return nil, fmt.Errorf("missing extra args for receiver function not found in module (%s)", functionName) + for _, objectId := range extractReceiverObjectIdStrings(extraArgs) { + paramValues = append(paramValues, bind.Object{Id: objectId}) } - // note: we cannot expect receiverObjectIds to be [][]byte, so check for []any type - var extraArgsValues [][]byte - switch vals := receiverObjectIds.(type) { - case [][]byte: - extraArgsValues = vals - case []any: - for _, v := range vals { - b, ok := v.([]byte) - if !ok { - lggr.Error("unexpected element type in receiverObjectIds", "type", fmt.Sprintf("%T", v)) - continue - } - extraArgsValues = append(extraArgsValues, b) + encodedReceiverCall, err := boundReceiverContract.EncodeCallArgsWithGenerics( + functionName, + typeArgsList, + typeParamsList, + paramTypes, + paramValues, + nil, + ) + if err != nil { + return nil, fmt.Errorf("failed to encode receiver call: %w", err) + } + + receiverCommandResult, err := boundReceiverContract.AppendPTB(ctx, callOpts, ptb, encodedReceiverCall) + if err != nil { + return nil, fmt.Errorf("failed to build PTB (receiver call) using bindings: %w", err) + } + + return receiverCommandResult, nil +} + +// ProcessReceiversV2 processes receiver calls using V2 protocol with object binding enforcement. +// It calls extract_any2sui_message_v2 (which verifies receiver_object_ids match the committed values) +// before invoking the receiver's ccip_receive function. +func ProcessReceiversV2( + ctx context.Context, + lggr logger.Logger, + ptbClient client.SuiPTBClient, + ptb *transaction.Transaction, + messages []ccipocr3.Message, + addressMappings *OffRampAddressMappings, + callOpts *bind.CallOpts, + receiverParams *transaction.Argument, + extraArgs map[string]any, +) ([]transaction.Argument, error) { + sdkClient := ptbClient.GetClient() + + receiverRegistryPkg, err := receiver_registry.NewReceiverRegistry(addressMappings.CcipPackageId, sdkClient) + if err != nil { + return nil, err + } + receiverRegistryDevInspect := receiverRegistryPkg.DevInspect() + + receiverCommandsResults := make([]transaction.Argument, 0) + for _, message := range messages { + if len(message.Receiver) == 0 || message.Receiver == nil { + lggr.Errorw("unexpected nil or zero length receiver, skipping message in offramp execution...", "message", message) + continue + } + + if bytes.Equal(message.Receiver, codec.AccountZero) { + lggr.Debugw("receiver is zero address, skipping message in offramp execution...", "message", message) + continue + } + + if !needsAppDelivery(message, extraArgs) { + lggr.Debugw("message has no data and zero gas limit, skipping receiver call", + "receiver", hex.EncodeToString(message.Receiver)) + continue } - default: - lggr.Error("unexpected receiverObjectIds type", "type", fmt.Sprintf("%T", receiverObjectIds)) + + receiverPackageId := "0x" + hex.EncodeToString(message.Receiver) + + isRegistered, err := receiverRegistryDevInspect.IsRegisteredReceiver(ctx, callOpts, bind.Object{Id: addressMappings.CcipObjectRef}, receiverPackageId) + if err != nil { + return nil, fmt.Errorf("failed to check if receiver is registered in offramp execution: %w", err) + } + if !isRegistered { + lggr.Warnw("receiver not registered, skipping receiver call (on-chain will not populate message)", + "receiver", receiverPackageId) + continue + } + + receiverConfig, err := receiverRegistryDevInspect.GetReceiverConfig(ctx, callOpts, bind.Object{Id: addressMappings.CcipObjectRef}, receiverPackageId) + if err != nil { + return nil, fmt.Errorf("failed to get receiver config in offramp execution: %w", err) + } + + receiverNormalizedModule, err := ptbClient.GetNormalizedModule(ctx, receiverPackageId, receiverConfig.ModuleName) + if err != nil { + return nil, fmt.Errorf("failed to get normalized module for receiver: %w", err) + } + + receiverCommandResult, err := AppendPTBCommandForReceiverV2( + ctx, + lggr, + sdkClient, + ptb, + callOpts, + receiverPackageId, + receiverConfig.ModuleName, + "ccip_receive", + addressMappings, + message.Header.MessageID, + &receiverNormalizedModule, + receiverParams, + extraArgs, + ) + if err != nil { + lggr.Errorw("skipping receiver command due to permanent build failure; PTB will fail on-chain", + "receiver", receiverPackageId, + "error", err) + continue + } + receiverCommandsResults = append(receiverCommandsResults, *receiverCommandResult) } - for _, value := range extraArgsValues { - objectId := hex.EncodeToString(value) - paramValues = append(paramValues, bind.Object{Id: "0x" + objectId}) + return receiverCommandsResults, nil +} + +// AppendPTBCommandForReceiverV2 builds the receiver call using V2 extract which enforces +// receiver_object_ids binding at the protocol level. +func AppendPTBCommandForReceiverV2( + ctx context.Context, + lggr logger.Logger, + sdkClient sui.ISuiAPI, + ptb *transaction.Transaction, + callOpts *bind.CallOpts, + packageId string, + moduleId string, + functionName string, + addressMappings *OffRampAddressMappings, + messageID [32]byte, + normalizedModule *models.GetNormalizedMoveModuleResponse, + receiverParams *transaction.Argument, + extraArgs map[string]any, +) (*transaction.Argument, error) { + boundReceiverContract, err := bind.NewBoundContract(packageId, packageId, moduleId, sdkClient) + if err != nil { + return nil, fmt.Errorf("failed to create receiver bound contract when appending PTB command: %w", err) + } + + offrampStateHelperContract, err := bind.NewBoundContract( + addressMappings.CcipPackageId, + addressMappings.CcipPackageId, + "offramp_state_helper", + sdkClient, + ) + if err != nil { + return nil, fmt.Errorf("failed to create offramp state helper bound contract when appending PTB command: %w", err) + } + + receiverObjectIdStrings := extractReceiverObjectIdStrings(extraArgs) + + // Call extract_any2sui_message_v2 with receiver_object_ids for protocol-level binding enforcement + typeArgsList := []string{} + typeParamsList := []string{} + paramTypes := []string{ + "&mut object", + "vector", + } + paramValues := []any{ + receiverParams, + receiverObjectIdStrings, + } + + lggr.Debugw("calling extract_any2sui_message_v2", + "receiverObjectIds", receiverObjectIdStrings) + + encodedAny2SuiExtractCall, err := offrampStateHelperContract.EncodeCallArgsWithGenerics( + "extract_any2sui_message_v2", + typeArgsList, + typeParamsList, + paramTypes, + paramValues, + nil, + ) + if err != nil { + return nil, fmt.Errorf("failed to encode extract_any2sui_message_v2 call: %w", err) + } + + extractedAny2SuiMessageResult, err := offrampStateHelperContract.AppendPTB(ctx, callOpts, ptb, encodedAny2SuiExtractCall) + if err != nil { + return nil, fmt.Errorf("failed to build PTB (extract_any2sui_message_v2) using bindings: %w", err) + } + + // Build the receiver function call + typeArgsList = []string{} + typeParamsList = []string{} + paramValues = []any{ + messageID, + bind.Object{Id: addressMappings.CcipObjectRef}, + extractedAny2SuiMessageResult, + } + + functionSignature, ok := normalizedModule.ExposedFunctions[functionName] + if !ok { + return nil, fmt.Errorf("missing function signature for receiver function not found in module (%s)", functionName) + } + + paramTypes, err = DecodeParameters(lggr, functionSignature.(map[string]any), "parameters") + if err != nil { + return nil, fmt.Errorf("failed to decode parameters for receiver function: %w", err) + } + + lggr.Debugw("calling receiver", "paramTypes", paramTypes, "paramValues", paramValues) + + // Pass the same receiver object IDs as PTB object arguments to ccip_receive + for _, objectId := range receiverObjectIdStrings { + paramValues = append(paramValues, bind.Object{Id: objectId}) } encodedReceiverCall, err := boundReceiverContract.EncodeCallArgsWithGenerics( diff --git a/relayer/chainwriter/ptb/offramp/execute_test.go b/relayer/chainwriter/ptb/offramp/execute_test.go new file mode 100644 index 000000000..256229de0 --- /dev/null +++ b/relayer/chainwriter/ptb/offramp/execute_test.go @@ -0,0 +1,235 @@ +package offramp + +import ( + "math/big" + "testing" + + "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" + "github.com/stretchr/testify/assert" +) + +func TestNeedsAppDelivery(t *testing.T) { + tests := []struct { + name string + message ccipocr3.Message + extraArgs map[string]any + expected bool + }{ + { + name: "empty data and no gasLimit in extraArgs", + message: ccipocr3.Message{Data: nil}, + extraArgs: map[string]any{}, + expected: false, + }, + { + name: "empty data and zero gasLimit (big.Int)", + message: ccipocr3.Message{Data: []byte{}}, + extraArgs: map[string]any{"gasLimit": big.NewInt(0)}, + expected: false, + }, + { + name: "empty data and nil gasLimit (big.Int)", + message: ccipocr3.Message{Data: nil}, + extraArgs: map[string]any{"gasLimit": (*big.Int)(nil)}, + expected: false, + }, + { + name: "empty data and zero gasLimit (uint64)", + message: ccipocr3.Message{Data: []byte{}}, + extraArgs: map[string]any{"gasLimit": uint64(0)}, + expected: false, + }, + { + name: "non-empty data and zero gasLimit", + message: ccipocr3.Message{Data: []byte{0x01}}, + extraArgs: map[string]any{"gasLimit": big.NewInt(0)}, + expected: true, + }, + { + name: "non-empty data and no extraArgs", + message: ccipocr3.Message{Data: []byte{0xab, 0xcd}}, + extraArgs: map[string]any{}, + expected: true, + }, + { + name: "empty data and positive gasLimit (big.Int)", + message: ccipocr3.Message{Data: nil}, + extraArgs: map[string]any{"gasLimit": big.NewInt(200000)}, + expected: true, + }, + { + name: "empty data and positive gasLimit (uint64)", + message: ccipocr3.Message{Data: []byte{}}, + extraArgs: map[string]any{"gasLimit": uint64(100000)}, + expected: true, + }, + { + name: "both data and gasLimit present", + message: ccipocr3.Message{Data: []byte{0x01}}, + extraArgs: map[string]any{"gasLimit": big.NewInt(500000)}, + expected: true, + }, + { + name: "gasLimit is unexpected type (string) — treated as absent", + message: ccipocr3.Message{Data: nil}, + extraArgs: map[string]any{"gasLimit": "not-a-number"}, + expected: false, + }, + { + name: "nil extraArgs map", + message: ccipocr3.Message{Data: nil}, + extraArgs: nil, + expected: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result := needsAppDelivery(tc.message, tc.extraArgs) + assert.Equal(t, tc.expected, result) + }) + } +} + +func TestProcessReceivers_SkipsZeroAddressReceiver(t *testing.T) { + // A 32-byte zero address receiver should be skipped. + zeroAddr := make([]byte, 32) + msg := ccipocr3.Message{ + Receiver: zeroAddr, + Data: []byte{0x01}, + } + // Verify our check matches codec.AccountZero pattern + allZero := true + for _, b := range msg.Receiver { + if b != 0 { + allZero = false + break + } + } + assert.True(t, allZero) +} + +func TestProcessReceivers_SkipsTokenOnlyMessage(t *testing.T) { + // Token-only messages (empty data, zero gas) should skip receiver processing. + receiver := make([]byte, 32) + receiver[31] = 0x01 // non-zero receiver + + msg := ccipocr3.Message{ + Receiver: receiver, + Data: nil, + } + extraArgs := map[string]any{"gasLimit": big.NewInt(0)} + + assert.False(t, needsAppDelivery(msg, extraArgs), + "token-only message should not need app delivery") +} + +func TestProcessReceivers_RequiresAppDeliveryForDataMessage(t *testing.T) { + // Message with data should require app delivery. + receiver := make([]byte, 32) + receiver[31] = 0x01 + + msg := ccipocr3.Message{ + Receiver: receiver, + Data: []byte{0xde, 0xad}, + } + extraArgs := map[string]any{} + + assert.True(t, needsAppDelivery(msg, extraArgs), + "message with data should need app delivery") +} + +func TestProcessReceivers_RequiresAppDeliveryForGasLimitMessage(t *testing.T) { + // Message with non-zero gasLimit should require app delivery even without data. + receiver := make([]byte, 32) + receiver[31] = 0x01 + + msg := ccipocr3.Message{ + Receiver: receiver, + Data: nil, + } + extraArgs := map[string]any{"gasLimit": big.NewInt(100000)} + + assert.True(t, needsAppDelivery(msg, extraArgs), + "message with gasLimit > 0 should need app delivery") +} + +func TestExtractReceiverObjectIdStrings(t *testing.T) { + tests := []struct { + name string + extraArgs map[string]any + expected []string + }{ + { + name: "missing key returns empty slice", + extraArgs: map[string]any{}, + expected: []string{}, + }, + { + name: "nil extraArgs returns empty slice", + extraArgs: nil, + expected: []string{}, + }, + { + name: "[][]byte with one entry", + extraArgs: map[string]any{ + "receiverObjectIds": [][]byte{ + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x11}, + }, + }, + expected: []string{"0x0000000000000000000000000000000000000000000000000000000000001111"}, + }, + { + name: "[][]byte with multiple entries", + extraArgs: map[string]any{ + "receiverObjectIds": [][]byte{ + {0xAA, 0xBB}, + {0xCC, 0xDD}, + }, + }, + expected: []string{"0xaabb", "0xccdd"}, + }, + { + name: "[]any with []byte elements", + extraArgs: map[string]any{ + "receiverObjectIds": []any{ + []byte{0x11, 0x22}, + []byte{0x33, 0x44}, + }, + }, + expected: []string{"0x1122", "0x3344"}, + }, + { + name: "[]any with non-byte elements skipped", + extraArgs: map[string]any{ + "receiverObjectIds": []any{ + []byte{0x11, 0x22}, + "not-bytes", + []byte{0x33, 0x44}, + }, + }, + expected: []string{"0x1122", "0x3344"}, + }, + { + name: "unexpected type returns empty", + extraArgs: map[string]any{ + "receiverObjectIds": "not-a-slice", + }, + expected: []string{}, + }, + { + name: "empty [][]byte returns empty", + extraArgs: map[string]any{ + "receiverObjectIds": [][]byte{}, + }, + expected: []string{}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result := extractReceiverObjectIdStrings(tc.extraArgs) + assert.Equal(t, tc.expected, result) + }) + } +} diff --git a/relayer/chainwriter/ptb/offramp/helpers.go b/relayer/chainwriter/ptb/offramp/helpers.go index 87e50bf16..c33ddac1a 100644 --- a/relayer/chainwriter/ptb/offramp/helpers.go +++ b/relayer/chainwriter/ptb/offramp/helpers.go @@ -145,7 +145,11 @@ type SuiArgumentMetadata struct { Type string `json:"type"` } -func decodeParam(lggr logger.Logger, param any, reference string) SuiArgumentMetadata { +func decodeParam(lggr logger.Logger, param any, reference string) (SuiArgumentMetadata, error) { + if param == nil { + return SuiArgumentMetadata{}, fmt.Errorf("nil parameter") + } + // Handle primitive types (strings like "U64", "Bool", etc.) if str, ok := param.(string); ok { return SuiArgumentMetadata{ @@ -155,51 +159,133 @@ func decodeParam(lggr logger.Logger, param any, reference string) SuiArgumentMet Reference: reference, TypeArguments: []TypeParameter{}, Type: ParseParamType(lggr, str), - } + }, nil + } + + m, ok := param.(map[string]any) + if !ok { + return SuiArgumentMetadata{}, fmt.Errorf("expected map[string]any, got %T", param) } - // Handle complex types (maps) - m := param.(map[string]any) for k, v := range m { switch k { case "Struct": - // Direct struct - s := v.(map[string]any) - typeArguments := []TypeParameter{} - for _, ta := range s["typeArguments"].([]any) { - typeArgument := ta.(map[string]any) - typeArguments = append(typeArguments, TypeParameter{TypeParameter: typeArgument["TypeParameter"].(float64)}) - } - return SuiArgumentMetadata{ - Address: s["address"].(string), - Module: s["module"].(string), - Name: s["name"].(string), - Reference: reference, - TypeArguments: typeArguments, - Type: ParseParamType(lggr, v), - } + return decodeStructParam(lggr, v, reference) case "Reference", "MutableReference", "Vector": - // Reference and MutableReference are the same thing - // We need to unwrap the struct return decodeParam(lggr, v, k) + case "TypeParameter": + return SuiArgumentMetadata{}, fmt.Errorf("unsupported ABI shape: TypeParameter (receiver uses generics)") default: - inner := v.(map[string]any)["Struct"].(map[string]any) - typeArguments := []TypeParameter{} - for _, ta := range inner["typeArguments"].([]any) { - typeArgument := ta.(map[string]any) - typeArguments = append(typeArguments, TypeParameter{TypeParameter: typeArgument["TypeParameter"].(float64)}) + vMap, ok := v.(map[string]any) + if !ok { + return SuiArgumentMetadata{}, fmt.Errorf("unsupported ABI shape: key %q has non-map value of type %T", k, v) + } + innerRaw, exists := vMap["Struct"] + if !exists { + return SuiArgumentMetadata{}, fmt.Errorf("unsupported ABI shape: key %q missing inner Struct", k) + } + inner, ok := innerRaw.(map[string]any) + if !ok { + return SuiArgumentMetadata{}, fmt.Errorf("unsupported ABI shape: key %q Struct value is %T, not map", k, innerRaw) + } + typeArguments, err := decodeTypeArguments(inner) + if err != nil { + return SuiArgumentMetadata{}, fmt.Errorf("key %q: %w", k, err) + } + address, module, name, err := extractStructFields(inner) + if err != nil { + return SuiArgumentMetadata{}, fmt.Errorf("key %q: %w", k, err) } return SuiArgumentMetadata{ - Address: inner["address"].(string), - Module: inner["module"].(string), - Name: inner["name"].(string), + Address: address, + Module: module, + Name: name, Reference: k, TypeArguments: typeArguments, Type: ParseParamType(lggr, v), - } + }, nil } } - return SuiArgumentMetadata{} + return SuiArgumentMetadata{}, fmt.Errorf("empty parameter map") +} + +func decodeStructParam(lggr logger.Logger, v any, reference string) (SuiArgumentMetadata, error) { + s, ok := v.(map[string]any) + if !ok { + return SuiArgumentMetadata{}, fmt.Errorf("Struct value is %T, expected map[string]any", v) + } + typeArguments, err := decodeTypeArguments(s) + if err != nil { + return SuiArgumentMetadata{}, fmt.Errorf("Struct: %w", err) + } + address, module, name, err := extractStructFields(s) + if err != nil { + return SuiArgumentMetadata{}, fmt.Errorf("Struct: %w", err) + } + return SuiArgumentMetadata{ + Address: address, + Module: module, + Name: name, + Reference: reference, + TypeArguments: typeArguments, + Type: ParseParamType(lggr, v), + }, nil +} + +func decodeTypeArguments(s map[string]any) ([]TypeParameter, error) { + taRaw, exists := s["typeArguments"] + if !exists { + return []TypeParameter{}, nil + } + taSlice, ok := taRaw.([]any) + if !ok { + return nil, fmt.Errorf("typeArguments is %T, expected []any", taRaw) + } + typeArguments := make([]TypeParameter, 0, len(taSlice)) + for i, ta := range taSlice { + taMap, ok := ta.(map[string]any) + if !ok { + return nil, fmt.Errorf("typeArguments[%d] is %T, expected map[string]any", i, ta) + } + tpRaw, exists := taMap["TypeParameter"] + if !exists { + return nil, fmt.Errorf("typeArguments[%d] missing TypeParameter key", i) + } + tp, ok := tpRaw.(float64) + if !ok { + return nil, fmt.Errorf("typeArguments[%d].TypeParameter is %T, expected float64", i, tpRaw) + } + typeArguments = append(typeArguments, TypeParameter{TypeParameter: tp}) + } + return typeArguments, nil +} + +func extractStructFields(s map[string]any) (address, module, name string, err error) { + addrRaw, ok := s["address"] + if !ok { + return "", "", "", fmt.Errorf("missing field \"address\"") + } + address, ok = addrRaw.(string) + if !ok { + return "", "", "", fmt.Errorf("field \"address\" is %T, expected string", addrRaw) + } + modRaw, ok := s["module"] + if !ok { + return "", "", "", fmt.Errorf("missing field \"module\"") + } + module, ok = modRaw.(string) + if !ok { + return "", "", "", fmt.Errorf("field \"module\" is %T, expected string", modRaw) + } + nameRaw, ok := s["name"] + if !ok { + return "", "", "", fmt.Errorf("missing field \"name\"") + } + name, ok = nameRaw.(string) + if !ok { + return "", "", "", fmt.Errorf("field \"name\" is %T, expected string", nameRaw) + } + return address, module, name, nil } func ParseParamType(lggr logger.Logger, param interface{}) string { @@ -276,7 +362,11 @@ func DecodeParameters(lggr logger.Logger, function map[string]any, key string) ( defaultReference := "Reference" decodedParameters := make([]SuiArgumentMetadata, len(parameters)) for i, parameter := range parameters { - decodedParameters[i] = decodeParam(lggr, parameter, defaultReference) + decoded, err := decodeParam(lggr, parameter, defaultReference) + if err != nil { + return nil, fmt.Errorf("failed to decode parameter %d: %w", i, err) + } + decodedParameters[i] = decoded } lggr.Debugw("decoded parameters", "decodedParameters", decodedParameters) @@ -289,7 +379,6 @@ func DecodeParameters(lggr logger.Logger, function map[string]any, key string) ( if param.Reference == "Reference" { if strings.HasPrefix(param.Type, "u") || param.Type == "bool" { - // It's a primitive, not an object reference paramTypes = append(paramTypes, param.Type) } else { paramTypes = append(paramTypes, "&object") diff --git a/relayer/chainwriter/ptb/offramp/helpers_test.go b/relayer/chainwriter/ptb/offramp/helpers_test.go new file mode 100644 index 000000000..865056249 --- /dev/null +++ b/relayer/chainwriter/ptb/offramp/helpers_test.go @@ -0,0 +1,296 @@ +package offramp + +import ( + "testing" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDecodeParam_PoisonABI_TypeParameter(t *testing.T) { + lggr := logger.Test(t) + + tests := []struct { + name string + param any + }{ + { + name: "Vector wrapping TypeParameter", + param: map[string]any{"Vector": map[string]any{"TypeParameter": float64(0)}}, + }, + { + name: "top-level TypeParameter", + param: map[string]any{"TypeParameter": float64(0)}, + }, + { + name: "Reference wrapping Vector wrapping TypeParameter", + param: map[string]any{"Reference": map[string]any{"Vector": map[string]any{"TypeParameter": float64(0)}}}, + }, + { + name: "MutableReference wrapping TypeParameter", + param: map[string]any{"MutableReference": map[string]any{"TypeParameter": float64(1)}}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result, err := decodeParam(lggr, tc.param, "Reference") + assert.Error(t, err) + assert.Contains(t, err.Error(), "TypeParameter") + assert.Equal(t, SuiArgumentMetadata{}, result) + }) + } +} + +func TestDecodeParam_MalformedInput_NoP(t *testing.T) { + lggr := logger.Test(t) + + tests := []struct { + name string + param any + }{ + { + name: "nil parameter", + param: nil, + }, + { + name: "float64 instead of map or string", + param: float64(42), + }, + { + name: "integer instead of map or string", + param: int(7), + }, + { + name: "Struct with nil value", + param: map[string]any{"Struct": nil}, + }, + { + name: "Struct with wrong type value", + param: map[string]any{"Struct": "not-a-map"}, + }, + { + name: "Struct missing address field", + param: map[string]any{"Struct": map[string]any{"module": "m", "name": "S", "typeArguments": []any{}}}, + }, + { + name: "Struct with non-string address", + param: map[string]any{"Struct": map[string]any{"address": 123, "module": "m", "name": "S", "typeArguments": []any{}}}, + }, + { + name: "Struct with bad typeArguments type", + param: map[string]any{"Struct": map[string]any{"address": "0x1", "module": "m", "name": "S", "typeArguments": "not-array"}}, + }, + { + name: "Struct with typeArgument missing TypeParameter key", + param: map[string]any{"Struct": map[string]any{"address": "0x1", "module": "m", "name": "S", "typeArguments": []any{map[string]any{"NotTypeParameter": float64(0)}}}}, + }, + { + name: "empty map", + param: map[string]any{}, + }, + { + name: "default key with non-map value", + param: map[string]any{"SomeUnknownKey": float64(99)}, + }, + { + name: "default key with map missing Struct", + param: map[string]any{"SomeKey": map[string]any{"NotStruct": "x"}}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert.NotPanics(t, func() { + _, err := decodeParam(lggr, tc.param, "Reference") + assert.Error(t, err, "expected error for input: %v", tc.param) + }) + }) + } +} + +func TestDecodeParam_ValidABI(t *testing.T) { + lggr := logger.Test(t) + + tests := []struct { + name string + param any + reference string + expected SuiArgumentMetadata + }{ + { + name: "primitive U64", + param: "U64", + reference: "Reference", + expected: SuiArgumentMetadata{ + Name: "U64", + Reference: "Reference", + TypeArguments: []TypeParameter{}, + Type: "u64", + }, + }, + { + name: "primitive Bool", + param: "Bool", + reference: "Reference", + expected: SuiArgumentMetadata{ + Name: "Bool", + Reference: "Reference", + TypeArguments: []TypeParameter{}, + Type: "bool", + }, + }, + { + name: "Vector of U8", + param: map[string]any{"Vector": "U8"}, + reference: "Reference", + expected: SuiArgumentMetadata{ + Name: "U8", + Reference: "Vector", + TypeArguments: []TypeParameter{}, + Type: "u8", + }, + }, + { + name: "Struct with no type arguments", + param: map[string]any{"Struct": map[string]any{ + "address": "0x1", + "module": "state_object", + "name": "CCIPObjectRef", + "typeArguments": []any{}, + }}, + reference: "Reference", + expected: SuiArgumentMetadata{ + Address: "0x1", + Module: "state_object", + Name: "CCIPObjectRef", + Reference: "Reference", + TypeArguments: []TypeParameter{}, + Type: "object_id", + }, + }, + { + name: "MutableReference wrapping Struct", + param: map[string]any{"MutableReference": map[string]any{"Struct": map[string]any{ + "address": "0xabc", + "module": "dummy_receiver", + "name": "CCIPReceiverState", + "typeArguments": []any{}, + }}}, + reference: "Reference", + expected: SuiArgumentMetadata{ + Address: "0xabc", + Module: "dummy_receiver", + Name: "CCIPReceiverState", + Reference: "MutableReference", + TypeArguments: []TypeParameter{}, + Type: "object_id", + }, + }, + { + name: "Struct with one type argument", + param: map[string]any{"Struct": map[string]any{ + "address": "0xpool", + "module": "burn_mint", + "name": "BurnMintTokenPoolState", + "typeArguments": []any{map[string]any{"TypeParameter": float64(0)}}, + }}, + reference: "MutableReference", + expected: SuiArgumentMetadata{ + Address: "0xpool", + Module: "burn_mint", + Name: "BurnMintTokenPoolState", + Reference: "MutableReference", + TypeArguments: []TypeParameter{{TypeParameter: 0}}, + Type: "object_id", + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result, err := decodeParam(lggr, tc.param, tc.reference) + require.NoError(t, err) + assert.Equal(t, tc.expected, result) + }) + } +} + +func TestDecodeParameters_PoisonABI_ReturnsError(t *testing.T) { + lggr := logger.Test(t) + + function := map[string]any{ + "parameters": []any{ + map[string]any{"Vector": map[string]any{"TypeParameter": float64(0)}}, + "U64", + }, + } + + assert.NotPanics(t, func() { + result, err := DecodeParameters(lggr, function, "parameters") + assert.Error(t, err) + assert.Nil(t, result) + assert.Contains(t, err.Error(), "TypeParameter") + }) +} + +func TestDecodeParameters_ValidDummyReceiverSignature(t *testing.T) { + lggr := logger.Test(t) + + // Matches the normalized ABI of dummy_receiver::ccip_receive: + // ccip_receive(expected_message_id: vector, ref: &CCIPObjectRef, message: Any2SuiMessage, _: &Clock, state: &mut CCIPReceiverState) + function := map[string]any{ + "parameters": []any{ + map[string]any{"Vector": "U8"}, + map[string]any{"Reference": map[string]any{"Struct": map[string]any{ + "address": "0xccip", "module": "state_object", "name": "CCIPObjectRef", "typeArguments": []any{}, + }}}, + map[string]any{"Struct": map[string]any{ + "address": "0xccip", "module": "client", "name": "Any2SuiMessage", "typeArguments": []any{}, + }}, + map[string]any{"Reference": map[string]any{"Struct": map[string]any{ + "address": "0x2", "module": "clock", "name": "Clock", "typeArguments": []any{}, + }}}, + map[string]any{"MutableReference": map[string]any{"Struct": map[string]any{ + "address": "0xreceiver", "module": "dummy_receiver", "name": "CCIPReceiverState", "typeArguments": []any{}, + }}}, + map[string]any{"MutableReference": map[string]any{"Struct": map[string]any{ + "address": "0x2", "module": "tx_context", "name": "TxContext", "typeArguments": []any{}, + }}}, + }, + } + + result, err := DecodeParameters(lggr, function, "parameters") + require.NoError(t, err) + + // TxContext is skipped. Any2SuiMessage is a Struct with default reference "Reference" → "&object" + expected := []string{ + "vector", + "&object", + "&object", + "&object", + "&mut object", + } + assert.Equal(t, expected, result) +} + +func TestDecodeParameters_MissingKey(t *testing.T) { + lggr := logger.Test(t) + + function := map[string]any{"return": []any{}} + + result, err := DecodeParameters(lggr, function, "parameters") + assert.Error(t, err) + assert.Nil(t, result) +} + +func TestDecodeParameters_NilValue(t *testing.T) { + lggr := logger.Test(t) + + function := map[string]any{"parameters": nil} + + result, err := DecodeParameters(lggr, function, "parameters") + assert.Error(t, err) + assert.Nil(t, result) +} diff --git a/relayer/testutils/execute.go b/relayer/testutils/execute.go index e7ce2643a..a3ac885a0 100644 --- a/relayer/testutils/execute.go +++ b/relayer/testutils/execute.go @@ -385,3 +385,131 @@ func serializeAny2SuiTokenTransfer(s *bcs.Serializer, transfer Any2SuiTokenTrans return nil } + +// ================================================================ +// V2 Types and Serializer (includes receiver_object_ids) +// ================================================================ + +type Any2SuiRampMessageV2 struct { + Header RampMessageHeader `json:"header"` + Sender []byte `json:"sender"` + Data []byte `json:"data"` + Receiver []byte `json:"receiver"` + GasLimit *big.Int `json:"gas_limit"` + TokenReceiver []byte `json:"token_receiver"` + ReceiverObjectIds [][]byte `json:"receiver_object_ids"` + TokenAmounts []Any2SuiTokenTransfer `json:"token_amounts"` +} + +type ExecutionReportV2 struct { + SourceChainSelector uint64 `json:"source_chain_selector"` + Message Any2SuiRampMessageV2 `json:"message"` + OffchainTokenData [][]byte `json:"offchain_token_data"` + Proofs [][]byte `json:"proofs"` +} + +func SerializeExecutionReportV2(report ExecutionReportV2) ([]byte, error) { + s := &bcs.Serializer{} + + s.U64(report.SourceChainSelector) + if s.Error() != nil { + return nil, fmt.Errorf("failed to serialize SourceChainSelector: %w", s.Error()) + } + + if err := serializeAny2SuiRampMessageV2(s, report.Message); err != nil { + return nil, fmt.Errorf("failed to serialize Message: %w", err) + } + + bcs.SerializeSequenceWithFunction(report.OffchainTokenData, s, func(s *bcs.Serializer, item []byte) { + s.WriteBytes(item) + }) + if s.Error() != nil { + return nil, fmt.Errorf("failed to serialize OffchainTokenData: %w", s.Error()) + } + + bcs.SerializeSequenceWithFunction(report.Proofs, s, func(s *bcs.Serializer, item []byte) { + s.WriteBytes(item) + }) + if s.Error() != nil { + return nil, fmt.Errorf("failed to serialize Proofs: %w", s.Error()) + } + + return s.ToBytes(), nil +} + +func serializeAny2SuiRampMessageV2(s *bcs.Serializer, message Any2SuiRampMessageV2) error { + if err := serializeRampMessageHeader(s, message.Header); err != nil { + return fmt.Errorf("failed to serialize Header: %w", err) + } + + s.WriteBytes(message.Sender) + if s.Error() != nil { + return fmt.Errorf("failed to serialize Sender: %w", s.Error()) + } + + dataBytes := message.Data + if dataBytes == nil { + dataBytes = make([]byte, 0) + } + s.WriteBytes(dataBytes) + if s.Error() != nil { + return fmt.Errorf("failed to serialize Data: %w", s.Error()) + } + + receiverBytes := make([]byte, DefaultByteSize) + if len(message.Receiver) > 0 && string(message.Receiver) != "0x" { + receiverStr := string(message.Receiver) + if strings.Contains(receiverStr, "::") { + parts := strings.Split(receiverStr, "::") + if len(parts) >= 1 { + receiverStr = parts[0] + } + } + receiverStr = strings.TrimPrefix(receiverStr, "0x") + if decoded, err := hex.DecodeString(receiverStr); err == nil && len(decoded) <= DefaultByteSize { + copy(receiverBytes[DefaultByteSize-len(decoded):], decoded) + } + } + s.FixedBytes(receiverBytes) + if s.Error() != nil { + return fmt.Errorf("failed to serialize Receiver: %w", s.Error()) + } + + if message.GasLimit == nil { + s.U256(*big.NewInt(0)) + } else { + s.U256(*message.GasLimit) + } + if s.Error() != nil { + return fmt.Errorf("failed to serialize GasLimit: %w", s.Error()) + } + + s.FixedBytes(message.TokenReceiver) + if s.Error() != nil { + return fmt.Errorf("failed to serialize TokenReceiver: %w", s.Error()) + } + + // V2: serialize receiver_object_ids as vector
(each 32 bytes) + bcs.SerializeSequenceWithFunction(message.ReceiverObjectIds, s, func(s *bcs.Serializer, item []byte) { + addrBytes := make([]byte, DefaultByteSize) + if len(item) <= DefaultByteSize { + copy(addrBytes[DefaultByteSize-len(item):], item) + } + s.FixedBytes(addrBytes) + }) + if s.Error() != nil { + return fmt.Errorf("failed to serialize ReceiverObjectIds: %w", s.Error()) + } + + // Serialize TokenAmounts + bcs.SerializeSequenceWithFunction(message.TokenAmounts, s, func(s *bcs.Serializer, item Any2SuiTokenTransfer) { + if err := serializeAny2SuiTokenTransfer(s, item); err != nil { + s.SetError(err) + } + }) + if s.Error() != nil { + return fmt.Errorf("failed to serialize TokenAmounts: %w", s.Error()) + } + + return nil +}