diff --git a/.github/workflows/aptos-movefmt.yml b/.github/workflows/aptos-movefmt.yml index 4af36241..db17ae6a 100644 --- a/.github/workflows/aptos-movefmt.yml +++ b/.github/workflows/aptos-movefmt.yml @@ -11,7 +11,7 @@ jobs: with: CLI_VERSION: 7.11.1 - name: Update movefmt - run: aptos update movefmt --target-version 1.3.7 # Fix version to prevent accidentally breaking the CI + run: aptos update movefmt --target-version 1.3.8 # Fix version to prevent accidentally breaking the CI - name: Run movefmt run: make fmt - name: Check if any files have been changed diff --git a/bindings/ccip/rmn_remote/rmn_remote.go b/bindings/ccip/rmn_remote/rmn_remote.go index 4991b245..264011d3 100644 --- a/bindings/ccip/rmn_remote/rmn_remote.go +++ b/bindings/ccip/rmn_remote/rmn_remote.go @@ -32,6 +32,8 @@ type RMNRemoteInterface interface { IsCursedGlobal(opts *bind.CallOpts) (bool, error) IsCursed(opts *bind.CallOpts, subject []byte) (bool, error) IsCursedU128(opts *bind.CallOpts, subjectValue *big.Int) (bool, error) + IsAllowedCurser(opts *bind.CallOpts, curser aptos.AccountAddress) (bool, error) + GetAllowedCursers(opts *bind.CallOpts) ([]aptos.AccountAddress, error) Initialize(opts *bind.TransactOpts, localChainSelector uint64) (*api.PendingTransaction, error) SetConfig(opts *bind.TransactOpts, rmnHomeContractConfigDigest []byte, signerOnchainPublicKeys [][]byte, nodeIndexes []uint64, fSign uint64) (*api.PendingTransaction, error) @@ -39,6 +41,9 @@ type RMNRemoteInterface interface { CurseMultiple(opts *bind.TransactOpts, subjects [][]byte) (*api.PendingTransaction, error) Uncurse(opts *bind.TransactOpts, subject []byte) (*api.PendingTransaction, error) UncurseMultiple(opts *bind.TransactOpts, subjects [][]byte) (*api.PendingTransaction, error) + InitializeAllowedCursersV2(opts *bind.TransactOpts, initialCursers []aptos.AccountAddress) (*api.PendingTransaction, error) + AddAllowedCursers(opts *bind.TransactOpts, cursersToAdd []aptos.AccountAddress) (*api.PendingTransaction, error) + RemoveAllowedCursers(opts *bind.TransactOpts, cursersToRemove []aptos.AccountAddress) (*api.PendingTransaction, error) // Encoder returns the encoder implementation of this module. Encoder() RMNRemoteEncoder @@ -55,17 +60,23 @@ type RMNRemoteEncoder interface { IsCursedGlobal() (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) IsCursed(subject []byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) IsCursedU128(subjectValue *big.Int) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + IsAllowedCurser(curser aptos.AccountAddress) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + GetAllowedCursers() (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) Initialize(localChainSelector uint64) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) SetConfig(rmnHomeContractConfigDigest []byte, signerOnchainPublicKeys [][]byte, nodeIndexes []uint64, fSign uint64) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) Curse(subject []byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) CurseMultiple(subjects [][]byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) Uncurse(subject []byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) UncurseMultiple(subjects [][]byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + InitializeAllowedCursersV2(initialCursers []aptos.AccountAddress) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + AddAllowedCursers(cursersToAdd []aptos.AccountAddress) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + RemoveAllowedCursers(cursersToRemove []aptos.AccountAddress) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + AssertOwnerOrAllowedCurser(caller aptos.AccountAddress) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) MCMSEntrypoint(Metadata aptos.AccountAddress) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) RegisterMCMSEntrypoint() (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) } -const FunctionInfo = `[{"package":"ccip","module":"rmn_remote","name":"curse","parameters":[{"name":"subject","type":"vector\u003cu8\u003e"}]},{"package":"ccip","module":"rmn_remote","name":"curse_multiple","parameters":[{"name":"subjects","type":"vector\u003cvector\u003cu8\u003e\u003e"}]},{"package":"ccip","module":"rmn_remote","name":"initialize","parameters":[{"name":"local_chain_selector","type":"u64"}]},{"package":"ccip","module":"rmn_remote","name":"mcms_entrypoint","parameters":[{"name":"_metadata","type":"address"}]},{"package":"ccip","module":"rmn_remote","name":"register_mcms_entrypoint","parameters":null},{"package":"ccip","module":"rmn_remote","name":"set_config","parameters":[{"name":"rmn_home_contract_config_digest","type":"vector\u003cu8\u003e"},{"name":"signer_onchain_public_keys","type":"vector\u003cvector\u003cu8\u003e\u003e"},{"name":"node_indexes","type":"vector\u003cu64\u003e"},{"name":"f_sign","type":"u64"}]},{"package":"ccip","module":"rmn_remote","name":"uncurse","parameters":[{"name":"subject","type":"vector\u003cu8\u003e"}]},{"package":"ccip","module":"rmn_remote","name":"uncurse_multiple","parameters":[{"name":"subjects","type":"vector\u003cvector\u003cu8\u003e\u003e"}]}]` +const FunctionInfo = `[{"package":"ccip","module":"rmn_remote","name":"add_allowed_cursers","parameters":[{"name":"cursers_to_add","type":"vector\u003caddress\u003e"}]},{"package":"ccip","module":"rmn_remote","name":"assert_owner_or_allowed_curser","parameters":[{"name":"caller","type":"address"}]},{"package":"ccip","module":"rmn_remote","name":"curse","parameters":[{"name":"subject","type":"vector\u003cu8\u003e"}]},{"package":"ccip","module":"rmn_remote","name":"curse_multiple","parameters":[{"name":"subjects","type":"vector\u003cvector\u003cu8\u003e\u003e"}]},{"package":"ccip","module":"rmn_remote","name":"initialize","parameters":[{"name":"local_chain_selector","type":"u64"}]},{"package":"ccip","module":"rmn_remote","name":"initialize_allowed_cursers_v2","parameters":[{"name":"initial_cursers","type":"vector\u003caddress\u003e"}]},{"package":"ccip","module":"rmn_remote","name":"mcms_entrypoint","parameters":[{"name":"_metadata","type":"address"}]},{"package":"ccip","module":"rmn_remote","name":"register_mcms_entrypoint","parameters":null},{"package":"ccip","module":"rmn_remote","name":"remove_allowed_cursers","parameters":[{"name":"cursers_to_remove","type":"vector\u003caddress\u003e"}]},{"package":"ccip","module":"rmn_remote","name":"set_config","parameters":[{"name":"rmn_home_contract_config_digest","type":"vector\u003cu8\u003e"},{"name":"signer_onchain_public_keys","type":"vector\u003cvector\u003cu8\u003e\u003e"},{"name":"node_indexes","type":"vector\u003cu64\u003e"},{"name":"f_sign","type":"u64"}]},{"package":"ccip","module":"rmn_remote","name":"uncurse","parameters":[{"name":"subject","type":"vector\u003cu8\u003e"}]},{"package":"ccip","module":"rmn_remote","name":"uncurse_multiple","parameters":[{"name":"subjects","type":"vector\u003cvector\u003cu8\u003e\u003e"}]}]` func NewRMNRemote(address aptos.AccountAddress, client aptos.AptosRpcClient) RMNRemoteInterface { contract := bind.NewBoundContract(address, "ccip", "rmn_remote", client) @@ -77,24 +88,29 @@ func NewRMNRemote(address aptos.AccountAddress, client aptos.AptosRpcClient) RMN // Constants const ( - E_ALREADY_INITIALIZED uint64 = 1 - E_ALREADY_CURSED uint64 = 2 - E_CONFIG_NOT_SET uint64 = 3 - E_DUPLICATE_SIGNER uint64 = 4 - E_INVALID_SIGNATURE uint64 = 5 - E_INVALID_SIGNER_ORDER uint64 = 6 - E_NOT_ENOUGH_SIGNERS uint64 = 7 - E_NOT_CURSED uint64 = 8 - E_OUT_OF_ORDER_SIGNATURES uint64 = 9 - E_THRESHOLD_NOT_MET uint64 = 10 - E_UNEXPECTED_SIGNER uint64 = 11 - E_ZERO_VALUE_NOT_ALLOWED uint64 = 12 - E_MERKLE_ROOT_LENGTH_MISMATCH uint64 = 13 - E_INVALID_DIGEST_LENGTH uint64 = 14 - E_SIGNERS_MISMATCH uint64 = 15 - E_INVALID_SUBJECT_LENGTH uint64 = 16 - E_INVALID_PUBLIC_KEY_LENGTH uint64 = 17 - E_UNKNOWN_FUNCTION uint64 = 18 + E_ALREADY_INITIALIZED uint64 = 1 + E_ALREADY_CURSED uint64 = 2 + E_CONFIG_NOT_SET uint64 = 3 + E_DUPLICATE_SIGNER uint64 = 4 + E_INVALID_SIGNATURE uint64 = 5 + E_INVALID_SIGNER_ORDER uint64 = 6 + E_NOT_ENOUGH_SIGNERS uint64 = 7 + E_NOT_CURSED uint64 = 8 + E_OUT_OF_ORDER_SIGNATURES uint64 = 9 + E_THRESHOLD_NOT_MET uint64 = 10 + E_UNEXPECTED_SIGNER uint64 = 11 + E_ZERO_VALUE_NOT_ALLOWED uint64 = 12 + E_MERKLE_ROOT_LENGTH_MISMATCH uint64 = 13 + E_INVALID_DIGEST_LENGTH uint64 = 14 + E_SIGNERS_MISMATCH uint64 = 15 + E_INVALID_SUBJECT_LENGTH uint64 = 16 + E_INVALID_PUBLIC_KEY_LENGTH uint64 = 17 + E_UNKNOWN_FUNCTION uint64 = 18 + E_NOT_OWNER_OR_ALLOWED_CURSER uint64 = 19 + E_ALLOWED_CURSERS_V2_ALREADY_INITIALIZED uint64 = 20 + E_ALLOWED_CURSERS_V2_NOT_INITIALIZED uint64 = 21 + E_CURSER_ALREADY_ALLOWED uint64 = 22 + E_CURSER_NOT_ALLOWED uint64 = 23 ) // Structs @@ -146,6 +162,17 @@ type Uncursed struct { Subjects [][]byte `move:"vector>"` } +type AllowedCursersV2 struct { +} + +type AllowedCursersAdded struct { + Cursers []aptos.AccountAddress `move:"vector
"` +} + +type AllowedCursersRemoved struct { + Cursers []aptos.AccountAddress `move:"vector
"` +} + type McmsCallback struct { } @@ -373,6 +400,48 @@ func (c RMNRemoteContract) IsCursedU128(opts *bind.CallOpts, subjectValue *big.I return r0, nil } +func (c RMNRemoteContract) IsAllowedCurser(opts *bind.CallOpts, curser aptos.AccountAddress) (bool, error) { + module, function, typeTags, args, err := c.rmnRemoteEncoder.IsAllowedCurser(curser) + if err != nil { + return *new(bool), err + } + + callData, err := c.Call(opts, module, function, typeTags, args) + if err != nil { + return *new(bool), err + } + + var ( + r0 bool + ) + + if err := codec.DecodeAptosJsonArray(callData, &r0); err != nil { + return *new(bool), err + } + return r0, nil +} + +func (c RMNRemoteContract) GetAllowedCursers(opts *bind.CallOpts) ([]aptos.AccountAddress, error) { + module, function, typeTags, args, err := c.rmnRemoteEncoder.GetAllowedCursers() + if err != nil { + return *new([]aptos.AccountAddress), err + } + + callData, err := c.Call(opts, module, function, typeTags, args) + if err != nil { + return *new([]aptos.AccountAddress), err + } + + var ( + r0 []aptos.AccountAddress + ) + + if err := codec.DecodeAptosJsonArray(callData, &r0); err != nil { + return *new([]aptos.AccountAddress), err + } + return r0, nil +} + // Entry Functions func (c RMNRemoteContract) Initialize(opts *bind.TransactOpts, localChainSelector uint64) (*api.PendingTransaction, error) { @@ -429,6 +498,33 @@ func (c RMNRemoteContract) UncurseMultiple(opts *bind.TransactOpts, subjects [][ return c.BoundContract.Transact(opts, module, function, typeTags, args) } +func (c RMNRemoteContract) InitializeAllowedCursersV2(opts *bind.TransactOpts, initialCursers []aptos.AccountAddress) (*api.PendingTransaction, error) { + module, function, typeTags, args, err := c.rmnRemoteEncoder.InitializeAllowedCursersV2(initialCursers) + if err != nil { + return nil, err + } + + return c.BoundContract.Transact(opts, module, function, typeTags, args) +} + +func (c RMNRemoteContract) AddAllowedCursers(opts *bind.TransactOpts, cursersToAdd []aptos.AccountAddress) (*api.PendingTransaction, error) { + module, function, typeTags, args, err := c.rmnRemoteEncoder.AddAllowedCursers(cursersToAdd) + if err != nil { + return nil, err + } + + return c.BoundContract.Transact(opts, module, function, typeTags, args) +} + +func (c RMNRemoteContract) RemoveAllowedCursers(opts *bind.TransactOpts, cursersToRemove []aptos.AccountAddress) (*api.PendingTransaction, error) { + module, function, typeTags, args, err := c.rmnRemoteEncoder.RemoveAllowedCursers(cursersToRemove) + if err != nil { + return nil, err + } + + return c.BoundContract.Transact(opts, module, function, typeTags, args) +} + // Encoder type rmnRemoteEncoder struct { *bind.BoundContract @@ -498,6 +594,18 @@ func (c rmnRemoteEncoder) IsCursedU128(subjectValue *big.Int) (bind.ModuleInform }) } +func (c rmnRemoteEncoder) IsAllowedCurser(curser aptos.AccountAddress) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("is_allowed_curser", nil, []string{ + "address", + }, []any{ + curser, + }) +} + +func (c rmnRemoteEncoder) GetAllowedCursers() (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("get_allowed_cursers", nil, []string{}, []any{}) +} + func (c rmnRemoteEncoder) Initialize(localChainSelector uint64) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { return c.BoundContract.Encode("initialize", nil, []string{ "u64", @@ -552,6 +660,38 @@ func (c rmnRemoteEncoder) UncurseMultiple(subjects [][]byte) (bind.ModuleInforma }) } +func (c rmnRemoteEncoder) InitializeAllowedCursersV2(initialCursers []aptos.AccountAddress) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("initialize_allowed_cursers_v2", nil, []string{ + "vector
", + }, []any{ + initialCursers, + }) +} + +func (c rmnRemoteEncoder) AddAllowedCursers(cursersToAdd []aptos.AccountAddress) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("add_allowed_cursers", nil, []string{ + "vector
", + }, []any{ + cursersToAdd, + }) +} + +func (c rmnRemoteEncoder) RemoveAllowedCursers(cursersToRemove []aptos.AccountAddress) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("remove_allowed_cursers", nil, []string{ + "vector
", + }, []any{ + cursersToRemove, + }) +} + +func (c rmnRemoteEncoder) AssertOwnerOrAllowedCurser(caller aptos.AccountAddress) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("assert_owner_or_allowed_curser", nil, []string{ + "address", + }, []any{ + caller, + }) +} + func (c rmnRemoteEncoder) MCMSEntrypoint(Metadata aptos.AccountAddress) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { return c.BoundContract.Encode("mcms_entrypoint", nil, []string{ "address", diff --git a/contracts/ccip/ccip/sources/rmn_remote.move b/contracts/ccip/ccip/sources/rmn_remote.move index d14c7340..b9e73243 100644 --- a/contracts/ccip/ccip/sources/rmn_remote.move +++ b/contracts/ccip/ccip/sources/rmn_remote.move @@ -11,6 +11,7 @@ module ccip::rmn_remote { use std::signer; use std::string::{Self, String}; use std::smart_table::{Self, SmartTable}; + use std::ordered_map::{Self, OrderedMap}; use ccip::auth; use ccip::eth_abi; @@ -77,6 +78,26 @@ module ccip::rmn_remote { subjects: vector> } + // ================================================================ + // | AllowedCursersV2 (Fast Cursing) | + // ================================================================ + + struct AllowedCursersV2 has key { + allowed_cursers: OrderedMap, + allowed_cursers_added_events: EventHandle, + allowed_cursers_removed_events: EventHandle + } + + #[event] + struct AllowedCursersAdded has store, drop { + cursers: vector
+ } + + #[event] + struct AllowedCursersRemoved has store, drop { + cursers: vector
+ } + const E_ALREADY_INITIALIZED: u64 = 1; const E_ALREADY_CURSED: u64 = 2; const E_CONFIG_NOT_SET: u64 = 3; @@ -95,6 +116,11 @@ module ccip::rmn_remote { const E_INVALID_SUBJECT_LENGTH: u64 = 16; const E_INVALID_PUBLIC_KEY_LENGTH: u64 = 17; const E_UNKNOWN_FUNCTION: u64 = 18; + const E_NOT_OWNER_OR_ALLOWED_CURSER: u64 = 19; + const E_ALLOWED_CURSERS_V2_ALREADY_INITIALIZED: u64 = 20; + const E_ALLOWED_CURSERS_V2_NOT_INITIALIZED: u64 = 21; + const E_CURSER_ALREADY_ALLOWED: u64 = 22; + const E_CURSER_NOT_ALLOWED: u64 = 23; #[view] public fun type_and_version(): String { @@ -122,6 +148,57 @@ module ccip::rmn_remote { error::invalid_argument(E_ALREADY_INITIALIZED) ); + let state_object_signer = state_object::object_signer(); + + // Create V1 state (RMNRemoteState) + let state = RMNRemoteState { + local_chain_selector, + config: Config { + rmn_home_contract_config_digest: vector[], + signers: vector[], + f_sign: 0 + }, + config_count: 0, + signers: smart_table::new(), + cursed_subjects: smart_table::new(), + config_set_events: account::new_event_handle(&state_object_signer), + cursed_events: account::new_event_handle(&state_object_signer), + uncursed_events: account::new_event_handle(&state_object_signer) + }; + move_to(&state_object_signer, state); + + // Create V2 state (AllowedCursersV2) - new deployments get both + move_to( + &state_object_signer, + AllowedCursersV2 { + allowed_cursers: ordered_map::new(), + allowed_cursers_added_events: account::new_event_handle( + &state_object_signer + ), + allowed_cursers_removed_events: account::new_event_handle( + &state_object_signer + ) + } + ); + } + + #[test_only] + /// Legacy initialization that only creates RMNRemoteState (V1). + /// Used for testing migration scenarios from V1 to V2. + public entry fun initialize_v1( + caller: &signer, local_chain_selector: u64 + ) { + auth::assert_only_owner(signer::address_of(caller)); + + assert!( + local_chain_selector != 0, + error::invalid_argument(E_ZERO_VALUE_NOT_ALLOWED) + ); + assert!( + !exists(state_object::object_address()), + error::invalid_argument(E_ALREADY_INITIALIZED) + ); + let state_object_signer = state_object::object_signer(); let state = RMNRemoteState { local_chain_selector, @@ -364,14 +441,16 @@ module ccip::rmn_remote { aptos_hash::keccak256(b"RMN_V1_6_ANY2APTOS_REPORT") } - public entry fun curse(caller: &signer, subject: vector) acquires RMNRemoteState { + public entry fun curse( + caller: &signer, subject: vector + ) acquires RMNRemoteState, AllowedCursersV2 { curse_multiple(caller, vector[subject]); } public entry fun curse_multiple( caller: &signer, subjects: vector> - ) acquires RMNRemoteState { - auth::assert_only_owner(signer::address_of(caller)); + ) acquires RMNRemoteState, AllowedCursersV2 { + assert_owner_or_allowed_curser(signer::address_of(caller)); let state = borrow_state_mut(); @@ -392,14 +471,16 @@ module ccip::rmn_remote { event::emit_event(&mut state.cursed_events, Cursed { subjects }); } - public entry fun uncurse(caller: &signer, subject: vector) acquires RMNRemoteState { + public entry fun uncurse( + caller: &signer, subject: vector + ) acquires RMNRemoteState, AllowedCursersV2 { uncurse_multiple(caller, vector[subject]); } public entry fun uncurse_multiple( caller: &signer, subjects: vector> - ) acquires RMNRemoteState { - auth::assert_only_owner(signer::address_of(caller)); + ) acquires RMNRemoteState, AllowedCursersV2 { + assert_owner_or_allowed_curser(signer::address_of(caller)); let state = borrow_state_mut(); @@ -446,6 +527,149 @@ module ccip::rmn_remote { borrow_global_mut(state_object::object_address()) } + // ================================================================ + // | AllowedCursersV2 Helper Functions | + // ================================================================ + + inline fun borrow_allowed_cursers_v2(): &AllowedCursersV2 { + borrow_global(state_object::object_address()) + } + + inline fun borrow_allowed_cursers_v2_mut(): &mut AllowedCursersV2 { + borrow_global_mut(state_object::object_address()) + } + + #[view] + /// Check if an address is an allowed curser. + /// Returns false if AllowedCursersV2 is not initialized (V1 behavior: only owner can curse). + public fun is_allowed_curser(curser: address): bool acquires AllowedCursersV2 { + if (!exists(state_object::object_address())) { false } + else { + borrow_allowed_cursers_v2().allowed_cursers.contains(&curser) + } + } + + #[view] + /// Get the list of allowed cursers. + /// Returns empty vector if AllowedCursersV2 is not initialized. + public fun get_allowed_cursers(): vector
acquires AllowedCursersV2 { + if (!exists(state_object::object_address())) { + vector[] + } else { + borrow_allowed_cursers_v2().allowed_cursers.keys() + } + } + + inline fun assert_owner_or_allowed_curser(caller: address) { + assert!( + caller == auth::owner() || is_allowed_curser(caller), + error::permission_denied(E_NOT_OWNER_OR_ALLOWED_CURSER) + ); + } + + // ================================================================ + // | AllowedCursersV2 Admin Functions (Owner Only) | + // ================================================================ + + /// Initialize the AllowedCursersV2 resource. Owner only. + /// This must be called before adding allowed cursers. + public entry fun initialize_allowed_cursers_v2( + caller: &signer, initial_cursers: vector
+ ) { + auth::assert_only_owner(signer::address_of(caller)); + + assert!( + !exists(state_object::object_address()), + error::already_exists(E_ALLOWED_CURSERS_V2_ALREADY_INITIALIZED) + ); + + let state_object_signer = state_object::object_signer(); + let allowed_cursers = ordered_map::new(); + + initial_cursers.for_each_ref( + |curser| { + allowed_cursers.add(*curser, true); + } + ); + + move_to( + &state_object_signer, + AllowedCursersV2 { + allowed_cursers, + allowed_cursers_added_events: account::new_event_handle( + &state_object_signer + ), + allowed_cursers_removed_events: account::new_event_handle( + &state_object_signer + ) + } + ); + + if (!initial_cursers.is_empty()) { + event::emit(AllowedCursersAdded { cursers: initial_cursers }); + }; + } + + /// Add allowed cursers. Owner only. + /// AllowedCursersV2 must be initialized first. + public entry fun add_allowed_cursers( + caller: &signer, cursers_to_add: vector
+ ) acquires AllowedCursersV2 { + auth::assert_only_owner(signer::address_of(caller)); + + assert!( + exists(state_object::object_address()), + error::invalid_state(E_ALLOWED_CURSERS_V2_NOT_INITIALIZED) + ); + + let state = borrow_allowed_cursers_v2_mut(); + + cursers_to_add.for_each_ref( + |curser| { + assert!( + !state.allowed_cursers.contains(curser), + error::already_exists(E_CURSER_ALREADY_ALLOWED) + ); + state.allowed_cursers.add(*curser, true); + } + ); + + event::emit_event( + &mut state.allowed_cursers_added_events, + AllowedCursersAdded { cursers: cursers_to_add } + ); + } + + /// Remove allowed cursers. Owner only. + /// AllowedCursersV2 must be initialized first. + public entry fun remove_allowed_cursers( + caller: &signer, cursers_to_remove: vector
+ ) acquires AllowedCursersV2 { + auth::assert_only_owner(signer::address_of(caller)); + + assert!( + exists(state_object::object_address()), + error::invalid_state(E_ALLOWED_CURSERS_V2_NOT_INITIALIZED) + ); + + let state = borrow_allowed_cursers_v2_mut(); + + cursers_to_remove.for_each_ref( + |curser| { + assert!( + state.allowed_cursers.contains(curser), + error::not_found(E_CURSER_NOT_ALLOWED) + ); + state.allowed_cursers.remove(curser); + } + ); + + event::emit_event( + &mut state.allowed_cursers_removed_events, + AllowedCursersRemoved { cursers: cursers_to_remove } + ); + } + // ================================================================ // | MCMS Entrypoint | // ================================================================ @@ -454,7 +678,7 @@ module ccip::rmn_remote { public fun mcms_entrypoint( _metadata: object::Object - ): option::Option acquires RMNRemoteState { + ): option::Option acquires RMNRemoteState, AllowedCursersV2 { let (caller, function, data) = mcms_registry::get_callback_params(@ccip, McmsCallback {}); @@ -511,6 +735,30 @@ module ccip::rmn_remote { ); bcs_stream::assert_is_consumed(&stream); uncurse_multiple(&caller, subjects) + } else if (function_bytes == b"initialize_allowed_cursers_v2") { + let initial_cursers = + bcs_stream::deserialize_vector( + &mut stream, + |stream| bcs_stream::deserialize_address(stream) + ); + bcs_stream::assert_is_consumed(&stream); + initialize_allowed_cursers_v2(&caller, initial_cursers) + } else if (function_bytes == b"add_allowed_cursers") { + let cursers_to_add = + bcs_stream::deserialize_vector( + &mut stream, + |stream| bcs_stream::deserialize_address(stream) + ); + bcs_stream::assert_is_consumed(&stream); + add_allowed_cursers(&caller, cursers_to_add) + } else if (function_bytes == b"remove_allowed_cursers") { + let cursers_to_remove = + bcs_stream::deserialize_vector( + &mut stream, + |stream| bcs_stream::deserialize_address(stream) + ); + bcs_stream::assert_is_consumed(&stream); + remove_allowed_cursers(&caller, cursers_to_remove) } else { abort error::invalid_argument(E_UNKNOWN_FUNCTION) }; diff --git a/contracts/ccip/ccip/tests/rmn_remote_test.move b/contracts/ccip/ccip/tests/rmn_remote_test.move new file mode 100644 index 00000000..e49d5cac --- /dev/null +++ b/contracts/ccip/ccip/tests/rmn_remote_test.move @@ -0,0 +1,597 @@ +#[test_only] +module ccip::rmn_remote_test { + use std::signer; + use std::account; + use std::vector; + use std::object; + + use ccip::auth; + use ccip::rmn_remote; + use ccip::state_object; + + const CHAIN_SELECTOR: u64 = 743186221051783445; + + const OWNER: address = @mcms; + const CURSER_1: address = @0x100; + const CURSER_2: address = @0x200; + const CURSER_3: address = @0x300; + const UNAUTHORIZED: address = @0x400; + + // 16-byte subject for curse tests + const SUBJECT_1: vector = x"01000000000000000000000000000002"; + const SUBJECT_2: vector = x"01000000000000000000000000000003"; + + fun setup(ccip: &signer, owner: &signer) { + account::create_account_for_test(signer::address_of(ccip)); + account::create_account_for_test(CURSER_1); + account::create_account_for_test(CURSER_2); + account::create_account_for_test(CURSER_3); + account::create_account_for_test(UNAUTHORIZED); + + // Create object for @ccip + let _constructor_ref = object::create_named_object(owner, b"ccip"); + + state_object::init_module_for_testing(ccip); + auth::test_init_module(ccip); + // Use initialize_v1 for legacy tests (testing V1 -> V2 migration scenarios) + rmn_remote::initialize_v1(owner, CHAIN_SELECTOR); + } + + /// Setup function for tests that need full V2 initialization + fun setup_v2(ccip: &signer, owner: &signer) { + account::create_account_for_test(signer::address_of(ccip)); + account::create_account_for_test(CURSER_1); + account::create_account_for_test(CURSER_2); + account::create_account_for_test(CURSER_3); + account::create_account_for_test(UNAUTHORIZED); + + // Create object for @ccip + let _constructor_ref = object::create_named_object(owner, b"ccip"); + + state_object::init_module_for_testing(ccip); + auth::test_init_module(ccip); + + // Use full initialize (creates both V1 and V2 resources) + rmn_remote::initialize(owner, CHAIN_SELECTOR); + } + + // ================================================================ + // | V2 Initialize Pattern Tests | + // ================================================================ + + #[test(ccip = @ccip, owner = @mcms)] + fun test_initialize_creates_v2_resource( + ccip: &signer, owner: &signer + ) { + setup_v2(ccip, owner); + + // AllowedCursersV2 should exist and be empty + let cursers = rmn_remote::get_allowed_cursers(); + assert!(vector::length(&cursers) == 0, 0); + + // Owner can curse (V2 resource exists, no need to call initialize_allowed_cursers_v2) + rmn_remote::curse(owner, SUBJECT_1); + assert!(rmn_remote::is_cursed(SUBJECT_1), 1); + } + + #[test(ccip = @ccip, owner = @mcms)] + fun test_initialize_v2_can_add_cursers_directly( + ccip: &signer, owner: &signer + ) { + setup_v2(ccip, owner); + + // With full initialize, we can add cursers directly (no need to initialize_allowed_cursers_v2) + rmn_remote::add_allowed_cursers(owner, vector[CURSER_1]); + + assert!(rmn_remote::is_allowed_curser(CURSER_1), 0); + + let curser = + account::create_signer_with_capability( + &account::create_test_signer_cap(CURSER_1) + ); + + // Curser can curse + rmn_remote::curse(&curser, SUBJECT_1); + assert!(rmn_remote::is_cursed(SUBJECT_1), 1); + } + + #[test(ccip = @ccip, owner = @mcms)] + #[expected_failure(abort_code = 524308, location = ccip::rmn_remote)] + fun test_initialize_v2_cannot_call_initialize_allowed_cursers_v2( + ccip: &signer, owner: &signer + ) { + setup_v2(ccip, owner); + + // E_ALLOWED_CURSERS_V2_ALREADY_INITIALIZED - V2 resource already exists from initialize + rmn_remote::initialize_allowed_cursers_v2(owner, vector[CURSER_1]); + } + + // ================================================================ + // | AllowedCursersV2 Initialization Tests (Legacy V1) | + // ================================================================ + + #[test(ccip = @ccip, owner = @mcms)] + fun test_initialize_allowed_cursers_v2_empty( + ccip: &signer, owner: &signer + ) { + setup(ccip, owner); + + // Before initialization, get_allowed_cursers should return empty + let cursers = rmn_remote::get_allowed_cursers(); + assert!(vector::length(&cursers) == 0, 0); + + // Initialize with empty list + rmn_remote::initialize_allowed_cursers_v2(owner, vector[]); + + // Still empty after initialization + let cursers = rmn_remote::get_allowed_cursers(); + assert!(vector::length(&cursers) == 0, 1); + } + + #[test(ccip = @ccip, owner = @mcms)] + fun test_initialize_allowed_cursers_v2_with_cursers( + ccip: &signer, owner: &signer + ) { + setup(ccip, owner); + + // Initialize with cursers + rmn_remote::initialize_allowed_cursers_v2(owner, vector[CURSER_1, CURSER_2]); + + // Verify cursers are allowed + assert!(rmn_remote::is_allowed_curser(CURSER_1), 0); + assert!(rmn_remote::is_allowed_curser(CURSER_2), 1); + assert!(!rmn_remote::is_allowed_curser(CURSER_3), 2); + assert!(!rmn_remote::is_allowed_curser(UNAUTHORIZED), 3); + + let cursers = rmn_remote::get_allowed_cursers(); + assert!(vector::length(&cursers) == 2, 4); + } + + #[test(ccip = @ccip, owner = @mcms)] + #[expected_failure(abort_code = 524308, location = ccip::rmn_remote)] + fun test_initialize_allowed_cursers_v2_already_initialized( + ccip: &signer, owner: &signer + ) { + setup(ccip, owner); + + // Initialize once + rmn_remote::initialize_allowed_cursers_v2(owner, vector[CURSER_1]); + + // E_ALLOWED_CURSERS_V2_ALREADY_INITIALIZED - second initialization should fail + rmn_remote::initialize_allowed_cursers_v2(owner, vector[CURSER_2]); + } + + #[test(ccip = @ccip, owner = @mcms)] + #[expected_failure(abort_code = 327683, location = ccip::ownable)] + fun test_initialize_allowed_cursers_v2_unauthorized( + ccip: &signer, owner: &signer + ) { + setup(ccip, owner); + + let unauthorized = + account::create_signer_with_capability( + &account::create_test_signer_cap(UNAUTHORIZED) + ); + + // E_ONLY_CALLABLE_BY_OWNER - non-owner cannot initialize + rmn_remote::initialize_allowed_cursers_v2(&unauthorized, vector[CURSER_1]); + } + + // ================================================================ + // | Add/Remove Allowed Cursers Tests | + // ================================================================ + + #[test(ccip = @ccip, owner = @mcms)] + fun test_add_allowed_cursers(ccip: &signer, owner: &signer) { + setup(ccip, owner); + + // Initialize first + rmn_remote::initialize_allowed_cursers_v2(owner, vector[]); + + // Add cursers + rmn_remote::add_allowed_cursers(owner, vector[CURSER_1, CURSER_2]); + + // Verify + assert!(rmn_remote::is_allowed_curser(CURSER_1), 0); + assert!(rmn_remote::is_allowed_curser(CURSER_2), 1); + assert!(!rmn_remote::is_allowed_curser(CURSER_3), 2); + + let cursers = rmn_remote::get_allowed_cursers(); + assert!(vector::length(&cursers) == 2, 3); + } + + #[test(ccip = @ccip, owner = @mcms)] + fun test_remove_allowed_cursers(ccip: &signer, owner: &signer) { + setup(ccip, owner); + + // Initialize with cursers + rmn_remote::initialize_allowed_cursers_v2(owner, vector[CURSER_1, CURSER_2]); + + // Remove one + rmn_remote::remove_allowed_cursers(owner, vector[CURSER_1]); + + // Verify + assert!(!rmn_remote::is_allowed_curser(CURSER_1), 0); + assert!(rmn_remote::is_allowed_curser(CURSER_2), 1); + + let cursers = rmn_remote::get_allowed_cursers(); + assert!(vector::length(&cursers) == 1, 2); + } + + #[test(ccip = @ccip, owner = @mcms)] + #[expected_failure(abort_code = 196629, location = ccip::rmn_remote)] + fun test_add_allowed_cursers_not_initialized( + ccip: &signer, owner: &signer + ) { + setup(ccip, owner); + + // E_ALLOWED_CURSERS_V2_NOT_INITIALIZED - cannot add without initialization + rmn_remote::add_allowed_cursers(owner, vector[CURSER_1]); + } + + #[test(ccip = @ccip, owner = @mcms)] + #[expected_failure(abort_code = 196629, location = ccip::rmn_remote)] + fun test_remove_allowed_cursers_not_initialized( + ccip: &signer, owner: &signer + ) { + setup(ccip, owner); + + // E_ALLOWED_CURSERS_V2_NOT_INITIALIZED - cannot remove without initialization + rmn_remote::remove_allowed_cursers(owner, vector[CURSER_1]); + } + + #[test(ccip = @ccip, owner = @mcms)] + #[expected_failure(abort_code = 524310, location = ccip::rmn_remote)] + fun test_add_allowed_cursers_already_allowed( + ccip: &signer, owner: &signer + ) { + setup(ccip, owner); + + rmn_remote::initialize_allowed_cursers_v2(owner, vector[CURSER_1]); + + // E_CURSER_ALREADY_ALLOWED - cannot add duplicate + rmn_remote::add_allowed_cursers(owner, vector[CURSER_1]); + } + + #[test(ccip = @ccip, owner = @mcms)] + #[expected_failure(abort_code = 393239, location = ccip::rmn_remote)] + fun test_remove_allowed_cursers_not_allowed( + ccip: &signer, owner: &signer + ) { + setup(ccip, owner); + + rmn_remote::initialize_allowed_cursers_v2(owner, vector[CURSER_1]); + + // E_CURSER_NOT_ALLOWED - cannot remove non-existent curser + rmn_remote::remove_allowed_cursers(owner, vector[CURSER_2]); + } + + #[test(ccip = @ccip, owner = @mcms)] + #[expected_failure(abort_code = 327683, location = ccip::ownable)] + fun test_add_allowed_cursers_unauthorized( + ccip: &signer, owner: &signer + ) { + setup(ccip, owner); + + rmn_remote::initialize_allowed_cursers_v2(owner, vector[]); + + let unauthorized = + account::create_signer_with_capability( + &account::create_test_signer_cap(UNAUTHORIZED) + ); + + // E_ONLY_CALLABLE_BY_OWNER - non-owner cannot add + rmn_remote::add_allowed_cursers(&unauthorized, vector[CURSER_1]); + } + + #[test(ccip = @ccip, owner = @mcms)] + #[expected_failure(abort_code = 327683, location = ccip::ownable)] + fun test_remove_allowed_cursers_unauthorized( + ccip: &signer, owner: &signer + ) { + setup(ccip, owner); + + rmn_remote::initialize_allowed_cursers_v2(owner, vector[CURSER_1]); + + let unauthorized = + account::create_signer_with_capability( + &account::create_test_signer_cap(UNAUTHORIZED) + ); + + // E_ONLY_CALLABLE_BY_OWNER - non-owner cannot remove + rmn_remote::remove_allowed_cursers(&unauthorized, vector[CURSER_1]); + } + + // ================================================================ + // | Curse/Uncurse Authorization Tests | + // ================================================================ + + #[test(ccip = @ccip, owner = @mcms)] + fun test_curse_by_owner(ccip: &signer, owner: &signer) { + setup(ccip, owner); + + // Owner can curse without AllowedCursersV2 initialized (backward compat) + rmn_remote::curse(owner, SUBJECT_1); + + assert!(rmn_remote::is_cursed(SUBJECT_1), 0); + } + + #[test(ccip = @ccip, owner = @mcms)] + fun test_curse_by_owner_with_v2_initialized( + ccip: &signer, owner: &signer + ) { + setup(ccip, owner); + + // Initialize V2 + rmn_remote::initialize_allowed_cursers_v2(owner, vector[CURSER_1]); + + // Owner can still curse + rmn_remote::curse(owner, SUBJECT_1); + + assert!(rmn_remote::is_cursed(SUBJECT_1), 0); + } + + #[test(ccip = @ccip, owner = @mcms)] + fun test_curse_by_allowed_curser(ccip: &signer, owner: &signer) { + setup(ccip, owner); + + // Initialize V2 with CURSER_1 + rmn_remote::initialize_allowed_cursers_v2(owner, vector[CURSER_1]); + + let curser = + account::create_signer_with_capability( + &account::create_test_signer_cap(CURSER_1) + ); + + // Allowed curser can curse + rmn_remote::curse(&curser, SUBJECT_1); + + assert!(rmn_remote::is_cursed(SUBJECT_1), 0); + } + + #[test(ccip = @ccip, owner = @mcms)] + fun test_curse_multiple_by_allowed_curser( + ccip: &signer, owner: &signer + ) { + setup(ccip, owner); + + rmn_remote::initialize_allowed_cursers_v2(owner, vector[CURSER_1]); + + let curser = + account::create_signer_with_capability( + &account::create_test_signer_cap(CURSER_1) + ); + + // Allowed curser can curse multiple subjects + rmn_remote::curse_multiple(&curser, vector[SUBJECT_1, SUBJECT_2]); + + assert!(rmn_remote::is_cursed(SUBJECT_1), 0); + assert!(rmn_remote::is_cursed(SUBJECT_2), 1); + } + + #[test(ccip = @ccip, owner = @mcms)] + #[expected_failure(abort_code = 327699, location = ccip::rmn_remote)] + fun test_curse_by_unauthorized(ccip: &signer, owner: &signer) { + setup(ccip, owner); + + // Initialize V2 but don't add UNAUTHORIZED + rmn_remote::initialize_allowed_cursers_v2(owner, vector[CURSER_1]); + + let unauthorized = + account::create_signer_with_capability( + &account::create_test_signer_cap(UNAUTHORIZED) + ); + + // E_NOT_OWNER_OR_ALLOWED_CURSER - unauthorized cannot curse + rmn_remote::curse(&unauthorized, SUBJECT_1); + } + + #[test(ccip = @ccip, owner = @mcms)] + #[expected_failure(abort_code = 327699, location = ccip::rmn_remote)] + fun test_curse_by_unauthorized_v1(ccip: &signer, owner: &signer) { + setup(ccip, owner); + + // V1 behavior: without AllowedCursersV2, only owner can curse + let unauthorized = + account::create_signer_with_capability( + &account::create_test_signer_cap(UNAUTHORIZED) + ); + + // Since V2 is not initialized, is_allowed_curser returns false + // and caller is not owner, so it fails with E_NOT_OWNER_OR_ALLOWED_CURSER (19) + rmn_remote::curse(&unauthorized, SUBJECT_1); + } + + #[test(ccip = @ccip, owner = @mcms)] + fun test_uncurse_by_owner(ccip: &signer, owner: &signer) { + setup(ccip, owner); + + // Curse first + rmn_remote::curse(owner, SUBJECT_1); + assert!(rmn_remote::is_cursed(SUBJECT_1), 0); + + // Owner can uncurse + rmn_remote::uncurse(owner, SUBJECT_1); + assert!(!rmn_remote::is_cursed(SUBJECT_1), 1); + } + + #[test(ccip = @ccip, owner = @mcms)] + fun test_uncurse_by_allowed_curser(ccip: &signer, owner: &signer) { + setup(ccip, owner); + + rmn_remote::initialize_allowed_cursers_v2(owner, vector[CURSER_1]); + + // Owner curses + rmn_remote::curse(owner, SUBJECT_1); + assert!(rmn_remote::is_cursed(SUBJECT_1), 0); + + let curser = + account::create_signer_with_capability( + &account::create_test_signer_cap(CURSER_1) + ); + + // Allowed curser can uncurse + rmn_remote::uncurse(&curser, SUBJECT_1); + assert!(!rmn_remote::is_cursed(SUBJECT_1), 1); + } + + #[test(ccip = @ccip, owner = @mcms)] + fun test_uncurse_multiple_by_allowed_curser( + ccip: &signer, owner: &signer + ) { + setup(ccip, owner); + + rmn_remote::initialize_allowed_cursers_v2(owner, vector[CURSER_1]); + + // Curse multiple + rmn_remote::curse_multiple(owner, vector[SUBJECT_1, SUBJECT_2]); + + let curser = + account::create_signer_with_capability( + &account::create_test_signer_cap(CURSER_1) + ); + + // Allowed curser can uncurse multiple + rmn_remote::uncurse_multiple(&curser, vector[SUBJECT_1, SUBJECT_2]); + + assert!(!rmn_remote::is_cursed(SUBJECT_1), 0); + assert!(!rmn_remote::is_cursed(SUBJECT_2), 1); + } + + #[test(ccip = @ccip, owner = @mcms)] + #[expected_failure(abort_code = 327699, location = ccip::rmn_remote)] + fun test_uncurse_by_unauthorized(ccip: &signer, owner: &signer) { + setup(ccip, owner); + + rmn_remote::initialize_allowed_cursers_v2(owner, vector[CURSER_1]); + + // Curse first + rmn_remote::curse(owner, SUBJECT_1); + + let unauthorized = + account::create_signer_with_capability( + &account::create_test_signer_cap(UNAUTHORIZED) + ); + + // E_NOT_OWNER_OR_ALLOWED_CURSER - unauthorized cannot uncurse + rmn_remote::uncurse(&unauthorized, SUBJECT_1); + } + + #[test(ccip = @ccip, owner = @mcms)] + #[expected_failure(abort_code = 327699, location = ccip::rmn_remote)] + fun test_curse_multiple_by_unauthorized( + ccip: &signer, owner: &signer + ) { + setup(ccip, owner); + + // Initialize V2 but don't add UNAUTHORIZED + rmn_remote::initialize_allowed_cursers_v2(owner, vector[CURSER_1]); + + let unauthorized = + account::create_signer_with_capability( + &account::create_test_signer_cap(UNAUTHORIZED) + ); + + // E_NOT_OWNER_OR_ALLOWED_CURSER - unauthorized cannot curse_multiple + rmn_remote::curse_multiple(&unauthorized, vector[SUBJECT_1, SUBJECT_2]); + } + + #[test(ccip = @ccip, owner = @mcms)] + #[expected_failure(abort_code = 327699, location = ccip::rmn_remote)] + fun test_uncurse_multiple_by_unauthorized( + ccip: &signer, owner: &signer + ) { + setup(ccip, owner); + + rmn_remote::initialize_allowed_cursers_v2(owner, vector[CURSER_1]); + + // Curse multiple subjects first + rmn_remote::curse_multiple(owner, vector[SUBJECT_1, SUBJECT_2]); + + let unauthorized = + account::create_signer_with_capability( + &account::create_test_signer_cap(UNAUTHORIZED) + ); + + // E_NOT_OWNER_OR_ALLOWED_CURSER - unauthorized cannot uncurse_multiple + rmn_remote::uncurse_multiple(&unauthorized, vector[SUBJECT_1, SUBJECT_2]); + } + + // ================================================================ + // | View Functions Tests | + // ================================================================ + + #[test(ccip = @ccip, owner = @mcms)] + fun test_is_allowed_curser_without_v2(ccip: &signer, owner: &signer) { + setup(ccip, owner); + + // Without V2 initialized, everyone returns false + assert!(!rmn_remote::is_allowed_curser(CURSER_1), 0); + assert!(!rmn_remote::is_allowed_curser(OWNER), 1); + } + + #[test(ccip = @ccip, owner = @mcms)] + fun test_get_allowed_cursers_without_v2( + ccip: &signer, owner: &signer + ) { + setup(ccip, owner); + + // Without V2 initialized, returns empty vector + let cursers = rmn_remote::get_allowed_cursers(); + assert!(vector::length(&cursers) == 0, 0); + } + + #[test(ccip = @ccip, owner = @mcms)] + fun test_get_allowed_cursers_with_v2(ccip: &signer, owner: &signer) { + setup(ccip, owner); + + rmn_remote::initialize_allowed_cursers_v2(owner, vector[CURSER_1, CURSER_2]); + + let cursers = rmn_remote::get_allowed_cursers(); + assert!(vector::length(&cursers) == 2, 0); + assert!(vector::contains(&cursers, &CURSER_1), 1); + assert!(vector::contains(&cursers, &CURSER_2), 2); + } + + // ================================================================ + // | Curser Lifecycle Test | + // ================================================================ + + #[test(ccip = @ccip, owner = @mcms)] + fun test_curser_lifecycle(ccip: &signer, owner: &signer) { + setup(ccip, owner); + + // 1. Initialize with one curser + rmn_remote::initialize_allowed_cursers_v2(owner, vector[CURSER_1]); + assert!(rmn_remote::is_allowed_curser(CURSER_1), 0); + + // 2. Curser can curse + let curser1 = + account::create_signer_with_capability( + &account::create_test_signer_cap(CURSER_1) + ); + rmn_remote::curse(&curser1, SUBJECT_1); + assert!(rmn_remote::is_cursed(SUBJECT_1), 1); + + // 3. Add another curser + rmn_remote::add_allowed_cursers(owner, vector[CURSER_2]); + assert!(rmn_remote::is_allowed_curser(CURSER_2), 2); + + // 4. New curser can also curse + let curser2 = + account::create_signer_with_capability( + &account::create_test_signer_cap(CURSER_2) + ); + rmn_remote::curse(&curser2, SUBJECT_2); + assert!(rmn_remote::is_cursed(SUBJECT_2), 3); + + // 5. Remove first curser + rmn_remote::remove_allowed_cursers(owner, vector[CURSER_1]); + assert!(!rmn_remote::is_allowed_curser(CURSER_1), 4); + + // 6. Second curser can uncurse both + rmn_remote::uncurse_multiple(&curser2, vector[SUBJECT_1, SUBJECT_2]); + assert!(!rmn_remote::is_cursed(SUBJECT_1), 5); + assert!(!rmn_remote::is_cursed(SUBJECT_2), 6); + } +}