diff --git a/bindings/bindings.go b/bindings/bindings.go index af0c1899..cbc9a200 100644 --- a/bindings/bindings.go +++ b/bindings/bindings.go @@ -13,6 +13,7 @@ import ( "github.com/smartcontractkit/chainlink-aptos/bindings/ccip_token_pools/regulated_token_pool" "github.com/smartcontractkit/chainlink-aptos/bindings/ccip_token_pools/token_pool" "github.com/smartcontractkit/chainlink-aptos/bindings/ccip_token_pools/usdc_token_pool" + "github.com/smartcontractkit/chainlink-aptos/bindings/curse_mcms" "github.com/smartcontractkit/chainlink-aptos/bindings/data_feeds" "github.com/smartcontractkit/chainlink-aptos/bindings/managed_token" "github.com/smartcontractkit/chainlink-aptos/bindings/managed_token_faucet" @@ -36,6 +37,7 @@ var GlobalFunctionInfo = bind.CombineFunctionInfos( managed_token_pool.FunctionInfo, token_pool.FunctionInfo, usdc_token_pool.FunctionInfo, + curse_mcms.FunctionInfo, data_feeds.FunctionInfo, managed_token.FunctionInfo, managed_token_faucet.FunctionInfo, diff --git a/bindings/curse_mcms/curse_mcms.go b/bindings/curse_mcms/curse_mcms.go new file mode 100644 index 00000000..8bcbd4c3 --- /dev/null +++ b/bindings/curse_mcms/curse_mcms.go @@ -0,0 +1,58 @@ +package curse_mcms + +import ( + "github.com/aptos-labs/aptos-go-sdk" + + "github.com/smartcontractkit/chainlink-aptos/bindings/bind" + "github.com/smartcontractkit/chainlink-aptos/bindings/compile" + module_curse_mcms "github.com/smartcontractkit/chainlink-aptos/bindings/curse_mcms/curse_mcms" + "github.com/smartcontractkit/chainlink-aptos/contracts" +) + +type CurseMCMS interface { + Address() aptos.AccountAddress + CurseMCMS() module_curse_mcms.CurseMCMSInterface +} + +var _ CurseMCMS = CurseMCMSContract{} + +type CurseMCMSContract struct { + address aptos.AccountAddress + curseMcms module_curse_mcms.CurseMCMSInterface +} + +func (c CurseMCMSContract) Address() aptos.AccountAddress { + return c.address +} + +func (c CurseMCMSContract) CurseMCMS() module_curse_mcms.CurseMCMSInterface { + return c.curseMcms +} + +const ( + DefaultSeed = "chainlink_curse_mcms" +) + +var FunctionInfo = bind.MustParseFunctionInfo( + module_curse_mcms.FunctionInfo, +) + +func Bind( + address aptos.AccountAddress, + client aptos.AptosRpcClient, +) CurseMCMS { + return CurseMCMSContract{ + address: address, + curseMcms: module_curse_mcms.NewCurseMCMS(address, client), + } +} + +func Compile(address aptos.AccountAddress, ccipAddress aptos.AccountAddress, mcmsAddress aptos.AccountAddress, mcmsRegisterEntrypointsAddress aptos.AccountAddress) (compile.CompiledPackage, error) { + namedAddresses := map[string]aptos.AccountAddress{ + "curse_mcms": address, + "ccip": ccipAddress, + "mcms": mcmsAddress, + "mcms_register_entrypoints": mcmsRegisterEntrypointsAddress, + } + return compile.CompilePackage(contracts.CurseMCMS, namedAddresses) +} diff --git a/bindings/curse_mcms/curse_mcms/curse_mcms.go b/bindings/curse_mcms/curse_mcms/curse_mcms.go new file mode 100644 index 00000000..6f49247a --- /dev/null +++ b/bindings/curse_mcms/curse_mcms/curse_mcms.go @@ -0,0 +1,1438 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package module_curse_mcms + +import ( + "math/big" + + "github.com/aptos-labs/aptos-go-sdk" + "github.com/aptos-labs/aptos-go-sdk/api" + + "github.com/smartcontractkit/chainlink-aptos/bindings/bind" + "github.com/smartcontractkit/chainlink-aptos/relayer/codec" +) + +var ( + _ = aptos.AccountAddress{} + _ = api.PendingTransaction{} + _ = big.NewInt + _ = bind.NewBoundContract + _ = codec.DecodeAptosJsonValue +) + +type CurseMCMSInterface interface { + SeenSignedHashes(opts *bind.CallOpts, multisig aptos.AccountAddress) ([][]byte, error) + ExpiringRootAndOpCount(opts *bind.CallOpts, multisig aptos.AccountAddress) ([]byte, uint64, uint64, error) + RootMetadata(opts *bind.CallOpts, multisig aptos.AccountAddress) (RootMetadata, error) + GetRootMetadata(opts *bind.CallOpts, role byte) (RootMetadata, error) + GetOpCount(opts *bind.CallOpts, role byte) (uint64, error) + GetRoot(opts *bind.CallOpts, role byte) ([]byte, uint64, error) + GetConfig(opts *bind.CallOpts, role byte) (Config, error) + Signers(opts *bind.CallOpts, multisig aptos.AccountAddress) ([]Signer, error) + MultisigObject(opts *bind.CallOpts, role byte) (aptos.AccountAddress, error) + NumGroups(opts *bind.CallOpts) (uint64, error) + MaxNumSigners(opts *bind.CallOpts) (uint64, error) + BypasserRole(opts *bind.CallOpts) (byte, error) + CancellerRole(opts *bind.CallOpts) (byte, error) + ProposerRole(opts *bind.CallOpts) (byte, error) + TimelockRole(opts *bind.CallOpts) (byte, error) + IsValidRole(opts *bind.CallOpts, role byte) (bool, error) + ZeroHash(opts *bind.CallOpts) ([]byte, error) + TimelockGetBlockedFunction(opts *bind.CallOpts, index uint64) (string, error) + TimelockIsOperation(opts *bind.CallOpts, id []byte) (bool, error) + TimelockIsOperationPending(opts *bind.CallOpts, id []byte) (bool, error) + TimelockIsOperationReady(opts *bind.CallOpts, id []byte) (bool, error) + TimelockIsOperationDone(opts *bind.CallOpts, id []byte) (bool, error) + TimelockGetTimestamp(opts *bind.CallOpts, id []byte) (uint64, error) + TimelockMinDelay(opts *bind.CallOpts) (uint64, error) + TimelockGetBlockedFunctions(opts *bind.CallOpts) ([]string, error) + TimelockGetBlockedFunctionsCount(opts *bind.CallOpts) (uint64, error) + + SetRoot(opts *bind.TransactOpts, role byte, root []byte, validUntil uint64, chainId *big.Int, multisigAddr aptos.AccountAddress, preOpCount uint64, postOpCount uint64, overridePreviousRoot bool, metadataProof [][]byte, signatures [][]byte) (*api.PendingTransaction, error) + Execute(opts *bind.TransactOpts, role byte, chainId *big.Int, multisigAddr aptos.AccountAddress, nonce uint64, to aptos.AccountAddress, moduleName string, functionName string, data []byte, proof [][]byte) (*api.PendingTransaction, error) + SetConfig(opts *bind.TransactOpts, role byte, signerAddresses [][]byte, signerGroups []byte, groupQuorums []byte, groupParents []byte, clearRoot bool) (*api.PendingTransaction, error) + TimelockExecuteBatch(opts *bind.TransactOpts, subjects [][]byte, functionNames []string, datas [][]byte, predecessor []byte, salt []byte) (*api.PendingTransaction, error) + + // Encoder returns the encoder implementation of this module. + Encoder() CurseMCMSEncoder +} + +type CurseMCMSEncoder interface { + SeenSignedHashes(multisig aptos.AccountAddress) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + ExpiringRootAndOpCount(multisig aptos.AccountAddress) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + RootMetadata(multisig aptos.AccountAddress) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + GetRootMetadata(role byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + GetOpCount(role byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + GetRoot(role byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + GetConfig(role byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + Signers(multisig aptos.AccountAddress) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + MultisigObject(role byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + NumGroups() (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + MaxNumSigners() (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + BypasserRole() (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + CancellerRole() (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + ProposerRole() (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + TimelockRole() (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + IsValidRole(role byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + ZeroHash() (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + TimelockGetBlockedFunction(index uint64) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + TimelockIsOperation(id []byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + TimelockIsOperationPending(id []byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + TimelockIsOperationReady(id []byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + TimelockIsOperationDone(id []byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + TimelockGetTimestamp(id []byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + TimelockMinDelay() (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + TimelockGetBlockedFunctions() (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + TimelockGetBlockedFunctionsCount() (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + SetRoot(role byte, root []byte, validUntil uint64, chainId *big.Int, multisigAddr aptos.AccountAddress, preOpCount uint64, postOpCount uint64, overridePreviousRoot bool, metadataProof [][]byte, signatures [][]byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + Execute(role byte, chainId *big.Int, multisigAddr aptos.AccountAddress, nonce uint64, to aptos.AccountAddress, moduleName string, functionName string, data []byte, proof [][]byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + SetConfig(role byte, signerAddresses [][]byte, signerGroups []byte, groupQuorums []byte, groupParents []byte, clearRoot bool) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + TimelockExecuteBatch(subjects [][]byte, functionNames []string, datas [][]byte, predecessor []byte, salt []byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + CreateMultisig(role byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + EcdsaRecoverEvmAddr(ethSignedMessageHash []byte, signature []byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + DispatchToTimelock(role byte, functionName string, data []byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + VerifyMerkleProof(proof [][]byte, root []byte, leaf []byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + ComputeEthMessageHash(root []byte, validUntil uint64) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + HashOpLeaf(domainSeparator []byte, op Op) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + HashMetadataLeaf(metadata RootMetadata) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + Role(rootMetadata RootMetadata) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + ChainId(rootMetadata RootMetadata) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + RootMetadataMultisig(rootMetadata RootMetadata) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + PreOpCount(rootMetadata RootMetadata) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + PostOpCount(rootMetadata RootMetadata) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + OverridePreviousRoot(rootMetadata RootMetadata) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + TimelockScheduleBatch(subjects [][]byte, functionNames []string, datas [][]byte, predecessor []byte, salt []byte, delay uint64) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + TimelockBeforeCall(id []byte, predecessor []byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + TimelockAfterCall(id []byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + TimelockBypasserExecuteBatch(subjects [][]byte, functionNames []string, datas [][]byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + TimelockDispatchToRMNRemote(functionName string, data []byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + TimelockCancel(id []byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + TimelockUpdateMinDelay(newMinDelay uint64) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + TimelockBlockFunction(functionName string) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + TimelockUnblockFunction(functionName string) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + CreateCalls(subjects [][]byte, functionNames []string, datas [][]byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + HashOperationBatch(calls []Call, predecessor []byte, salt []byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + FunctionName(call Call) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + Subject(call Call) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + Data(call Call) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) +} + +const FunctionInfo = `[{"package":"curse_mcms","module":"curse_mcms","name":"chain_id","parameters":[{"name":"root_metadata","type":"RootMetadata"}]},{"package":"curse_mcms","module":"curse_mcms","name":"compute_eth_message_hash","parameters":[{"name":"root","type":"vector\u003cu8\u003e"},{"name":"valid_until","type":"u64"}]},{"package":"curse_mcms","module":"curse_mcms","name":"create_calls","parameters":[{"name":"subjects","type":"vector\u003cvector\u003cu8\u003e\u003e"},{"name":"function_names","type":"vector\u003c0x1::string::String\u003e"},{"name":"datas","type":"vector\u003cvector\u003cu8\u003e\u003e"}]},{"package":"curse_mcms","module":"curse_mcms","name":"create_multisig","parameters":[{"name":"role","type":"u8"}]},{"package":"curse_mcms","module":"curse_mcms","name":"data","parameters":[{"name":"call","type":"Call"}]},{"package":"curse_mcms","module":"curse_mcms","name":"dispatch_to_timelock","parameters":[{"name":"role","type":"u8"},{"name":"function_name","type":"0x1::string::String"},{"name":"data","type":"vector\u003cu8\u003e"}]},{"package":"curse_mcms","module":"curse_mcms","name":"ecdsa_recover_evm_addr","parameters":[{"name":"eth_signed_message_hash","type":"vector\u003cu8\u003e"},{"name":"signature","type":"vector\u003cu8\u003e"}]},{"package":"curse_mcms","module":"curse_mcms","name":"execute","parameters":[{"name":"role","type":"u8"},{"name":"chain_id","type":"u256"},{"name":"multisig_addr","type":"address"},{"name":"nonce","type":"u64"},{"name":"to","type":"address"},{"name":"module_name","type":"0x1::string::String"},{"name":"function_name","type":"0x1::string::String"},{"name":"data","type":"vector\u003cu8\u003e"},{"name":"proof","type":"vector\u003cvector\u003cu8\u003e\u003e"}]},{"package":"curse_mcms","module":"curse_mcms","name":"function_name","parameters":[{"name":"call","type":"Call"}]},{"package":"curse_mcms","module":"curse_mcms","name":"hash_metadata_leaf","parameters":[{"name":"metadata","type":"RootMetadata"}]},{"package":"curse_mcms","module":"curse_mcms","name":"hash_op_leaf","parameters":[{"name":"domain_separator","type":"vector\u003cu8\u003e"},{"name":"op","type":"Op"}]},{"package":"curse_mcms","module":"curse_mcms","name":"hash_operation_batch","parameters":[{"name":"calls","type":"vector\u003cCall\u003e"},{"name":"predecessor","type":"vector\u003cu8\u003e"},{"name":"salt","type":"vector\u003cu8\u003e"}]},{"package":"curse_mcms","module":"curse_mcms","name":"override_previous_root","parameters":[{"name":"root_metadata","type":"RootMetadata"}]},{"package":"curse_mcms","module":"curse_mcms","name":"post_op_count","parameters":[{"name":"root_metadata","type":"RootMetadata"}]},{"package":"curse_mcms","module":"curse_mcms","name":"pre_op_count","parameters":[{"name":"root_metadata","type":"RootMetadata"}]},{"package":"curse_mcms","module":"curse_mcms","name":"role","parameters":[{"name":"root_metadata","type":"RootMetadata"}]},{"package":"curse_mcms","module":"curse_mcms","name":"root_metadata_multisig","parameters":[{"name":"root_metadata","type":"RootMetadata"}]},{"package":"curse_mcms","module":"curse_mcms","name":"set_config","parameters":[{"name":"role","type":"u8"},{"name":"signer_addresses","type":"vector\u003cvector\u003cu8\u003e\u003e"},{"name":"signer_groups","type":"vector\u003cu8\u003e"},{"name":"group_quorums","type":"vector\u003cu8\u003e"},{"name":"group_parents","type":"vector\u003cu8\u003e"},{"name":"clear_root","type":"bool"}]},{"package":"curse_mcms","module":"curse_mcms","name":"set_root","parameters":[{"name":"role","type":"u8"},{"name":"root","type":"vector\u003cu8\u003e"},{"name":"valid_until","type":"u64"},{"name":"chain_id","type":"u256"},{"name":"multisig_addr","type":"address"},{"name":"pre_op_count","type":"u64"},{"name":"post_op_count","type":"u64"},{"name":"override_previous_root","type":"bool"},{"name":"metadata_proof","type":"vector\u003cvector\u003cu8\u003e\u003e"},{"name":"signatures","type":"vector\u003cvector\u003cu8\u003e\u003e"}]},{"package":"curse_mcms","module":"curse_mcms","name":"subject","parameters":[{"name":"call","type":"Call"}]},{"package":"curse_mcms","module":"curse_mcms","name":"timelock_after_call","parameters":[{"name":"id","type":"vector\u003cu8\u003e"}]},{"package":"curse_mcms","module":"curse_mcms","name":"timelock_before_call","parameters":[{"name":"id","type":"vector\u003cu8\u003e"},{"name":"predecessor","type":"vector\u003cu8\u003e"}]},{"package":"curse_mcms","module":"curse_mcms","name":"timelock_block_function","parameters":[{"name":"function_name","type":"0x1::string::String"}]},{"package":"curse_mcms","module":"curse_mcms","name":"timelock_bypasser_execute_batch","parameters":[{"name":"subjects","type":"vector\u003cvector\u003cu8\u003e\u003e"},{"name":"function_names","type":"vector\u003c0x1::string::String\u003e"},{"name":"datas","type":"vector\u003cvector\u003cu8\u003e\u003e"}]},{"package":"curse_mcms","module":"curse_mcms","name":"timelock_cancel","parameters":[{"name":"id","type":"vector\u003cu8\u003e"}]},{"package":"curse_mcms","module":"curse_mcms","name":"timelock_dispatch_to_rmn_remote","parameters":[{"name":"function_name","type":"0x1::string::String"},{"name":"data","type":"vector\u003cu8\u003e"}]},{"package":"curse_mcms","module":"curse_mcms","name":"timelock_execute_batch","parameters":[{"name":"subjects","type":"vector\u003cvector\u003cu8\u003e\u003e"},{"name":"function_names","type":"vector\u003c0x1::string::String\u003e"},{"name":"datas","type":"vector\u003cvector\u003cu8\u003e\u003e"},{"name":"predecessor","type":"vector\u003cu8\u003e"},{"name":"salt","type":"vector\u003cu8\u003e"}]},{"package":"curse_mcms","module":"curse_mcms","name":"timelock_schedule_batch","parameters":[{"name":"subjects","type":"vector\u003cvector\u003cu8\u003e\u003e"},{"name":"function_names","type":"vector\u003c0x1::string::String\u003e"},{"name":"datas","type":"vector\u003cvector\u003cu8\u003e\u003e"},{"name":"predecessor","type":"vector\u003cu8\u003e"},{"name":"salt","type":"vector\u003cu8\u003e"},{"name":"delay","type":"u64"}]},{"package":"curse_mcms","module":"curse_mcms","name":"timelock_unblock_function","parameters":[{"name":"function_name","type":"0x1::string::String"}]},{"package":"curse_mcms","module":"curse_mcms","name":"timelock_update_min_delay","parameters":[{"name":"new_min_delay","type":"u64"}]},{"package":"curse_mcms","module":"curse_mcms","name":"verify_merkle_proof","parameters":[{"name":"proof","type":"vector\u003cvector\u003cu8\u003e\u003e"},{"name":"root","type":"vector\u003cu8\u003e"},{"name":"leaf","type":"vector\u003cu8\u003e"}]}]` + +func NewCurseMCMS(address aptos.AccountAddress, client aptos.AptosRpcClient) CurseMCMSInterface { + contract := bind.NewBoundContract(address, "curse_mcms", "curse_mcms", client) + return CurseMCMSContract{ + BoundContract: contract, + curseMCMSEncoder: curseMCMSEncoder{BoundContract: contract}, + } +} + +// Constants +const ( + BYPASSER_ROLE byte = 0 + CANCELLER_ROLE byte = 1 + PROPOSER_ROLE byte = 2 + TIMELOCK_ROLE byte = 3 + MAX_ROLE byte = 4 + NUM_GROUPS uint64 = 32 + MAX_NUM_SIGNERS uint64 = 200 + DONE_TIMESTAMP uint64 = 1 + E_ALREADY_SEEN_HASH uint64 = 1 + E_POST_OP_COUNT_REACHED uint64 = 2 + E_WRONG_CHAIN_ID uint64 = 3 + E_WRONG_MULTISIG uint64 = 4 + E_ROOT_EXPIRED uint64 = 5 + E_WRONG_NONCE uint64 = 6 + E_VALID_UNTIL_EXPIRED uint64 = 7 + E_INVALID_SIGNER uint64 = 8 + E_MISSING_CONFIG uint64 = 9 + E_INSUFFICIENT_SIGNERS uint64 = 10 + E_PROOF_CANNOT_BE_VERIFIED uint64 = 11 + E_PENDING_OPS uint64 = 12 + E_WRONG_PRE_OP_COUNT uint64 = 13 + E_WRONG_POST_OP_COUNT uint64 = 14 + E_INVALID_NUM_SIGNERS uint64 = 15 + E_SIGNER_GROUPS_LEN_MISMATCH uint64 = 16 + E_INVALID_GROUP_QUORUM_LEN uint64 = 17 + E_INVALID_GROUP_PARENTS_LEN uint64 = 18 + E_OUT_OF_BOUNDS_GROUP uint64 = 19 + E_GROUP_TREE_NOT_WELL_FORMED uint64 = 20 + E_SIGNER_IN_DISABLED_GROUP uint64 = 21 + E_OUT_OF_BOUNDS_GROUP_QUORUM uint64 = 22 + E_SIGNER_ADDR_MUST_BE_INCREASING uint64 = 23 + E_INVALID_SIGNER_ADDR_LEN uint64 = 24 + E_UNKNOWN_CURSE_MCMS_FUNCTION uint64 = 25 + E_NOT_BYPASSER_ROLE uint64 = 29 + E_INVALID_ROLE uint64 = 30 + E_NOT_AUTHORIZED_ROLE uint64 = 31 + E_NOT_AUTHORIZED uint64 = 32 + E_OPERATION_ALREADY_SCHEDULED uint64 = 33 + E_INSUFFICIENT_DELAY uint64 = 34 + E_OPERATION_NOT_READY uint64 = 35 + E_MISSING_DEPENDENCY uint64 = 36 + E_OPERATION_CANNOT_BE_CANCELLED uint64 = 37 + E_FUNCTION_BLOCKED uint64 = 38 + E_INVALID_INDEX uint64 = 39 + E_INVALID_PARAMETERS uint64 = 43 + E_INVALID_SIGNATURE_LEN uint64 = 44 + E_INVALID_V_SIGNATURE uint64 = 45 + E_FAILED_ECDSA_RECOVER uint64 = 46 + E_INVALID_MODULE_NAME uint64 = 47 + E_UNKNOWN_CURSE_MCMS_TIMELOCK_FUNCTION uint64 = 48 + E_INVALID_ROOT_LEN uint64 = 49 + E_NOT_CANCELLER_ROLE uint64 = 50 + E_NOT_TIMELOCK_ROLE uint64 = 51 +) + +// Structs + +type MultisigState struct { + Bypasser bind.StdObject `move:"aptos_framework::object::Object"` + Canceller bind.StdObject `move:"aptos_framework::object::Object"` + Proposer bind.StdObject `move:"aptos_framework::object::Object"` +} + +type Multisig struct { + Config Config `move:"Config"` + ExpiringRootAndOpCount ExpiringRootAndOpCount `move:"ExpiringRootAndOpCount"` + RootMetadata RootMetadata `move:"RootMetadata"` +} + +type Op struct { + Role byte `move:"u8"` + ChainId *big.Int `move:"u256"` + Multisig aptos.AccountAddress `move:"address"` + Nonce uint64 `move:"u64"` + To aptos.AccountAddress `move:"address"` + ModuleName string `move:"0x1::string::String"` + FunctionName string `move:"0x1::string::String"` + Data []byte `move:"vector"` +} + +type RootMetadata struct { + Role byte `move:"u8"` + ChainId *big.Int `move:"u256"` + Multisig aptos.AccountAddress `move:"address"` + PreOpCount uint64 `move:"u64"` + PostOpCount uint64 `move:"u64"` + OverridePreviousRoot bool `move:"bool"` +} + +type Signer struct { + Addr []byte `move:"vector"` + Index byte `move:"u8"` + Group byte `move:"u8"` +} + +type Config struct { + Signers []Signer `move:"vector"` + GroupQuorums []byte `move:"vector"` + GroupParents []byte `move:"vector"` +} + +type ExpiringRootAndOpCount struct { + Root []byte `move:"vector"` + ValidUntil uint64 `move:"u64"` + OpCount uint64 `move:"u64"` +} + +type MultisigStateInitialized struct { + Bypasser bind.StdObject `move:"aptos_framework::object::Object"` + Canceller bind.StdObject `move:"aptos_framework::object::Object"` + Proposer bind.StdObject `move:"aptos_framework::object::Object"` +} + +type ConfigSet struct { + Role byte `move:"u8"` + Config Config `move:"Config"` + IsRootCleared bool `move:"bool"` +} + +type NewRoot struct { + Role byte `move:"u8"` + Root []byte `move:"vector"` + ValidUntil uint64 `move:"u64"` + Metadata RootMetadata `move:"RootMetadata"` +} + +type OpExecuted struct { + Role byte `move:"u8"` + ChainId *big.Int `move:"u256"` + Multisig aptos.AccountAddress `move:"address"` + Nonce uint64 `move:"u64"` + To aptos.AccountAddress `move:"address"` + ModuleName string `move:"0x1::string::String"` + FunctionName string `move:"0x1::string::String"` + Data []byte `move:"vector"` +} + +type Timelock struct { + MinDelay uint64 `move:"u64"` +} + +type Call struct { + Subject []byte `move:"vector"` + FunctionName string `move:"0x1::string::String"` + Data []byte `move:"vector"` +} + +type TimelockInitialized struct { + MinDelay uint64 `move:"u64"` +} + +type BypasserCallExecuted struct { + Index uint64 `move:"u64"` + Subject []byte `move:"vector"` + FunctionName string `move:"0x1::string::String"` + Data []byte `move:"vector"` +} + +type Cancelled struct { + Id []byte `move:"vector"` +} + +type CallScheduled struct { + Id []byte `move:"vector"` + Index uint64 `move:"u64"` + Subject []byte `move:"vector"` + FunctionName string `move:"0x1::string::String"` + Data []byte `move:"vector"` + Predecessor []byte `move:"vector"` + Salt []byte `move:"vector"` + Delay uint64 `move:"u64"` +} + +type CallExecuted struct { + Id []byte `move:"vector"` + Index uint64 `move:"u64"` + Subject []byte `move:"vector"` + FunctionName string `move:"0x1::string::String"` + Data []byte `move:"vector"` +} + +type UpdateMinDelay struct { + OldMinDelay uint64 `move:"u64"` + NewMinDelay uint64 `move:"u64"` +} + +type FunctionBlocked struct { + FunctionName string `move:"0x1::string::String"` +} + +type FunctionUnblocked struct { + FunctionName string `move:"0x1::string::String"` +} + +type CurseMCMSContract struct { + *bind.BoundContract + curseMCMSEncoder +} + +var _ CurseMCMSInterface = CurseMCMSContract{} + +func (c CurseMCMSContract) Encoder() CurseMCMSEncoder { + return c.curseMCMSEncoder +} + +// View Functions + +func (c CurseMCMSContract) SeenSignedHashes(opts *bind.CallOpts, multisig aptos.AccountAddress) ([][]byte, error) { + module, function, typeTags, args, err := c.curseMCMSEncoder.SeenSignedHashes(multisig) + if err != nil { + return *new([][]byte), err + } + + callData, err := c.Call(opts, module, function, typeTags, args) + if err != nil { + return *new([][]byte), err + } + + var ( + r0 [][]byte + ) + + if err := codec.DecodeAptosJsonArray(callData, &r0); err != nil { + return *new([][]byte), err + } + return r0, nil +} + +func (c CurseMCMSContract) ExpiringRootAndOpCount(opts *bind.CallOpts, multisig aptos.AccountAddress) ([]byte, uint64, uint64, error) { + module, function, typeTags, args, err := c.curseMCMSEncoder.ExpiringRootAndOpCount(multisig) + if err != nil { + return *new([]byte), *new(uint64), *new(uint64), err + } + + callData, err := c.Call(opts, module, function, typeTags, args) + if err != nil { + return *new([]byte), *new(uint64), *new(uint64), err + } + + var ( + r0 []byte + r1 uint64 + r2 uint64 + ) + + if err := codec.DecodeAptosJsonArray(callData, &r0, &r1, &r2); err != nil { + return *new([]byte), *new(uint64), *new(uint64), err + } + return r0, r1, r2, nil +} + +func (c CurseMCMSContract) RootMetadata(opts *bind.CallOpts, multisig aptos.AccountAddress) (RootMetadata, error) { + module, function, typeTags, args, err := c.curseMCMSEncoder.RootMetadata(multisig) + if err != nil { + return *new(RootMetadata), err + } + + callData, err := c.Call(opts, module, function, typeTags, args) + if err != nil { + return *new(RootMetadata), err + } + + var ( + r0 RootMetadata + ) + + if err := codec.DecodeAptosJsonArray(callData, &r0); err != nil { + return *new(RootMetadata), err + } + return r0, nil +} + +func (c CurseMCMSContract) GetRootMetadata(opts *bind.CallOpts, role byte) (RootMetadata, error) { + module, function, typeTags, args, err := c.curseMCMSEncoder.GetRootMetadata(role) + if err != nil { + return *new(RootMetadata), err + } + + callData, err := c.Call(opts, module, function, typeTags, args) + if err != nil { + return *new(RootMetadata), err + } + + var ( + r0 RootMetadata + ) + + if err := codec.DecodeAptosJsonArray(callData, &r0); err != nil { + return *new(RootMetadata), err + } + return r0, nil +} + +func (c CurseMCMSContract) GetOpCount(opts *bind.CallOpts, role byte) (uint64, error) { + module, function, typeTags, args, err := c.curseMCMSEncoder.GetOpCount(role) + if err != nil { + return *new(uint64), err + } + + callData, err := c.Call(opts, module, function, typeTags, args) + if err != nil { + return *new(uint64), err + } + + var ( + r0 uint64 + ) + + if err := codec.DecodeAptosJsonArray(callData, &r0); err != nil { + return *new(uint64), err + } + return r0, nil +} + +func (c CurseMCMSContract) GetRoot(opts *bind.CallOpts, role byte) ([]byte, uint64, error) { + module, function, typeTags, args, err := c.curseMCMSEncoder.GetRoot(role) + if err != nil { + return *new([]byte), *new(uint64), err + } + + callData, err := c.Call(opts, module, function, typeTags, args) + if err != nil { + return *new([]byte), *new(uint64), err + } + + var ( + r0 []byte + r1 uint64 + ) + + if err := codec.DecodeAptosJsonArray(callData, &r0, &r1); err != nil { + return *new([]byte), *new(uint64), err + } + return r0, r1, nil +} + +func (c CurseMCMSContract) GetConfig(opts *bind.CallOpts, role byte) (Config, error) { + module, function, typeTags, args, err := c.curseMCMSEncoder.GetConfig(role) + if err != nil { + return *new(Config), err + } + + callData, err := c.Call(opts, module, function, typeTags, args) + if err != nil { + return *new(Config), err + } + + var ( + r0 Config + ) + + if err := codec.DecodeAptosJsonArray(callData, &r0); err != nil { + return *new(Config), err + } + return r0, nil +} + +func (c CurseMCMSContract) Signers(opts *bind.CallOpts, multisig aptos.AccountAddress) ([]Signer, error) { + module, function, typeTags, args, err := c.curseMCMSEncoder.Signers(multisig) + if err != nil { + return *new([]Signer), err + } + + callData, err := c.Call(opts, module, function, typeTags, args) + if err != nil { + return *new([]Signer), err + } + + var ( + r0 []Signer + ) + + if err := codec.DecodeAptosJsonArray(callData, &r0); err != nil { + return *new([]Signer), err + } + return r0, nil +} + +func (c CurseMCMSContract) MultisigObject(opts *bind.CallOpts, role byte) (aptos.AccountAddress, error) { + module, function, typeTags, args, err := c.curseMCMSEncoder.MultisigObject(role) + 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 bind.StdObject + ) + + if err := codec.DecodeAptosJsonArray(callData, &r0); err != nil { + return *new(aptos.AccountAddress), err + } + return r0.Address(), nil +} + +func (c CurseMCMSContract) NumGroups(opts *bind.CallOpts) (uint64, error) { + module, function, typeTags, args, err := c.curseMCMSEncoder.NumGroups() + if err != nil { + return *new(uint64), err + } + + callData, err := c.Call(opts, module, function, typeTags, args) + if err != nil { + return *new(uint64), err + } + + var ( + r0 uint64 + ) + + if err := codec.DecodeAptosJsonArray(callData, &r0); err != nil { + return *new(uint64), err + } + return r0, nil +} + +func (c CurseMCMSContract) MaxNumSigners(opts *bind.CallOpts) (uint64, error) { + module, function, typeTags, args, err := c.curseMCMSEncoder.MaxNumSigners() + if err != nil { + return *new(uint64), err + } + + callData, err := c.Call(opts, module, function, typeTags, args) + if err != nil { + return *new(uint64), err + } + + var ( + r0 uint64 + ) + + if err := codec.DecodeAptosJsonArray(callData, &r0); err != nil { + return *new(uint64), err + } + return r0, nil +} + +func (c CurseMCMSContract) BypasserRole(opts *bind.CallOpts) (byte, error) { + module, function, typeTags, args, err := c.curseMCMSEncoder.BypasserRole() + if err != nil { + return *new(byte), err + } + + callData, err := c.Call(opts, module, function, typeTags, args) + if err != nil { + return *new(byte), err + } + + var ( + r0 byte + ) + + if err := codec.DecodeAptosJsonArray(callData, &r0); err != nil { + return *new(byte), err + } + return r0, nil +} + +func (c CurseMCMSContract) CancellerRole(opts *bind.CallOpts) (byte, error) { + module, function, typeTags, args, err := c.curseMCMSEncoder.CancellerRole() + if err != nil { + return *new(byte), err + } + + callData, err := c.Call(opts, module, function, typeTags, args) + if err != nil { + return *new(byte), err + } + + var ( + r0 byte + ) + + if err := codec.DecodeAptosJsonArray(callData, &r0); err != nil { + return *new(byte), err + } + return r0, nil +} + +func (c CurseMCMSContract) ProposerRole(opts *bind.CallOpts) (byte, error) { + module, function, typeTags, args, err := c.curseMCMSEncoder.ProposerRole() + if err != nil { + return *new(byte), err + } + + callData, err := c.Call(opts, module, function, typeTags, args) + if err != nil { + return *new(byte), err + } + + var ( + r0 byte + ) + + if err := codec.DecodeAptosJsonArray(callData, &r0); err != nil { + return *new(byte), err + } + return r0, nil +} + +func (c CurseMCMSContract) TimelockRole(opts *bind.CallOpts) (byte, error) { + module, function, typeTags, args, err := c.curseMCMSEncoder.TimelockRole() + if err != nil { + return *new(byte), err + } + + callData, err := c.Call(opts, module, function, typeTags, args) + if err != nil { + return *new(byte), err + } + + var ( + r0 byte + ) + + if err := codec.DecodeAptosJsonArray(callData, &r0); err != nil { + return *new(byte), err + } + return r0, nil +} + +func (c CurseMCMSContract) IsValidRole(opts *bind.CallOpts, role byte) (bool, error) { + module, function, typeTags, args, err := c.curseMCMSEncoder.IsValidRole(role) + 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 CurseMCMSContract) ZeroHash(opts *bind.CallOpts) ([]byte, error) { + module, function, typeTags, args, err := c.curseMCMSEncoder.ZeroHash() + if err != nil { + return *new([]byte), err + } + + callData, err := c.Call(opts, module, function, typeTags, args) + if err != nil { + return *new([]byte), err + } + + var ( + r0 []byte + ) + + if err := codec.DecodeAptosJsonArray(callData, &r0); err != nil { + return *new([]byte), err + } + return r0, nil +} + +func (c CurseMCMSContract) TimelockGetBlockedFunction(opts *bind.CallOpts, index uint64) (string, error) { + module, function, typeTags, args, err := c.curseMCMSEncoder.TimelockGetBlockedFunction(index) + if err != nil { + return *new(string), err + } + + callData, err := c.Call(opts, module, function, typeTags, args) + if err != nil { + return *new(string), err + } + + var ( + r0 string + ) + + if err := codec.DecodeAptosJsonArray(callData, &r0); err != nil { + return *new(string), err + } + return r0, nil +} + +func (c CurseMCMSContract) TimelockIsOperation(opts *bind.CallOpts, id []byte) (bool, error) { + module, function, typeTags, args, err := c.curseMCMSEncoder.TimelockIsOperation(id) + 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 CurseMCMSContract) TimelockIsOperationPending(opts *bind.CallOpts, id []byte) (bool, error) { + module, function, typeTags, args, err := c.curseMCMSEncoder.TimelockIsOperationPending(id) + 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 CurseMCMSContract) TimelockIsOperationReady(opts *bind.CallOpts, id []byte) (bool, error) { + module, function, typeTags, args, err := c.curseMCMSEncoder.TimelockIsOperationReady(id) + 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 CurseMCMSContract) TimelockIsOperationDone(opts *bind.CallOpts, id []byte) (bool, error) { + module, function, typeTags, args, err := c.curseMCMSEncoder.TimelockIsOperationDone(id) + 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 CurseMCMSContract) TimelockGetTimestamp(opts *bind.CallOpts, id []byte) (uint64, error) { + module, function, typeTags, args, err := c.curseMCMSEncoder.TimelockGetTimestamp(id) + if err != nil { + return *new(uint64), err + } + + callData, err := c.Call(opts, module, function, typeTags, args) + if err != nil { + return *new(uint64), err + } + + var ( + r0 uint64 + ) + + if err := codec.DecodeAptosJsonArray(callData, &r0); err != nil { + return *new(uint64), err + } + return r0, nil +} + +func (c CurseMCMSContract) TimelockMinDelay(opts *bind.CallOpts) (uint64, error) { + module, function, typeTags, args, err := c.curseMCMSEncoder.TimelockMinDelay() + if err != nil { + return *new(uint64), err + } + + callData, err := c.Call(opts, module, function, typeTags, args) + if err != nil { + return *new(uint64), err + } + + var ( + r0 uint64 + ) + + if err := codec.DecodeAptosJsonArray(callData, &r0); err != nil { + return *new(uint64), err + } + return r0, nil +} + +func (c CurseMCMSContract) TimelockGetBlockedFunctions(opts *bind.CallOpts) ([]string, error) { + module, function, typeTags, args, err := c.curseMCMSEncoder.TimelockGetBlockedFunctions() + if err != nil { + return *new([]string), err + } + + callData, err := c.Call(opts, module, function, typeTags, args) + if err != nil { + return *new([]string), err + } + + var ( + r0 []string + ) + + if err := codec.DecodeAptosJsonArray(callData, &r0); err != nil { + return *new([]string), err + } + return r0, nil +} + +func (c CurseMCMSContract) TimelockGetBlockedFunctionsCount(opts *bind.CallOpts) (uint64, error) { + module, function, typeTags, args, err := c.curseMCMSEncoder.TimelockGetBlockedFunctionsCount() + if err != nil { + return *new(uint64), err + } + + callData, err := c.Call(opts, module, function, typeTags, args) + if err != nil { + return *new(uint64), err + } + + var ( + r0 uint64 + ) + + if err := codec.DecodeAptosJsonArray(callData, &r0); err != nil { + return *new(uint64), err + } + return r0, nil +} + +// Entry Functions + +func (c CurseMCMSContract) SetRoot(opts *bind.TransactOpts, role byte, root []byte, validUntil uint64, chainId *big.Int, multisigAddr aptos.AccountAddress, preOpCount uint64, postOpCount uint64, overridePreviousRoot bool, metadataProof [][]byte, signatures [][]byte) (*api.PendingTransaction, error) { + module, function, typeTags, args, err := c.curseMCMSEncoder.SetRoot(role, root, validUntil, chainId, multisigAddr, preOpCount, postOpCount, overridePreviousRoot, metadataProof, signatures) + if err != nil { + return nil, err + } + + return c.BoundContract.Transact(opts, module, function, typeTags, args) +} + +func (c CurseMCMSContract) Execute(opts *bind.TransactOpts, role byte, chainId *big.Int, multisigAddr aptos.AccountAddress, nonce uint64, to aptos.AccountAddress, moduleName string, functionName string, data []byte, proof [][]byte) (*api.PendingTransaction, error) { + module, function, typeTags, args, err := c.curseMCMSEncoder.Execute(role, chainId, multisigAddr, nonce, to, moduleName, functionName, data, proof) + if err != nil { + return nil, err + } + + return c.BoundContract.Transact(opts, module, function, typeTags, args) +} + +func (c CurseMCMSContract) SetConfig(opts *bind.TransactOpts, role byte, signerAddresses [][]byte, signerGroups []byte, groupQuorums []byte, groupParents []byte, clearRoot bool) (*api.PendingTransaction, error) { + module, function, typeTags, args, err := c.curseMCMSEncoder.SetConfig(role, signerAddresses, signerGroups, groupQuorums, groupParents, clearRoot) + if err != nil { + return nil, err + } + + return c.BoundContract.Transact(opts, module, function, typeTags, args) +} + +func (c CurseMCMSContract) TimelockExecuteBatch(opts *bind.TransactOpts, subjects [][]byte, functionNames []string, datas [][]byte, predecessor []byte, salt []byte) (*api.PendingTransaction, error) { + module, function, typeTags, args, err := c.curseMCMSEncoder.TimelockExecuteBatch(subjects, functionNames, datas, predecessor, salt) + if err != nil { + return nil, err + } + + return c.BoundContract.Transact(opts, module, function, typeTags, args) +} + +// Encoder +type curseMCMSEncoder struct { + *bind.BoundContract +} + +func (c curseMCMSEncoder) SeenSignedHashes(multisig aptos.AccountAddress) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("seen_signed_hashes", nil, []string{ + "address", + }, []any{ + multisig, + }) +} + +func (c curseMCMSEncoder) ExpiringRootAndOpCount(multisig aptos.AccountAddress) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("expiring_root_and_op_count", nil, []string{ + "address", + }, []any{ + multisig, + }) +} + +func (c curseMCMSEncoder) RootMetadata(multisig aptos.AccountAddress) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("root_metadata", nil, []string{ + "address", + }, []any{ + multisig, + }) +} + +func (c curseMCMSEncoder) GetRootMetadata(role byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("get_root_metadata", nil, []string{ + "u8", + }, []any{ + role, + }) +} + +func (c curseMCMSEncoder) GetOpCount(role byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("get_op_count", nil, []string{ + "u8", + }, []any{ + role, + }) +} + +func (c curseMCMSEncoder) GetRoot(role byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("get_root", nil, []string{ + "u8", + }, []any{ + role, + }) +} + +func (c curseMCMSEncoder) GetConfig(role byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("get_config", nil, []string{ + "u8", + }, []any{ + role, + }) +} + +func (c curseMCMSEncoder) Signers(multisig aptos.AccountAddress) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("signers", nil, []string{ + "address", + }, []any{ + multisig, + }) +} + +func (c curseMCMSEncoder) MultisigObject(role byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("multisig_object", nil, []string{ + "u8", + }, []any{ + role, + }) +} + +func (c curseMCMSEncoder) NumGroups() (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("num_groups", nil, []string{}, []any{}) +} + +func (c curseMCMSEncoder) MaxNumSigners() (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("max_num_signers", nil, []string{}, []any{}) +} + +func (c curseMCMSEncoder) BypasserRole() (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("bypasser_role", nil, []string{}, []any{}) +} + +func (c curseMCMSEncoder) CancellerRole() (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("canceller_role", nil, []string{}, []any{}) +} + +func (c curseMCMSEncoder) ProposerRole() (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("proposer_role", nil, []string{}, []any{}) +} + +func (c curseMCMSEncoder) TimelockRole() (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("timelock_role", nil, []string{}, []any{}) +} + +func (c curseMCMSEncoder) IsValidRole(role byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("is_valid_role", nil, []string{ + "u8", + }, []any{ + role, + }) +} + +func (c curseMCMSEncoder) ZeroHash() (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("zero_hash", nil, []string{}, []any{}) +} + +func (c curseMCMSEncoder) TimelockGetBlockedFunction(index uint64) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("timelock_get_blocked_function", nil, []string{ + "u64", + }, []any{ + index, + }) +} + +func (c curseMCMSEncoder) TimelockIsOperation(id []byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("timelock_is_operation", nil, []string{ + "vector", + }, []any{ + id, + }) +} + +func (c curseMCMSEncoder) TimelockIsOperationPending(id []byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("timelock_is_operation_pending", nil, []string{ + "vector", + }, []any{ + id, + }) +} + +func (c curseMCMSEncoder) TimelockIsOperationReady(id []byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("timelock_is_operation_ready", nil, []string{ + "vector", + }, []any{ + id, + }) +} + +func (c curseMCMSEncoder) TimelockIsOperationDone(id []byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("timelock_is_operation_done", nil, []string{ + "vector", + }, []any{ + id, + }) +} + +func (c curseMCMSEncoder) TimelockGetTimestamp(id []byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("timelock_get_timestamp", nil, []string{ + "vector", + }, []any{ + id, + }) +} + +func (c curseMCMSEncoder) TimelockMinDelay() (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("timelock_min_delay", nil, []string{}, []any{}) +} + +func (c curseMCMSEncoder) TimelockGetBlockedFunctions() (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("timelock_get_blocked_functions", nil, []string{}, []any{}) +} + +func (c curseMCMSEncoder) TimelockGetBlockedFunctionsCount() (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("timelock_get_blocked_functions_count", nil, []string{}, []any{}) +} + +func (c curseMCMSEncoder) SetRoot(role byte, root []byte, validUntil uint64, chainId *big.Int, multisigAddr aptos.AccountAddress, preOpCount uint64, postOpCount uint64, overridePreviousRoot bool, metadataProof [][]byte, signatures [][]byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("set_root", nil, []string{ + "u8", + "vector", + "u64", + "u256", + "address", + "u64", + "u64", + "bool", + "vector>", + "vector>", + }, []any{ + role, + root, + validUntil, + chainId, + multisigAddr, + preOpCount, + postOpCount, + overridePreviousRoot, + metadataProof, + signatures, + }) +} + +func (c curseMCMSEncoder) Execute(role byte, chainId *big.Int, multisigAddr aptos.AccountAddress, nonce uint64, to aptos.AccountAddress, moduleName string, functionName string, data []byte, proof [][]byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("execute", nil, []string{ + "u8", + "u256", + "address", + "u64", + "address", + "0x1::string::String", + "0x1::string::String", + "vector", + "vector>", + }, []any{ + role, + chainId, + multisigAddr, + nonce, + to, + moduleName, + functionName, + data, + proof, + }) +} + +func (c curseMCMSEncoder) SetConfig(role byte, signerAddresses [][]byte, signerGroups []byte, groupQuorums []byte, groupParents []byte, clearRoot bool) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("set_config", nil, []string{ + "u8", + "vector>", + "vector", + "vector", + "vector", + "bool", + }, []any{ + role, + signerAddresses, + signerGroups, + groupQuorums, + groupParents, + clearRoot, + }) +} + +func (c curseMCMSEncoder) TimelockExecuteBatch(subjects [][]byte, functionNames []string, datas [][]byte, predecessor []byte, salt []byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("timelock_execute_batch", nil, []string{ + "vector>", + "vector<0x1::string::String>", + "vector>", + "vector", + "vector", + }, []any{ + subjects, + functionNames, + datas, + predecessor, + salt, + }) +} + +func (c curseMCMSEncoder) CreateMultisig(role byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("create_multisig", nil, []string{ + "u8", + }, []any{ + role, + }) +} + +func (c curseMCMSEncoder) EcdsaRecoverEvmAddr(ethSignedMessageHash []byte, signature []byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("ecdsa_recover_evm_addr", nil, []string{ + "vector", + "vector", + }, []any{ + ethSignedMessageHash, + signature, + }) +} + +func (c curseMCMSEncoder) DispatchToTimelock(role byte, functionName string, data []byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("dispatch_to_timelock", nil, []string{ + "u8", + "0x1::string::String", + "vector", + }, []any{ + role, + functionName, + data, + }) +} + +func (c curseMCMSEncoder) VerifyMerkleProof(proof [][]byte, root []byte, leaf []byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("verify_merkle_proof", nil, []string{ + "vector>", + "vector", + "vector", + }, []any{ + proof, + root, + leaf, + }) +} + +func (c curseMCMSEncoder) ComputeEthMessageHash(root []byte, validUntil uint64) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("compute_eth_message_hash", nil, []string{ + "vector", + "u64", + }, []any{ + root, + validUntil, + }) +} + +func (c curseMCMSEncoder) HashOpLeaf(domainSeparator []byte, op Op) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("hash_op_leaf", nil, []string{ + "vector", + "Op", + }, []any{ + domainSeparator, + op, + }) +} + +func (c curseMCMSEncoder) HashMetadataLeaf(metadata RootMetadata) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("hash_metadata_leaf", nil, []string{ + "RootMetadata", + }, []any{ + metadata, + }) +} + +func (c curseMCMSEncoder) Role(rootMetadata RootMetadata) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("role", nil, []string{ + "RootMetadata", + }, []any{ + rootMetadata, + }) +} + +func (c curseMCMSEncoder) ChainId(rootMetadata RootMetadata) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("chain_id", nil, []string{ + "RootMetadata", + }, []any{ + rootMetadata, + }) +} + +func (c curseMCMSEncoder) RootMetadataMultisig(rootMetadata RootMetadata) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("root_metadata_multisig", nil, []string{ + "RootMetadata", + }, []any{ + rootMetadata, + }) +} + +func (c curseMCMSEncoder) PreOpCount(rootMetadata RootMetadata) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("pre_op_count", nil, []string{ + "RootMetadata", + }, []any{ + rootMetadata, + }) +} + +func (c curseMCMSEncoder) PostOpCount(rootMetadata RootMetadata) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("post_op_count", nil, []string{ + "RootMetadata", + }, []any{ + rootMetadata, + }) +} + +func (c curseMCMSEncoder) OverridePreviousRoot(rootMetadata RootMetadata) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("override_previous_root", nil, []string{ + "RootMetadata", + }, []any{ + rootMetadata, + }) +} + +func (c curseMCMSEncoder) TimelockScheduleBatch(subjects [][]byte, functionNames []string, datas [][]byte, predecessor []byte, salt []byte, delay uint64) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("timelock_schedule_batch", nil, []string{ + "vector>", + "vector<0x1::string::String>", + "vector>", + "vector", + "vector", + "u64", + }, []any{ + subjects, + functionNames, + datas, + predecessor, + salt, + delay, + }) +} + +func (c curseMCMSEncoder) TimelockBeforeCall(id []byte, predecessor []byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("timelock_before_call", nil, []string{ + "vector", + "vector", + }, []any{ + id, + predecessor, + }) +} + +func (c curseMCMSEncoder) TimelockAfterCall(id []byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("timelock_after_call", nil, []string{ + "vector", + }, []any{ + id, + }) +} + +func (c curseMCMSEncoder) TimelockBypasserExecuteBatch(subjects [][]byte, functionNames []string, datas [][]byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("timelock_bypasser_execute_batch", nil, []string{ + "vector>", + "vector<0x1::string::String>", + "vector>", + }, []any{ + subjects, + functionNames, + datas, + }) +} + +func (c curseMCMSEncoder) TimelockDispatchToRMNRemote(functionName string, data []byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("timelock_dispatch_to_rmn_remote", nil, []string{ + "0x1::string::String", + "vector", + }, []any{ + functionName, + data, + }) +} + +func (c curseMCMSEncoder) TimelockCancel(id []byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("timelock_cancel", nil, []string{ + "vector", + }, []any{ + id, + }) +} + +func (c curseMCMSEncoder) TimelockUpdateMinDelay(newMinDelay uint64) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("timelock_update_min_delay", nil, []string{ + "u64", + }, []any{ + newMinDelay, + }) +} + +func (c curseMCMSEncoder) TimelockBlockFunction(functionName string) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("timelock_block_function", nil, []string{ + "0x1::string::String", + }, []any{ + functionName, + }) +} + +func (c curseMCMSEncoder) TimelockUnblockFunction(functionName string) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("timelock_unblock_function", nil, []string{ + "0x1::string::String", + }, []any{ + functionName, + }) +} + +func (c curseMCMSEncoder) CreateCalls(subjects [][]byte, functionNames []string, datas [][]byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("create_calls", nil, []string{ + "vector>", + "vector<0x1::string::String>", + "vector>", + }, []any{ + subjects, + functionNames, + datas, + }) +} + +func (c curseMCMSEncoder) HashOperationBatch(calls []Call, predecessor []byte, salt []byte) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("hash_operation_batch", nil, []string{ + "vector", + "vector", + "vector", + }, []any{ + calls, + predecessor, + salt, + }) +} + +func (c curseMCMSEncoder) FunctionName(call Call) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("function_name", nil, []string{ + "Call", + }, []any{ + call, + }) +} + +func (c curseMCMSEncoder) Subject(call Call) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("subject", nil, []string{ + "Call", + }, []any{ + call, + }) +} + +func (c curseMCMSEncoder) Data(call Call) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("data", nil, []string{ + "Call", + }, []any{ + call, + }) +} diff --git a/contracts/contracts.go b/contracts/contracts.go index 1162aaeb..51837bfc 100644 --- a/contracts/contracts.go +++ b/contracts/contracts.go @@ -35,8 +35,9 @@ const ( Platform = Package("platform") PlatformSecondary = Package("platform_secondary") - MCMS = Package("mcms") - MCMSTest = Package("mcms_test") + MCMS = Package("mcms") + MCMSTest = Package("mcms_test") + CurseMCMS = Package("curse_mcms") LargePackages = Package("large_packages") @@ -70,8 +71,9 @@ var Contracts map[Package]string = map[Package]string{ ManagedTokenMCMSRegistrar: filepath.Join("mcms_registrars", "managed_token_mcms_registrar"), RegulatedTokenMCMSRegistrar: filepath.Join("mcms_registrars", "regulated_token_mcms_registrar"), - MCMS: filepath.Join("mcms", "mcms"), - MCMSTest: filepath.Join("mcms", "mcms_test"), + MCMS: filepath.Join("mcms", "mcms"), + MCMSTest: filepath.Join("mcms", "mcms_test"), + CurseMCMS: filepath.Join("mcms", "curse_mcms"), LargePackages: "large_packages", diff --git a/contracts/mcms/curse_mcms/Move.toml b/contracts/mcms/curse_mcms/Move.toml new file mode 100644 index 00000000..023c88de --- /dev/null +++ b/contracts/mcms/curse_mcms/Move.toml @@ -0,0 +1,23 @@ +[package] +name = "ChainlinkCurseMCMS" +version = "1.0.0" +authors = [] +upgrade_policy = "immutable" + +[addresses] +curse_mcms = "_" +curse_mcms_owner = "_" +ccip = "_" +mcms = "_" +mcms_owner = "0x0" +mcms_register_entrypoints = "_" + +[dev-addresses] +curse_mcms = "0xCCC" +curse_mcms_owner = "0xCCD" +ccip = "0x30b33dec3fcac5ef3ea775128d88722b64ba59a4598277e537f284917403df29" +mcms = "0x4000" +mcms_register_entrypoints = "0x4001" + +[dependencies] +ChainlinkCCIP = { local = "../../ccip/ccip" } diff --git a/contracts/mcms/curse_mcms/sources/curse_mcms.move b/contracts/mcms/curse_mcms/sources/curse_mcms.move new file mode 100644 index 00000000..a76fe9ea --- /dev/null +++ b/contracts/mcms/curse_mcms/sources/curse_mcms.move @@ -0,0 +1,1576 @@ +/// This module is a scoped-down MCMS for fast curse/uncurse operations on RMN Remote. +/// It mirrors the full MCMS architecture but only dispatches to curse/uncurse functions. +module curse_mcms::curse_mcms { + use std::aptos_hash::keccak256; + use std::bcs; + use std::event; + use std::signer; + use std::ordered_map::{Self, OrderedMap}; + use std::big_ordered_map::{Self, BigOrderedMap}; + use std::string::{String}; + use aptos_std::smart_table::{Self, SmartTable}; + use aptos_std::smart_vector::{Self, SmartVector}; + use aptos_framework::chain_id; + use aptos_framework::object::{Self, ExtendRef, Object}; + use aptos_framework::timestamp; + use aptos_std::secp256k1; + use curse_mcms::bcs_stream::{Self, BCSStream}; + use curse_mcms::curse_mcms_account; + use curse_mcms::params::{Self}; + + use ccip::rmn_remote; + + const BYPASSER_ROLE: u8 = 0; + const CANCELLER_ROLE: u8 = 1; + const PROPOSER_ROLE: u8 = 2; + const TIMELOCK_ROLE: u8 = 3; + const MAX_ROLE: u8 = 4; + + const NUM_GROUPS: u64 = 32; + const MAX_NUM_SIGNERS: u64 = 200; + + // equivalent to initializing empty uint8[NUM_GROUPS] in Solidity + const VEC_NUM_GROUPS: vector = vector[ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0 + ]; + + // keccak256("MANY_CHAIN_MULTI_SIG_DOMAIN_SEPARATOR_METADATA_APTOS") + const MANY_CHAIN_MULTI_SIG_DOMAIN_SEPARATOR_METADATA: vector = x"a71d47b6c00b64ee21af96a1d424cb2dcbbed12becdcd3b4e6c7fc4c2f80a697"; + + // keccak256("MANY_CHAIN_MULTI_SIG_DOMAIN_SEPARATOR_OP_APTOS") + const MANY_CHAIN_MULTI_SIG_DOMAIN_SEPARATOR_OP: vector = x"e5a6d1256b00d7ec22512b6b60a3f4d75c559745d2dbf309f77b8b756caabe14"; + + /// Special timestamp value indicating an operation is done + const DONE_TIMESTAMP: u64 = 1; + + const ZERO_HASH: vector = vector[ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0 + ]; + + #[resource_group_member(group = aptos_framework::object::ObjectGroup)] + struct MultisigState has key { + bypasser: Object, + canceller: Object, + proposer: Object + } + + #[resource_group_member(group = aptos_framework::object::ObjectGroup)] + struct Multisig has key { + extend_ref: ExtendRef, + + /// signers is used to easily validate the existence of the signer by its address. We still + /// have signers stored in config in order to easily deactivate them when a new config is set. + signers: OrderedMap, Signer>, + config: Config, + + /// Remember signed hashes that this contract has seen. Each signed hash can only be set once. + seen_signed_hashes: BigOrderedMap, bool>, + expiring_root_and_op_count: ExpiringRootAndOpCount, + root_metadata: RootMetadata + } + + struct Op has copy, drop { + role: u8, + chain_id: u256, + multisig: address, + nonce: u64, + to: address, + module_name: String, + function_name: String, + data: vector + } + + struct RootMetadata has copy, drop, store { + role: u8, + chain_id: u256, + multisig: address, + pre_op_count: u64, + post_op_count: u64, + override_previous_root: bool + } + + struct Signer has store, copy, drop { + addr: vector, + index: u8, // index of signer in config.signers + group: u8 // 0 <= group < NUM_GROUPS. Each signer can only be in one group. + } + + struct Config has store, copy, drop { + signers: vector, + + // group_quorums[i] stores the quorum for the i-th signer group. Any group with + // group_quorums[i] = 0 is considered disabled. The i-th group is successful if + // it is enabled and at least group_quorums[i] of its children are successful. + group_quorums: vector, + + // group_parents[i] stores the parent group of the i-th signer group. We ensure that the + // groups form a tree structure (where the root/0-th signer group points to itself as + // parent) by enforcing + // - (i != 0) implies (group_parents[i] < i) + // - group_parents[0] == 0 + group_parents: vector + } + + struct ExpiringRootAndOpCount has store, drop { + root: vector, + valid_until: u64, + op_count: u64 + } + + #[event] + struct MultisigStateInitialized has drop, store { + bypasser: Object, + canceller: Object, + proposer: Object + } + + #[event] + struct ConfigSet has drop, store { + role: u8, + config: Config, + is_root_cleared: bool + } + + #[event] + struct NewRoot has drop, store { + role: u8, + root: vector, + valid_until: u64, + metadata: RootMetadata + } + + #[event] + struct OpExecuted has drop, store { + role: u8, + chain_id: u256, + multisig: address, + nonce: u64, + to: address, + module_name: String, + function_name: String, + data: vector + } + + const E_ALREADY_SEEN_HASH: u64 = 1; + const E_POST_OP_COUNT_REACHED: u64 = 2; + const E_WRONG_CHAIN_ID: u64 = 3; + const E_WRONG_MULTISIG: u64 = 4; + const E_ROOT_EXPIRED: u64 = 5; + const E_WRONG_NONCE: u64 = 6; + const E_VALID_UNTIL_EXPIRED: u64 = 7; + const E_INVALID_SIGNER: u64 = 8; + const E_MISSING_CONFIG: u64 = 9; + const E_INSUFFICIENT_SIGNERS: u64 = 10; + const E_PROOF_CANNOT_BE_VERIFIED: u64 = 11; + const E_PENDING_OPS: u64 = 12; + const E_WRONG_PRE_OP_COUNT: u64 = 13; + const E_WRONG_POST_OP_COUNT: u64 = 14; + const E_INVALID_NUM_SIGNERS: u64 = 15; + const E_SIGNER_GROUPS_LEN_MISMATCH: u64 = 16; + const E_INVALID_GROUP_QUORUM_LEN: u64 = 17; + const E_INVALID_GROUP_PARENTS_LEN: u64 = 18; + const E_OUT_OF_BOUNDS_GROUP: u64 = 19; + const E_GROUP_TREE_NOT_WELL_FORMED: u64 = 20; + const E_SIGNER_IN_DISABLED_GROUP: u64 = 21; + const E_OUT_OF_BOUNDS_GROUP_QUORUM: u64 = 22; + const E_SIGNER_ADDR_MUST_BE_INCREASING: u64 = 23; + const E_INVALID_SIGNER_ADDR_LEN: u64 = 24; + const E_UNKNOWN_CURSE_MCMS_FUNCTION: u64 = 25; + const E_NOT_BYPASSER_ROLE: u64 = 29; + const E_INVALID_ROLE: u64 = 30; + const E_NOT_AUTHORIZED_ROLE: u64 = 31; + const E_NOT_AUTHORIZED: u64 = 32; + const E_OPERATION_ALREADY_SCHEDULED: u64 = 33; + const E_INSUFFICIENT_DELAY: u64 = 34; + const E_OPERATION_NOT_READY: u64 = 35; + const E_MISSING_DEPENDENCY: u64 = 36; + const E_OPERATION_CANNOT_BE_CANCELLED: u64 = 37; + const E_FUNCTION_BLOCKED: u64 = 38; + const E_INVALID_INDEX: u64 = 39; + const E_INVALID_PARAMETERS: u64 = 43; + const E_INVALID_SIGNATURE_LEN: u64 = 44; + const E_INVALID_V_SIGNATURE: u64 = 45; + const E_FAILED_ECDSA_RECOVER: u64 = 46; + const E_INVALID_MODULE_NAME: u64 = 47; + const E_UNKNOWN_CURSE_MCMS_TIMELOCK_FUNCTION: u64 = 48; + const E_INVALID_ROOT_LEN: u64 = 49; + const E_NOT_CANCELLER_ROLE: u64 = 50; + const E_NOT_TIMELOCK_ROLE: u64 = 51; + + fun init_module(publisher: &signer) { + let bypasser = create_multisig(publisher, BYPASSER_ROLE); + let canceller = create_multisig(publisher, CANCELLER_ROLE); + let proposer = create_multisig(publisher, PROPOSER_ROLE); + + move_to( + publisher, + MultisigState { bypasser, canceller, proposer } + ); + + event::emit(MultisigStateInitialized { bypasser, canceller, proposer }); + + move_to( + publisher, + Timelock { + min_delay: 0, + timestamps: smart_table::new(), + blocked_functions: smart_vector::new() + } + ); + + event::emit(TimelockInitialized { min_delay: 0 }); + } + + inline fun create_multisig(publisher: &signer, role: u8): Object { + let constructor_ref = &object::create_object(signer::address_of(publisher)); + let object_signer = object::generate_signer(constructor_ref); + let extend_ref = object::generate_extend_ref(constructor_ref); + + move_to( + &object_signer, + Multisig { + extend_ref, + signers: ordered_map::new(), + config: Config { + signers: vector[], + group_quorums: VEC_NUM_GROUPS, + group_parents: VEC_NUM_GROUPS + }, + seen_signed_hashes: big_ordered_map::new_with_config(0, 0, false), + expiring_root_and_op_count: ExpiringRootAndOpCount { + root: vector[], + valid_until: 0, + op_count: 0 + }, + root_metadata: RootMetadata { + role, + chain_id: 0, + multisig: signer::address_of(&object_signer), + pre_op_count: 0, + post_op_count: 0, + override_previous_root: false + } + } + ); + + object::object_from_constructor_ref(constructor_ref) + } + + /// @notice set_root Sets a new expiring root. + /// + /// @param root is the new expiring root. + /// @param valid_until is the time by which root is valid + /// @param chain_id is the chain id of the chain on which the root is valid + /// @param multisig is the address of the multisig to set the root for + /// @param pre_op_count is the number of operations that have been executed before this root was set + /// @param post_op_count is the number of operations that have been executed after this root was set + /// @param override_previous_root is a boolean that indicates whether to override the previous root + /// @param metadata_proof is the MerkleProof of inclusion of the metadata in the Merkle tree. + /// @param signatures the ECDSA signatures on (root, valid_until). + /// + /// @dev the message (root, valid_until) should be signed by a sufficient set of signers. + /// This signature authenticates also the metadata. + /// + /// @dev this method can be executed by anyone who has the root and valid signatures. + /// as we validate the correctness of signatures, this imposes no risk. + public entry fun set_root( + role: u8, + root: vector, + valid_until: u64, + chain_id: u256, + multisig_addr: address, + pre_op_count: u64, + post_op_count: u64, + override_previous_root: bool, + metadata_proof: vector>, + signatures: vector> + ) acquires Multisig, MultisigState { + assert!(is_valid_role(role), E_INVALID_ROLE); + + let metadata = RootMetadata { + role, + chain_id, + multisig: multisig_addr, + pre_op_count, + post_op_count, + override_previous_root + }; + + let signed_hash = compute_eth_message_hash(root, valid_until); + + // Validate that `multisig` is a registered multisig for `role`. + let multisig = borrow_multisig_mut(multisig_object(role)); + + assert!( + !multisig.seen_signed_hashes.contains(&signed_hash), + E_ALREADY_SEEN_HASH + ); + assert!(timestamp::now_seconds() <= valid_until, E_VALID_UNTIL_EXPIRED); + assert!(metadata.chain_id == (chain_id::get() as u256), E_WRONG_CHAIN_ID); + assert!(metadata.multisig == @curse_mcms, E_WRONG_MULTISIG); + + let op_count = multisig.expiring_root_and_op_count.op_count; + assert!( + override_previous_root || op_count == multisig.root_metadata.post_op_count, + E_PENDING_OPS + ); + + assert!(op_count == metadata.pre_op_count, E_WRONG_PRE_OP_COUNT); + assert!(metadata.pre_op_count <= metadata.post_op_count, E_WRONG_POST_OP_COUNT); + + let metadata_leaf_hash = hash_metadata_leaf(metadata); + assert!( + verify_merkle_proof(metadata_proof, root, metadata_leaf_hash), + E_PROOF_CANNOT_BE_VERIFIED + ); + + let prev_address = vector[]; + let group_vote_counts: vector = vector[]; + params::right_pad_vec(&mut group_vote_counts, NUM_GROUPS); + + let signatures_len = signatures.length(); + for (i in 0..signatures_len) { + let signature = signatures[i]; + let signer_addr = ecdsa_recover_evm_addr(signed_hash, signature); + // the off-chain system is required to sort the signatures by the + // signer address in an increasing order + if (i > 0) { + assert!( + params::vector_u8_gt(&signer_addr, &prev_address), + E_SIGNER_ADDR_MUST_BE_INCREASING + ); + }; + prev_address = signer_addr; + + assert!(multisig.signers.contains(&signer_addr), E_INVALID_SIGNER); + let signer = *multisig.signers.borrow(&signer_addr); + + // check group quorums + let group: u8 = signer.group; + while (true) { + let group_vote_count = group_vote_counts.borrow_mut((group as u64)); + *group_vote_count += 1; + + let quorum = multisig.config.group_quorums.borrow((group as u64)); + if (*group_vote_count != *quorum) { + // bail out unless we just hit the quorum. we only hit each quorum once, + // so we never move on to the parent of a group more than once. + break + }; + + if (group == 0) { + // root group reached + break + }; + + // group quorum reached, restart loop and check parent group + group = multisig.config.group_parents[(group as u64)]; + }; + }; + + // the group at the root of the tree (with index 0) determines whether the vote passed, + // we cannot proceed if it isn't configured with a valid (non-zero) quorum + let root_group_quorum = multisig.config.group_quorums[0]; + assert!(root_group_quorum != 0, E_MISSING_CONFIG); + + // check root group reached quorum + let root_group_vote_count = group_vote_counts[0]; + assert!(root_group_vote_count >= root_group_quorum, E_INSUFFICIENT_SIGNERS); + + multisig.seen_signed_hashes.add(signed_hash, true); + multisig.expiring_root_and_op_count = ExpiringRootAndOpCount { + root, + valid_until, + op_count: metadata.pre_op_count + }; + multisig.root_metadata = metadata; + + event::emit( + NewRoot { + role, + root, + valid_until, + metadata: RootMetadata { + role, + chain_id, + multisig: multisig_addr, + pre_op_count: metadata.pre_op_count, + post_op_count: metadata.post_op_count, + override_previous_root: metadata.override_previous_root + } + } + ); + } + + inline fun ecdsa_recover_evm_addr( + eth_signed_message_hash: vector, signature: vector + ): vector { + // ensure signature has correct length - (r,s,v) concatenated = 65 bytes + assert!(signature.length() == 65, E_INVALID_SIGNATURE_LEN); + // extract v from signature + let v = signature.pop_back(); + // convert 64 byte signature into ECDSASignature struct + let sig = secp256k1::ecdsa_signature_from_bytes(signature); + // Aptos uses the rust libsecp256k1 parse() under the hood which has a different numbering scheme + // see: https://docs.rs/libsecp256k1/latest/libsecp256k1/struct.RecoveryId.html#method.parse_rpc + assert!(v >= 27 && v < 27 + 4, E_INVALID_V_SIGNATURE); + let v = v - 27; + + // retrieve signer public key + let public_key = secp256k1::ecdsa_recover(eth_signed_message_hash, v, &sig); + assert!(public_key.is_some(), E_FAILED_ECDSA_RECOVER); + + // return last 20 bytes of hashed public key as the recovered ethereum address + let public_key_bytes = + secp256k1::ecdsa_raw_public_key_to_bytes(&public_key.extract()); + keccak256(public_key_bytes).trim(12) // trims publicKeyBytes to 12 bytes, returns trimmed last 20 bytes + } + + /// Execute an operation after verifying its inclusion in the merkle tree + public entry fun execute( + role: u8, + chain_id: u256, + multisig_addr: address, + nonce: u64, + to: address, + module_name: String, + function_name: String, + data: vector, + proof: vector> + ) acquires Multisig, MultisigState, Timelock { + assert!(is_valid_role(role), E_INVALID_ROLE); + + let op = Op { + role, + chain_id, + multisig: multisig_addr, + nonce, + to, + module_name, + function_name, + data + }; + let multisig = borrow_multisig_mut(multisig_object(role)); + + assert!( + multisig.root_metadata.post_op_count + > multisig.expiring_root_and_op_count.op_count, + E_POST_OP_COUNT_REACHED + ); + assert!(chain_id == (chain_id::get() as u256), E_WRONG_CHAIN_ID); + assert!( + timestamp::now_seconds() <= multisig.expiring_root_and_op_count.valid_until, + E_ROOT_EXPIRED + ); + assert!(op.multisig == @curse_mcms, E_WRONG_MULTISIG); + assert!(nonce == multisig.expiring_root_and_op_count.op_count, E_WRONG_NONCE); + + // computes keccak256(abi.encode(MANY_CHAIN_MULTI_SIG_DOMAIN_SEPARATOR_OP, op)) + let hashed_leaf = hash_op_leaf(MANY_CHAIN_MULTI_SIG_DOMAIN_SEPARATOR_OP, op); + assert!( + verify_merkle_proof( + proof, multisig.expiring_root_and_op_count.root, hashed_leaf + ), + E_PROOF_CANNOT_BE_VERIFIED + ); + + multisig.expiring_root_and_op_count.op_count += 1; + + // Only allow dispatching to timelock functions + assert!( + op.to == @curse_mcms && *op.module_name.bytes() == b"curse_mcms", + E_INVALID_MODULE_NAME + ); + + dispatch_to_timelock(role, op.function_name, op.data); + + event::emit( + OpExecuted { + role, + chain_id, + multisig: multisig_addr, + nonce, + to, + module_name, + function_name, + data + } + ); + } + + /// Only callable from `execute`, the role that was validated is passed down to the timelock functions + inline fun dispatch_to_timelock( + role: u8, function_name: String, data: vector + ) { + let function_name_bytes = *function_name.bytes(); + let stream = bcs_stream::new(data); + + if (function_name_bytes == b"timelock_schedule_batch") { + dispatch_timelock_schedule_batch(role, &mut stream) + } else if (function_name_bytes == b"timelock_bypasser_execute_batch") { + dispatch_timelock_bypasser_execute_batch(role, &mut stream) + } else if (function_name_bytes == b"timelock_execute_batch") { + dispatch_timelock_execute_batch(&mut stream) + } else if (function_name_bytes == b"timelock_cancel") { + dispatch_timelock_cancel(role, &mut stream) + } else if (function_name_bytes == b"timelock_update_min_delay") { + dispatch_timelock_update_min_delay(role, &mut stream) + } else if (function_name_bytes == b"timelock_block_function") { + dispatch_timelock_block_function(role, &mut stream) + } else if (function_name_bytes == b"timelock_unblock_function") { + dispatch_timelock_unblock_function(role, &mut stream) + } else { + abort E_UNKNOWN_CURSE_MCMS_TIMELOCK_FUNCTION + } + } + + /// `dispatch_timelock_` functions should only be called from dispatch functions + inline fun dispatch_timelock_schedule_batch( + role: u8, stream: &mut BCSStream + ) { + assert!( + role == PROPOSER_ROLE || role == TIMELOCK_ROLE, + E_NOT_AUTHORIZED_ROLE + ); + + let subjects = + bcs_stream::deserialize_vector( + stream, |stream| bcs_stream::deserialize_vector_u8(stream) + ); + let function_names = + bcs_stream::deserialize_vector( + stream, |stream| bcs_stream::deserialize_string(stream) + ); + let datas = + bcs_stream::deserialize_vector( + stream, |stream| bcs_stream::deserialize_vector_u8(stream) + ); + let predecessor = bcs_stream::deserialize_vector_u8(stream); + let salt = bcs_stream::deserialize_vector_u8(stream); + let delay = bcs_stream::deserialize_u64(stream); + bcs_stream::assert_is_consumed(stream); + + timelock_schedule_batch( + subjects, + function_names, + datas, + predecessor, + salt, + delay + ) + } + + inline fun dispatch_timelock_bypasser_execute_batch( + role: u8, stream: &mut BCSStream + ) { + assert!( + role == BYPASSER_ROLE || role == TIMELOCK_ROLE, + E_NOT_AUTHORIZED_ROLE + ); + + let subjects = + bcs_stream::deserialize_vector( + stream, |stream| bcs_stream::deserialize_vector_u8(stream) + ); + let function_names = + bcs_stream::deserialize_vector( + stream, |stream| bcs_stream::deserialize_string(stream) + ); + let datas = + bcs_stream::deserialize_vector( + stream, |stream| bcs_stream::deserialize_vector_u8(stream) + ); + bcs_stream::assert_is_consumed(stream); + + timelock_bypasser_execute_batch(subjects, function_names, datas) + } + + inline fun dispatch_timelock_execute_batch(stream: &mut BCSStream) { + let subjects = + bcs_stream::deserialize_vector( + stream, |stream| bcs_stream::deserialize_vector_u8(stream) + ); + let function_names = + bcs_stream::deserialize_vector( + stream, |stream| bcs_stream::deserialize_string(stream) + ); + let datas = + bcs_stream::deserialize_vector( + stream, |stream| bcs_stream::deserialize_vector_u8(stream) + ); + let predecessor = bcs_stream::deserialize_vector_u8(stream); + let salt = bcs_stream::deserialize_vector_u8(stream); + bcs_stream::assert_is_consumed(stream); + + timelock_execute_batch( + subjects, + function_names, + datas, + predecessor, + salt + ) + } + + inline fun dispatch_timelock_cancel(role: u8, stream: &mut BCSStream) { + assert!( + role == CANCELLER_ROLE || role == TIMELOCK_ROLE, + E_NOT_AUTHORIZED_ROLE + ); + + let id = bcs_stream::deserialize_vector_u8(stream); + bcs_stream::assert_is_consumed(stream); + + timelock_cancel(id) + } + + inline fun dispatch_timelock_update_min_delay( + role: u8, stream: &mut BCSStream + ) { + assert!(role == TIMELOCK_ROLE, E_NOT_TIMELOCK_ROLE); + + let new_min_delay = bcs_stream::deserialize_u64(stream); + bcs_stream::assert_is_consumed(stream); + + timelock_update_min_delay(new_min_delay) + } + + inline fun dispatch_timelock_block_function( + role: u8, stream: &mut BCSStream + ) { + assert!(role == TIMELOCK_ROLE, E_NOT_TIMELOCK_ROLE); + + let function_name = bcs_stream::deserialize_string(stream); + bcs_stream::assert_is_consumed(stream); + + timelock_block_function(function_name) + } + + inline fun dispatch_timelock_unblock_function( + role: u8, stream: &mut BCSStream + ) { + assert!(role == TIMELOCK_ROLE, E_NOT_TIMELOCK_ROLE); + + let function_name = bcs_stream::deserialize_string(stream); + bcs_stream::assert_is_consumed(stream); + + timelock_unblock_function(function_name) + } + + /// Updates the multisig configuration, including signer addresses and group settings. + public entry fun set_config( + caller: &signer, + role: u8, + signer_addresses: vector>, + signer_groups: vector, + group_quorums: vector, + group_parents: vector, + clear_root: bool + ) acquires Multisig, MultisigState { + curse_mcms_account::assert_is_owner(caller); + + assert!( + signer_addresses.length() != 0 + && signer_addresses.length() <= MAX_NUM_SIGNERS, + E_INVALID_NUM_SIGNERS + ); + assert!( + signer_addresses.length() == signer_groups.length(), + E_SIGNER_GROUPS_LEN_MISMATCH + ); + assert!(group_quorums.length() == NUM_GROUPS, E_INVALID_GROUP_QUORUM_LEN); + assert!(group_parents.length() == NUM_GROUPS, E_INVALID_GROUP_PARENTS_LEN); + + // validate group structure + // counts number of children of each group + let group_children_counts = vector[]; + params::right_pad_vec(&mut group_children_counts, NUM_GROUPS); + // first, we count the signers as children + signer_groups.for_each_ref( + |group| { + let group: u64 = *group as u64; + assert!(group < NUM_GROUPS, E_OUT_OF_BOUNDS_GROUP); + let count = group_children_counts.borrow_mut(group); + *count += 1; + } + ); + + // second, we iterate backwards so as to check each group and propagate counts from + // child group to parent groups up the tree to the root + for (j in 0..NUM_GROUPS) { + let i = NUM_GROUPS - j - 1; + // ensure we have a well-formed group tree: + // - the root should have itself as parent + // - all other groups should have a parent group with a lower index + let group_parent = group_parents[i] as u64; + assert!( + i == 0 || group_parent < i, + E_GROUP_TREE_NOT_WELL_FORMED + ); + assert!( + i != 0 || group_parent == 0, + E_GROUP_TREE_NOT_WELL_FORMED + ); + + let group_quorum = group_quorums[i]; + let disabled = group_quorum == 0; + let group_children_count = group_children_counts[i]; + if (disabled) { + // if group is disabled, ensure it has no children + assert!(group_children_count == 0, E_SIGNER_IN_DISABLED_GROUP); + } else { + // if group is enabled, ensure group quorum can be met + assert!( + group_children_count >= group_quorum, E_OUT_OF_BOUNDS_GROUP_QUORUM + ); + + // propagate children counts to parent group + let count = group_children_counts.borrow_mut(group_parent); + *count += 1; + }; + }; + + let multisig = borrow_multisig_mut(multisig_object(role)); + + // remove old signer addresses + multisig.signers = ordered_map::new(); + multisig.config.signers = vector[]; + + // save group quorums and parents to timelock + multisig.config.group_quorums = group_quorums; + multisig.config.group_parents = group_parents; + + // check signer addresses are in increasing order and save signers to timelock + // evm zero address (20 bytes of 0) is the smallest address possible + let prev_signer_addr = vector[]; + for (i in 0..signer_addresses.length()) { + let signer_addr = signer_addresses[i]; + assert!(signer_addr.length() == 20, E_INVALID_SIGNER_ADDR_LEN); + + if (i > 0) { + assert!( + params::vector_u8_gt(&signer_addr, &prev_signer_addr), + E_SIGNER_ADDR_MUST_BE_INCREASING + ); + }; + + let signer = Signer { + addr: signer_addr, + index: (i as u8), + group: signer_groups[i] + }; + multisig.signers.add(signer_addr, signer); + multisig.config.signers.push_back(signer); + prev_signer_addr = signer_addr; + }; + + if (clear_root) { + // clearRoot is equivalent to overriding with a completely empty root + let op_count = multisig.expiring_root_and_op_count.op_count; + multisig.expiring_root_and_op_count = ExpiringRootAndOpCount { + root: vector[], + valid_until: 0, + op_count + }; + multisig.root_metadata = RootMetadata { + role, + chain_id: (chain_id::get() as u256), + multisig: @curse_mcms, + pre_op_count: op_count, + post_op_count: op_count, + override_previous_root: true + }; + }; + + event::emit( + ConfigSet { role, config: multisig.config, is_root_cleared: clear_root } + ); + } + + public fun verify_merkle_proof( + proof: vector>, + root: vector, + leaf: vector + ): bool { + let computed_hash = leaf; + proof.for_each_ref( + |proof_element| { + let (left, right) = + if (params::vector_u8_gt(&computed_hash, proof_element)) { + (*proof_element, computed_hash) + } else { + (computed_hash, *proof_element) + }; + let hash_input: vector = left; + hash_input.append(right); + computed_hash = keccak256(hash_input); + } + ); + computed_hash == root + } + + public fun compute_eth_message_hash( + root: vector, valid_until: u64 + ): vector { + // abi.encode(root (bytes32), valid_until) + let valid_until_bytes = params::encode_uint(valid_until, 32); + assert!(root.length() == 32, E_INVALID_ROOT_LEN); // root should be 32 bytes + let abi_encoded_params = &mut root; + abi_encoded_params.append(valid_until_bytes); + + // keccak256(abi_encoded_params) + let hashed_encoded_params = keccak256(*abi_encoded_params); + + // ECDSA.toEthSignedMessageHash() + let eth_msg_prefix = b"\x19Ethereum Signed Message:\n32"; + let hash = &mut eth_msg_prefix; + hash.append(hashed_encoded_params); + keccak256(*hash) + } + + public fun hash_op_leaf(domain_separator: vector, op: Op): vector { + let packed = vector[]; + packed.append(domain_separator); + packed.append(bcs::to_bytes(&op.role)); + packed.append(bcs::to_bytes(&op.chain_id)); + packed.append(bcs::to_bytes(&op.multisig)); + packed.append(bcs::to_bytes(&op.nonce)); + packed.append(bcs::to_bytes(&op.to)); + packed.append(bcs::to_bytes(&op.module_name)); + packed.append(bcs::to_bytes(&op.function_name)); + packed.append(bcs::to_bytes(&op.data)); + keccak256(packed) + } + + #[view] + public fun seen_signed_hashes(multisig: Object): vector> acquires Multisig { + borrow_multisig(multisig).seen_signed_hashes.keys() + } + + #[view] + /// Returns the current Merkle root along with its expiration timestamp and op count. + public fun expiring_root_and_op_count( + multisig: Object + ): (vector, u64, u64) acquires Multisig { + let multisig = borrow_multisig(multisig); + ( + multisig.expiring_root_and_op_count.root, + multisig.expiring_root_and_op_count.valid_until, + multisig.expiring_root_and_op_count.op_count + ) + } + + #[view] + public fun root_metadata(multisig: Object): RootMetadata acquires Multisig { + borrow_multisig(multisig).root_metadata + } + + #[view] + public fun get_root_metadata(role: u8): RootMetadata acquires MultisigState, Multisig { + let multisig = multisig_object(role); + borrow_multisig(multisig).root_metadata + } + + #[view] + public fun get_op_count(role: u8): u64 acquires MultisigState, Multisig { + let multisig = multisig_object(role); + borrow_multisig(multisig).expiring_root_and_op_count.op_count + } + + #[view] + public fun get_root(role: u8): (vector, u64) acquires MultisigState, Multisig { + let multisig = borrow_multisig(multisig_object(role)); + ( + multisig.expiring_root_and_op_count.root, + multisig.expiring_root_and_op_count.valid_until + ) + } + + #[view] + public fun get_config(role: u8): Config acquires MultisigState, Multisig { + let multisig = multisig_object(role); + borrow_multisig(multisig).config + } + + #[view] + public fun signers(multisig: Object): vector acquires Multisig { + borrow_multisig(multisig).signers.values() + } + + #[view] + /// Returns the registered multisig objects for the given role. + public fun multisig_object(role: u8): Object acquires MultisigState { + let state = borrow(); + if (role == BYPASSER_ROLE) { + state.bypasser + } else if (role == CANCELLER_ROLE) { + state.canceller + } else if (role == PROPOSER_ROLE) { + state.proposer + } else { + abort E_INVALID_ROLE + } + } + + #[view] + public fun num_groups(): u64 { + NUM_GROUPS + } + + #[view] + public fun max_num_signers(): u64 { + MAX_NUM_SIGNERS + } + + #[view] + public fun bypasser_role(): u8 { + BYPASSER_ROLE + } + + #[view] + public fun canceller_role(): u8 { + CANCELLER_ROLE + } + + #[view] + public fun proposer_role(): u8 { + PROPOSER_ROLE + } + + #[view] + public fun timelock_role(): u8 { + TIMELOCK_ROLE + } + + #[view] + public fun is_valid_role(role: u8): bool { + role < MAX_ROLE + } + + #[view] + public fun zero_hash(): vector { + ZERO_HASH + } + + fun hash_metadata_leaf(metadata: RootMetadata): vector { + let packed = vector[]; + packed.append(MANY_CHAIN_MULTI_SIG_DOMAIN_SEPARATOR_METADATA); + packed.append(bcs::to_bytes(&metadata.role)); + packed.append(bcs::to_bytes(&metadata.chain_id)); + packed.append(bcs::to_bytes(&metadata.multisig)); + packed.append(bcs::to_bytes(&metadata.pre_op_count)); + packed.append(bcs::to_bytes(&metadata.post_op_count)); + packed.append(bcs::to_bytes(&metadata.override_previous_root)); + keccak256(packed) + } + + inline fun borrow_multisig(obj: Object): &Multisig { + borrow_global(object::object_address(&obj)) + } + + inline fun borrow_multisig_mut(multisig: Object): &mut Multisig { + borrow_global_mut(object::object_address(&multisig)) + } + + inline fun borrow(): &MultisigState { + borrow_global(@curse_mcms) + } + + inline fun borrow_mut(): &mut MultisigState { + borrow_global_mut(@curse_mcms) + } + + public fun role(root_metadata: RootMetadata): u8 { + root_metadata.role + } + + public fun chain_id(root_metadata: RootMetadata): u256 { + root_metadata.chain_id + } + + public fun root_metadata_multisig(root_metadata: RootMetadata): address { + root_metadata.multisig + } + + public fun pre_op_count(root_metadata: RootMetadata): u64 { + root_metadata.pre_op_count + } + + public fun post_op_count(root_metadata: RootMetadata): u64 { + root_metadata.post_op_count + } + + public fun override_previous_root(root_metadata: RootMetadata): bool { + root_metadata.override_previous_root + } + + public fun config_signers(config: &Config): vector { + config.signers + } + + public fun config_group_quorums(config: &Config): vector { + config.group_quorums + } + + public fun config_group_parents(config: &Config): vector { + config.group_parents + } + + // ======================================================================================= + // | Timelock Implementation | + // ======================================================================================= + + #[resource_group_member(group = aptos_framework::object::ObjectGroup)] + struct Timelock has key { + min_delay: u64, + /// hashed batch of hashed calls -> timestamp + timestamps: SmartTable, u64>, + /// blocked functions (only curse/uncurse function names) + blocked_functions: SmartVector + } + + struct Call has copy, drop, store { + subject: vector, + function_name: String, + data: vector + } + + #[event] + struct TimelockInitialized has drop, store { + min_delay: u64 + } + + #[event] + struct BypasserCallExecuted has drop, store { + index: u64, + subject: vector, + function_name: String, + data: vector + } + + #[event] + struct Cancelled has drop, store { + id: vector + } + + #[event] + struct CallScheduled has drop, store { + id: vector, + index: u64, + subject: vector, + function_name: String, + data: vector, + predecessor: vector, + salt: vector, + delay: u64 + } + + #[event] + struct CallExecuted has drop, store { + id: vector, + index: u64, + subject: vector, + function_name: String, + data: vector + } + + #[event] + struct UpdateMinDelay has drop, store { + old_min_delay: u64, + new_min_delay: u64 + } + + #[event] + struct FunctionBlocked has drop, store { + function_name: String + } + + #[event] + struct FunctionUnblocked has drop, store { + function_name: String + } + + /// Schedule a batch of curse/uncurse calls to be executed after a delay. + /// This function can only be called by PROPOSER or CURSER role. + inline fun timelock_schedule_batch( + subjects: vector>, + function_names: vector, + datas: vector>, + predecessor: vector, + salt: vector, + delay: u64 + ) { + let calls = create_calls(subjects, function_names, datas); + let id = hash_operation_batch(calls, predecessor, salt); + let timelock = borrow_mut_timelock(); + + timelock_schedule(timelock, id, delay); + + for (i in 0..calls.length()) { + assert_not_blocked(timelock, &calls[i].function_name); + event::emit( + CallScheduled { + id, + index: i, + subject: calls[i].subject, + function_name: calls[i].function_name, + data: calls[i].data, + predecessor, + salt, + delay + } + ); + }; + } + + inline fun timelock_schedule( + timelock: &mut Timelock, id: vector, delay: u64 + ) { + assert!( + !timelock_is_operation_internal(timelock, id), + E_OPERATION_ALREADY_SCHEDULED + ); + assert!(delay >= timelock.min_delay, E_INSUFFICIENT_DELAY); + + let timestamp = timestamp::now_seconds() + delay; + timelock.timestamps.add(id, timestamp); + + } + + inline fun timelock_before_call( + id: vector, predecessor: vector + ) { + assert!(timelock_is_operation_ready(id), E_OPERATION_NOT_READY); + assert!( + predecessor == ZERO_HASH || timelock_is_operation_done(predecessor), + E_MISSING_DEPENDENCY + ); + } + + inline fun timelock_after_call(id: vector) { + assert!(timelock_is_operation_ready(id), E_OPERATION_NOT_READY); + *borrow_mut_timelock().timestamps.borrow_mut(id) = DONE_TIMESTAMP; + } + + /// Anyone can call this as it checks if the operation was scheduled by a bypasser or proposer. + public entry fun timelock_execute_batch( + subjects: vector>, + function_names: vector, + datas: vector>, + predecessor: vector, + salt: vector + ) acquires Timelock { + let calls = create_calls(subjects, function_names, datas); + let id = hash_operation_batch(calls, predecessor, salt); + + timelock_before_call(id, predecessor); + + for (i in 0..calls.length()) { + let subject = calls[i].subject; + let function_name = calls[i].function_name; + let data = calls[i].data; + + timelock_dispatch_to_rmn_remote(function_name, data); + + event::emit( + CallExecuted { id, index: i, subject, function_name, data } + ); + }; + + timelock_after_call(id); + } + + fun timelock_bypasser_execute_batch( + subjects: vector>, + function_names: vector, + datas: vector> + ) { + let len = subjects.length(); + assert!( + len == function_names.length() && len == datas.length(), + E_INVALID_PARAMETERS + ); + + for (i in 0..len) { + let subject = subjects[i]; + let function_name = function_names[i]; + let data = datas[i]; + + timelock_dispatch_to_rmn_remote(function_name, data); + + event::emit( + BypasserCallExecuted { index: i, subject, function_name, data } + ); + }; + } + + /// Dispatch to RMN Remote - ONLY allows curse/uncurse functions + /// This is the scoped-down dispatch that replaces the full MCMS dispatch + inline fun timelock_dispatch_to_rmn_remote( + function_name: String, data: vector + ) { + let function_bytes = *function_name.bytes(); + let stream = bcs_stream::new(data); + + // Get signer for calling RMN Remote + let object_signer = &curse_mcms_account::get_signer(); + + if (function_bytes == b"curse") { + let subject = bcs_stream::deserialize_vector_u8(&mut stream); + bcs_stream::assert_is_consumed(&stream); + rmn_remote::curse(object_signer, subject); + } else if (function_bytes == b"uncurse") { + let subject = bcs_stream::deserialize_vector_u8(&mut stream); + bcs_stream::assert_is_consumed(&stream); + rmn_remote::uncurse(object_signer, subject); + } else if (function_bytes == b"curse_multiple") { + let subjects = + bcs_stream::deserialize_vector( + &mut stream, + |stream| bcs_stream::deserialize_vector_u8(stream) + ); + bcs_stream::assert_is_consumed(&stream); + rmn_remote::curse_multiple(object_signer, subjects); + } else if (function_bytes == b"uncurse_multiple") { + let subjects = + bcs_stream::deserialize_vector( + &mut stream, + |stream| bcs_stream::deserialize_vector_u8(stream) + ); + bcs_stream::assert_is_consumed(&stream); + rmn_remote::uncurse_multiple(object_signer, subjects); + } else { + abort E_UNKNOWN_CURSE_MCMS_FUNCTION + } + } + + inline fun timelock_cancel(id: vector) { + assert!(timelock_is_operation_pending(id), E_OPERATION_CANNOT_BE_CANCELLED); + + borrow_mut_timelock().timestamps.remove(id); + event::emit(Cancelled { id }); + } + + inline fun timelock_update_min_delay(new_min_delay: u64) { + let timelock = borrow_mut_timelock(); + let old_min_delay = timelock.min_delay; + timelock.min_delay = new_min_delay; + + event::emit(UpdateMinDelay { old_min_delay, new_min_delay }); + } + + inline fun timelock_block_function(function_name: String) { + let already_blocked = false; + let timelock = borrow_mut_timelock(); + + for (i in 0..timelock.blocked_functions.length()) { + let blocked_function = timelock.blocked_functions.borrow(i); + if (*blocked_function.bytes() == *function_name.bytes()) { + already_blocked = true; + break + }; + }; + + if (!already_blocked) { + timelock.blocked_functions.push_back(function_name); + event::emit(FunctionBlocked { function_name }); + }; + } + + inline fun timelock_unblock_function(function_name: String) { + let timelock = borrow_mut_timelock(); + + for (i in 0..timelock.blocked_functions.length()) { + let blocked_function = timelock.blocked_functions.borrow(i); + if (*blocked_function.bytes() == *function_name.bytes()) { + timelock.blocked_functions.swap_remove(i); + event::emit(FunctionUnblocked { function_name }); + break + }; + }; + } + + inline fun assert_not_blocked( + timelock: &Timelock, function_name: &String + ) { + for (i in 0..timelock.blocked_functions.length()) { + let blocked_function = timelock.blocked_functions.borrow(i); + if (*blocked_function.bytes() == *function_name.bytes()) { + abort E_FUNCTION_BLOCKED; + }; + }; + } + + #[view] + public fun timelock_get_blocked_function(index: u64): String acquires Timelock { + let timelock = borrow_timelock(); + assert!(index < timelock.blocked_functions.length(), E_INVALID_INDEX); + *timelock.blocked_functions.borrow(index) + } + + #[view] + public fun timelock_is_operation(id: vector): bool acquires Timelock { + timelock_is_operation_internal(borrow_timelock(), id) + } + + inline fun timelock_is_operation_internal( + timelock: &Timelock, id: vector + ): bool { + timelock.timestamps.contains(id) && *timelock.timestamps.borrow(id) > 0 + } + + #[view] + public fun timelock_is_operation_pending(id: vector): bool acquires Timelock { + let timelock = borrow_timelock(); + timelock.timestamps.contains(id) + && *timelock.timestamps.borrow(id) > DONE_TIMESTAMP + } + + #[view] + public fun timelock_is_operation_ready(id: vector): bool acquires Timelock { + let timelock = borrow_timelock(); + if (!timelock.timestamps.contains(id)) { + return false + }; + + let timestamp_value = *timelock.timestamps.borrow(id); + timestamp_value > DONE_TIMESTAMP && timestamp_value <= timestamp::now_seconds() + } + + #[view] + public fun timelock_is_operation_done(id: vector): bool acquires Timelock { + let timelock = borrow_timelock(); + timelock.timestamps.contains(id) + && *timelock.timestamps.borrow(id) == DONE_TIMESTAMP + } + + #[view] + public fun timelock_get_timestamp(id: vector): u64 acquires Timelock { + let timelock = borrow_timelock(); + if (timelock.timestamps.contains(id)) { + *timelock.timestamps.borrow(id) + } else { 0 } + } + + #[view] + public fun timelock_min_delay(): u64 acquires Timelock { + borrow_timelock().min_delay + } + + #[view] + public fun timelock_get_blocked_functions(): vector acquires Timelock { + let timelock = borrow_timelock(); + let blocked_functions = vector[]; + for (i in 0..timelock.blocked_functions.length()) { + blocked_functions.push_back(*timelock.blocked_functions.borrow(i)); + }; + blocked_functions + } + + #[view] + public fun timelock_get_blocked_functions_count(): u64 acquires Timelock { + borrow_timelock().blocked_functions.length() + } + + public fun create_calls( + subjects: vector>, + function_names: vector, + datas: vector> + ): vector { + let len = subjects.length(); + assert!( + len == function_names.length() && len == datas.length(), + E_INVALID_PARAMETERS + ); + + let calls = vector[]; + for (i in 0..len) { + let subject = subjects[i]; + let function_name = function_names[i]; + let data = datas[i]; + let call = Call { subject, function_name, data }; + calls.push_back(call); + }; + + calls + } + + public fun hash_operation_batch( + calls: vector, predecessor: vector, salt: vector + ): vector { + let packed = vector[]; + packed.append(bcs::to_bytes(&calls)); + packed.append(predecessor); + packed.append(salt); + keccak256(packed) + } + + inline fun borrow_timelock(): &Timelock { + borrow_global(@curse_mcms) + } + + inline fun borrow_mut_timelock(): &mut Timelock { + borrow_global_mut(@curse_mcms) + } + + public fun signer_view(signer_: &Signer): (vector, u8, u8) { + (signer_.addr, signer_.index, signer_.group) + } + + public fun function_name(call: Call): String { + call.function_name + } + + public fun subject(call: Call): vector { + call.subject + } + + public fun data(call: Call): vector { + call.data + } + + // ======================= TEST ONLY FUNCTIONS ======================= // + + #[test_only] + public fun init_module_for_testing(publisher: &signer) { + init_module(publisher); + } + + #[test_only] + public fun test_hash_metadata_leaf( + role: u8, + chain_id: u256, + multisig: address, + pre_op_count: u64, + post_op_count: u64, + override_previous_root: bool + ): vector { + let metadata = RootMetadata { + role, + chain_id, + multisig, + pre_op_count, + post_op_count, + override_previous_root + }; + hash_metadata_leaf(metadata) + } + + #[test_only] + public fun test_set_expiring_root_and_op_count( + multisig: Object, + root: vector, + valid_until: u64, + op_count: u64 + ) acquires Multisig { + let multisig = borrow_multisig_mut(multisig); + multisig.expiring_root_and_op_count.root = root; + multisig.expiring_root_and_op_count.valid_until = valid_until; + multisig.expiring_root_and_op_count.op_count = op_count; + } + + #[test_only] + public fun test_set_root_metadata( + multisig: Object, + role: u8, + chain_id: u256, + multisig_addr: address, + pre_op_count: u64, + post_op_count: u64, + override_previous_root: bool + ) acquires Multisig { + let multisig = borrow_multisig_mut(multisig); + multisig.root_metadata.role = role; + multisig.root_metadata.chain_id = chain_id; + multisig.root_metadata.multisig = multisig_addr; + multisig.root_metadata.pre_op_count = pre_op_count; + multisig.root_metadata.post_op_count = post_op_count; + multisig.root_metadata.override_previous_root = override_previous_root; + } + + #[test_only] + public fun test_ecdsa_recover_evm_addr( + eth_signed_message_hash: vector, signature: vector + ): vector { + ecdsa_recover_evm_addr(eth_signed_message_hash, signature) + } + + #[test_only] + public fun test_timelock_schedule_batch( + subjects: vector>, + function_names: vector, + datas: vector>, + predecessor: vector, + salt: vector, + delay: u64 + ) acquires Timelock { + timelock_schedule_batch( + subjects, + function_names, + datas, + predecessor, + salt, + delay + ); + } + + #[test_only] + public fun test_timelock_update_min_delay(delay: u64) acquires Timelock { + timelock_update_min_delay(delay); + } + + #[test_only] + public fun test_timelock_cancel(id: vector) acquires Timelock { + timelock_cancel(id); + } + + #[test_only] + public fun test_timelock_bypasser_execute_batch( + subjects: vector>, + function_names: vector, + datas: vector> + ) { + timelock_bypasser_execute_batch(subjects, function_names, datas); + } + + #[test_only] + public fun test_timelock_block_function(function_name: String) acquires Timelock { + timelock_block_function(function_name); + } + + #[test_only] + public fun test_timelock_unblock_function(function_name: String) acquires Timelock { + timelock_unblock_function(function_name); + } + + #[test_only] + public fun create_op( + role: u8, + chain_id: u256, + multisig: address, + nonce: u64, + to: address, + module_name: String, + function_name: String, + data: vector + ): Op { + Op { + role, + chain_id, + multisig, + nonce, + to, + module_name, + function_name, + data + } + } + + #[test_only] + public fun test_timelock_dispatch_to_rmn_remote( + function_name: String, data: vector + ) { + timelock_dispatch_to_rmn_remote(function_name, data) + } +} diff --git a/contracts/mcms/curse_mcms/sources/curse_mcms_account.move b/contracts/mcms/curse_mcms/sources/curse_mcms_account.move new file mode 100644 index 00000000..190fdb40 --- /dev/null +++ b/contracts/mcms/curse_mcms/sources/curse_mcms_account.move @@ -0,0 +1,136 @@ +/// This module manages the ownership of the CurseMCMS package. +module curse_mcms::curse_mcms_account { + use std::account::{Self, SignerCapability}; + use std::error; + use std::event; + use std::resource_account; + use std::signer; + + friend curse_mcms::curse_mcms; + + struct AccountState has key, store { + signer_cap: SignerCapability, + owner: address, + pending_owner: address + } + + #[event] + struct OwnershipTransferRequested has store, drop { + from: address, + to: address + } + + #[event] + struct OwnershipTransferred has store, drop { + from: address, + to: address + } + + const E_CANNOT_TRANSFER_TO_SELF: u64 = 1; + const E_MUST_BE_PROPOSED_OWNER: u64 = 2; + const E_UNAUTHORIZED: u64 = 3; + + fun init_module(publisher: &signer) { + let signer_cap = + resource_account::retrieve_resource_account_cap( + publisher, @curse_mcms_owner + ); + init_module_internal(publisher, signer_cap); + } + + inline fun init_module_internal( + publisher: &signer, signer_cap: SignerCapability + ) { + move_to( + publisher, + AccountState { signer_cap, owner: @curse_mcms_owner, pending_owner: @0x0 } + ); + } + + /// Transfers ownership to the specified address. + public entry fun transfer_ownership(caller: &signer, to: address) acquires AccountState { + let state = borrow_state_mut(); + + assert_is_owner_internal(state, caller); + + assert!( + signer::address_of(caller) != to, + error::invalid_argument(E_CANNOT_TRANSFER_TO_SELF) + ); + + state.pending_owner = to; + + event::emit(OwnershipTransferRequested { from: state.owner, to }); + } + + /// Transfers ownership back to the `@curse_mcms` address. + public entry fun transfer_ownership_to_self(caller: &signer) acquires AccountState { + transfer_ownership(caller, @curse_mcms); + } + + /// Accepts ownership transfer. Can only be called by the pending owner. + public entry fun accept_ownership(caller: &signer) acquires AccountState { + let state = borrow_state_mut(); + + let caller_address = signer::address_of(caller); + assert!( + caller_address == state.pending_owner, + error::permission_denied(E_MUST_BE_PROPOSED_OWNER) + ); + + let previous_owner = state.owner; + state.owner = caller_address; + state.pending_owner = @0x0; + + event::emit(OwnershipTransferred { from: previous_owner, to: state.owner }); + } + + #[view] + /// Returns the current owner. + public fun owner(): address acquires AccountState { + borrow_state().owner + } + + #[view] + /// Returns `true` if the module is self-owned (owned by `@curse_mcms`). + public fun is_self_owned(): bool acquires AccountState { + owner() == @curse_mcms + } + + #[view] + /// Returns the address of the CurseMCMS account signer. + public fun get_address(): address acquires AccountState { + account::get_signer_capability_address(&borrow_state().signer_cap) + } + + public(friend) fun get_signer(): signer acquires AccountState { + account::create_signer_with_capability(&borrow_state().signer_cap) + } + + public(friend) fun assert_is_owner(caller: &signer) acquires AccountState { + assert_is_owner_internal(borrow_state(), caller); + } + + inline fun assert_is_owner_internal( + state: &AccountState, caller: &signer + ) { + assert!( + state.owner == signer::address_of(caller), + error::permission_denied(E_UNAUTHORIZED) + ); + } + + inline fun borrow_state(): &AccountState { + borrow_global(@curse_mcms) + } + + inline fun borrow_state_mut(): &mut AccountState { + borrow_global_mut(@curse_mcms) + } + + #[test_only] + public fun init_module_for_testing(publisher: &signer) { + let test_signer_cap = account::create_test_signer_cap(@curse_mcms); + init_module_internal(publisher, test_signer_cap); + } +} diff --git a/contracts/mcms/curse_mcms/sources/utils/bcs_stream.move b/contracts/mcms/curse_mcms/sources/utils/bcs_stream.move new file mode 100644 index 00000000..74f24f18 --- /dev/null +++ b/contracts/mcms/curse_mcms/sources/utils/bcs_stream.move @@ -0,0 +1,332 @@ +/// Copied and modified from: https://github.com/aptos-labs/aptos-core/blob/9baf39b6fba7812f09238c91973f61fd0955057c/aptos-move/move-examples/bcs-stream/sources/stream.move +/// +/// This module enables the deserialization of BCS-formatted byte arrays into Move primitive types. +/// Deserialization Strategies: +/// - Per-Byte Deserialization: Employed for most types to ensure lower gas consumption, this method processes each byte +/// individually to match the length and type requirements of target Move types. +/// - Exception: For the `deserialize_address` function, the function-based approach from `aptos_std::from_bcs` is used +/// due to type constraints, even though it is generally more gas-intensive. +/// - This can be optimized further by introducing native vector slices. +/// Application: +/// - This deserializer is particularly valuable for processing BCS serialized data within Move modules, +/// especially useful for systems requiring cross-chain message interpretation or off-chain data verification. +module curse_mcms::bcs_stream { + use std::error; + use std::vector; + use std::option::{Self, Option}; + use std::string::{Self, String}; + + use aptos_std::from_bcs; + + /// The data does not fit the expected format. + const E_MALFORMED_DATA: u64 = 1; + /// There are not enough bytes to deserialize for the given type. + const E_OUT_OF_BYTES: u64 = 2; + /// The stream has not been consumed. + const E_NOT_CONSUMED: u64 = 3; + + struct BCSStream has drop { + /// Byte buffer containing the serialized data. + data: vector, + /// Cursor indicating the current position in the byte buffer. + cur: u64 + } + + /// Constructs a new BCSStream instance from the provided byte array. + public fun new(data: vector): BCSStream { + BCSStream { data, cur: 0 } + } + + /// Asserts that the stream has been fully consumed. + public fun assert_is_consumed(stream: &BCSStream) { + assert!( + stream.cur == stream.data.length(), + error::invalid_state(E_NOT_CONSUMED) + ); + } + + /// Deserializes a ULEB128-encoded integer from the stream. + /// In the BCS format, lengths of vectors are represented using ULEB128 encoding. + public fun deserialize_uleb128(stream: &mut BCSStream): u64 { + let res = 0; + let shift = 0; + + while (stream.cur < stream.data.length()) { + let byte = stream.data[stream.cur]; + stream.cur += 1; + + let val = ((byte & 0x7f) as u64); + if (((val << shift) >> shift) != val) { + abort error::invalid_argument(E_MALFORMED_DATA) + }; + res |=(val << shift); + + if ((byte & 0x80) == 0) { + if (shift > 0 && val == 0) { + abort error::invalid_argument(E_MALFORMED_DATA) + }; + return res + }; + + shift += 7; + if (shift > 64) { + abort error::invalid_argument(E_MALFORMED_DATA) + }; + }; + + abort error::out_of_range(E_OUT_OF_BYTES) + } + + /// Deserializes a `bool` value from the stream. + public fun deserialize_bool(stream: &mut BCSStream): bool { + assert!( + stream.cur < stream.data.length(), + error::out_of_range(E_OUT_OF_BYTES) + ); + let byte = stream.data[stream.cur]; + stream.cur += 1; + if (byte == 0) { false } + else if (byte == 1) { true } + else { + abort error::invalid_argument(E_MALFORMED_DATA) + } + } + + /// Deserializes an `address` value from the stream. + /// 32-byte `address` values are serialized using little-endian byte order. + /// This function utilizes the `to_address` function from the `aptos_std::from_bcs` module, + /// because the Move type system does not permit per-byte referencing of addresses. + public fun deserialize_address(stream: &mut BCSStream): address { + let data = &stream.data; + let cur = stream.cur; + + assert!( + cur + 32 <= data.length(), + error::out_of_range(E_OUT_OF_BYTES) + ); + let res = from_bcs::to_address(data.slice(cur, cur + 32)); + + stream.cur = cur + 32; + res + } + + /// Deserializes a `u8` value from the stream. + /// 1-byte `u8` values are serialized using little-endian byte order. + public fun deserialize_u8(stream: &mut BCSStream): u8 { + let data = &stream.data; + let cur = stream.cur; + + assert!(cur < data.length(), error::out_of_range(E_OUT_OF_BYTES)); + + let res = data[cur]; + + stream.cur = cur + 1; + res + } + + /// Deserializes a `u16` value from the stream. + /// 2-byte `u16` values are serialized using little-endian byte order. + public fun deserialize_u16(stream: &mut BCSStream): u16 { + let data = &stream.data; + let cur = stream.cur; + + assert!( + cur + 2 <= data.length(), + error::out_of_range(E_OUT_OF_BYTES) + ); + let res = (data[cur] as u16) | ((data[cur + 1] as u16) << 8); + + stream.cur += 2; + res + } + + /// Deserializes a `u32` value from the stream. + /// 4-byte `u32` values are serialized using little-endian byte order. + public fun deserialize_u32(stream: &mut BCSStream): u32 { + let data = &stream.data; + let cur = stream.cur; + + assert!( + cur + 4 <= data.length(), + error::out_of_range(E_OUT_OF_BYTES) + ); + let res = + (data[cur] as u32) | ((data[cur + 1] as u32) << 8) | ((data[cur + 2] as u32) + << 16) | ((data[cur + 3] as u32) << 24); + + stream.cur += 4; + res + } + + /// Deserializes a `u64` value from the stream. + /// 8-byte `u64` values are serialized using little-endian byte order. + public fun deserialize_u64(stream: &mut BCSStream): u64 { + let data = &stream.data; + let cur = stream.cur; + + assert!( + cur + 8 <= data.length(), + error::out_of_range(E_OUT_OF_BYTES) + ); + let res = + (data[cur] as u64) | ((data[cur + 1] as u64) << 8) | ((data[cur + 2] as u64) + << 16) | ((data[cur + 3] as u64) << 24) | ((data[cur + 4] as u64) << 32) + | ((data[cur + 5] as u64) << 40) | ((data[cur + 6] as u64) << 48) + | ((data[cur + 7] as u64) << 56); + + stream.cur += 8; + res + } + + /// Deserializes a `u128` value from the stream. + /// 16-byte `u128` values are serialized using little-endian byte order. + public fun deserialize_u128(stream: &mut BCSStream): u128 { + let data = &stream.data; + let cur = stream.cur; + + assert!( + cur + 16 <= data.length(), + error::out_of_range(E_OUT_OF_BYTES) + ); + let res = + (data[cur] as u128) | ((data[cur + 1] as u128) << 8) + | ((data[cur + 2] as u128) << 16) | ((data[cur + 3] as u128) << 24) + | ((data[cur + 4] as u128) << 32) | ((data[cur + 5] as u128) << 40) + | ((data[cur + 6] as u128) << 48) | ((data[cur + 7] as u128) << 56) + | ((data[cur + 8] as u128) << 64) | ((data[cur + 9] as u128) << 72) + | ((data[cur + 10] as u128) << 80) | ((data[cur + 11] as u128) << 88) + | ((data[cur + 12] as u128) << 96) | ((data[cur + 13] as u128) << 104) + | ((data[cur + 14] as u128) << 112) | ((data[cur + 15] as u128) << 120); + + stream.cur += 16; + res + } + + /// Deserializes a `u256` value from the stream. + /// 32-byte `u256` values are serialized using little-endian byte order. + public fun deserialize_u256(stream: &mut BCSStream): u256 { + let data = &stream.data; + let cur = stream.cur; + + assert!( + cur + 32 <= data.length(), + error::out_of_range(E_OUT_OF_BYTES) + ); + let res = + (data[cur] as u256) | ((data[cur + 1] as u256) << 8) + | ((data[cur + 2] as u256) << 16) | ((data[cur + 3] as u256) << 24) + | ((data[cur + 4] as u256) << 32) | ((data[cur + 5] as u256) << 40) + | ((data[cur + 6] as u256) << 48) | ((data[cur + 7] as u256) << 56) + | ((data[cur + 8] as u256) << 64) | ((data[cur + 9] as u256) << 72) + | ((data[cur + 10] as u256) << 80) | ((data[cur + 11] as u256) << 88) + | ((data[cur + 12] as u256) << 96) | ((data[cur + 13] as u256) << 104) + | ((data[cur + 14] as u256) << 112) | ((data[cur + 15] as u256) << 120) + | ((data[cur + 16] as u256) << 128) | ((data[cur + 17] as u256) << 136) + | ((data[cur + 18] as u256) << 144) | ((data[cur + 19] as u256) << 152) + | ((data[cur + 20] as u256) << 160) | ((data[cur + 21] as u256) << 168) + | ((data[cur + 22] as u256) << 176) | ((data[cur + 23] as u256) << 184) + | ((data[cur + 24] as u256) << 192) | ((data[cur + 25] as u256) << 200) + | ((data[cur + 26] as u256) << 208) | ((data[cur + 27] as u256) << 216) + | ((data[cur + 28] as u256) << 224) | ((data[cur + 29] as u256) << 232) + | ((data[cur + 30] as u256) << 240) | ((data[cur + 31] as u256) << 248); + + stream.cur += 32; + res + } + + /// Deserializes a `u256` value from the stream. + public entry fun deserialize_u256_entry(data: vector, cursor: u64) { + let stream = BCSStream { data, cur: cursor }; + deserialize_u256(&mut stream); + } + + /// Deserializes an array of BCS deserializable elements from the stream. + /// First, reads the length of the vector, which is in uleb128 format. + /// After determining the length, it then reads the contents of the vector. + /// The `elem_deserializer` lambda expression is used sequentially to deserialize each element of the vector. + public inline fun deserialize_vector( + stream: &mut BCSStream, elem_deserializer: |&mut BCSStream| E + ): vector { + let len = deserialize_uleb128(stream); + let v = vector::empty(); + + for (i in 0..len) { + v.push_back(elem_deserializer(stream)); + }; + + v + } + + public fun deserialize_vector_u8(stream: &mut BCSStream): vector { + let len = deserialize_uleb128(stream); + let data = &mut stream.data; + let cur = stream.cur; + + assert!( + cur + len <= data.length(), + error::out_of_range(E_OUT_OF_BYTES) + ); + + // AIP-105 introduces vector::move_range to efficiently move a range of elements from one vector to another. + let res = data.trim(cur); + stream.data = res.trim(len); + stream.cur = 0; + + res + } + + public fun deserialize_fixed_vector_u8( + stream: &mut BCSStream, len: u64 + ): vector { + let data = &mut stream.data; + let cur = stream.cur; + + assert!( + cur + len <= data.length(), + error::out_of_range(E_OUT_OF_BYTES) + ); + + // AIP-105 introduces vector::move_range to efficiently move a range of elements from one vector to another. + let res = data.trim(cur); + stream.data = res.trim(len); + stream.cur = 0; + + res + } + + /// Deserializes utf-8 `String` from the stream. + /// First, reads the length of the String, which is in uleb128 format. + /// After determining the length, it then reads the contents of the String. + public fun deserialize_string(stream: &mut BCSStream): String { + let len = deserialize_uleb128(stream); + let data = &mut stream.data; + let cur = stream.cur; + + assert!( + cur + len <= data.length(), + error::out_of_range(E_OUT_OF_BYTES) + ); + + // AIP-105 introduces vector::move_range to efficiently move a range of elements from one vector to another. + let res = data.trim(cur); + stream.data = res.trim(len); + stream.cur = 0; + + string::utf8(res) + } + + /// Deserializes `Option` from the stream. + /// First, reads a single byte representing the presence (0x01) or absence (0x00) of data. + /// After determining the presence of data, it then reads the actual data if present. + /// The `elem_deserializer` lambda expression is used to deserialize the element contained within the `Option`. + public inline fun deserialize_option( + stream: &mut BCSStream, elem_deserializer: |&mut BCSStream| E + ): Option { + let is_data = deserialize_bool(stream); + if (is_data) { + option::some(elem_deserializer(stream)) + } else { + option::none() + } + } +} diff --git a/contracts/mcms/curse_mcms/sources/utils/params.move b/contracts/mcms/curse_mcms/sources/utils/params.move new file mode 100644 index 00000000..f4558b47 --- /dev/null +++ b/contracts/mcms/curse_mcms/sources/utils/params.move @@ -0,0 +1,59 @@ +module curse_mcms::params { + use std::bcs; + + const E_CMP_VECTORS_DIFF_LEN: u64 = 1; + const E_INPUT_TOO_LARGE_FOR_NUM_BYTES: u64 = 2; + + public inline fun encode_uint(input: T, num_bytes: u64): vector { + let bcs_bytes = bcs::to_bytes(&input); + + let len = bcs_bytes.length(); + assert!(len <= num_bytes, E_INPUT_TOO_LARGE_FOR_NUM_BYTES); + + if (len < num_bytes) { + let bytes_to_pad = num_bytes - len; + for (i in 0..bytes_to_pad) { + bcs_bytes.push_back(0); + }; + }; + + // little endian to big endian + bcs_bytes.reverse(); + + bcs_bytes + } + + public inline fun right_pad_vec(v: &mut vector, num_bytes: u64) { + let len = v.length(); + if (len < num_bytes) { + let bytes_to_pad = num_bytes - len; + for (i in 0..bytes_to_pad) { + v.push_back(0); + }; + }; + } + + /// compares two vectors of equal length, returns true if a > b, false otherwise. + public fun vector_u8_gt(a: &vector, b: &vector): bool { + let len = a.length(); + assert!(len == b.length(), E_CMP_VECTORS_DIFF_LEN); + + if (len == 0) { + return false + }; + + // compare each byte until not equal + for (i in 0..len) { + let byte_a = a[i]; + let byte_b = b[i]; + if (byte_a > byte_b) { + return true + } else if (byte_a < byte_b) { + return false + }; + }; + + // vectors are equal, a == b + false + } +} diff --git a/contracts/mcms/curse_mcms/tests/curse_mcms_integration_test.move b/contracts/mcms/curse_mcms/tests/curse_mcms_integration_test.move new file mode 100644 index 00000000..28bcd835 --- /dev/null +++ b/contracts/mcms/curse_mcms/tests/curse_mcms_integration_test.move @@ -0,0 +1,1223 @@ +// Integration tests for CurseMCMS calling into RMN Remote. +// These tests verify the complete curse flow: CurseMCMS -> dispatch -> RMN Remote. +#[test_only] +module curse_mcms::curse_mcms_integration_test { + use std::account; + use std::bcs; + use std::object; + use std::signer; + use std::string; + use aptos_framework::chain_id; + use aptos_framework::timestamp; + + use ccip::auth; + use ccip::rmn_remote; + use ccip::state_object; + + use curse_mcms::curse_mcms; + use curse_mcms::curse_mcms_account; + + // ================================================================ + // | Test Constants (from Go) | + // ================================================================ + + // Generated by: go test -v -run TestGenerateCurseMCMSTestData ./relayer/txm/ + // Using deterministic signers for reproducible tests + + const CHAIN_ID: u256 = 4; + const VALID_UNTIL: u64 = 1893456000; + const CHAIN_SELECTOR: u64 = 743186221051783445; + + // Contract Addresses (from Move.toml dev-addresses) + // curse_mcms = 0xCCC + + // Signer Addresses (sorted, 20 bytes each) - derived from deterministic private keys + const SIGNER_1: vector = x"2b5ad5c4795c026514f8317c7a215e218dccd6cf"; + const SIGNER_2: vector = x"6813eb9362372eef6200f3b1dbc3f819671cba69"; + const SIGNER_3: vector = x"7e5f4552091a69125d5dfcb7b8c2659029395bdf"; + + // Root and Metadata + const ROOT: vector = x"19a38b2f7483d700db8246c27f18542903c4f7c2b426ac5f62b4d383eb3b7921"; + + // Metadata Proof + const METADATA_PROOF: vector> = vector[ + x"e719621565299a5d8fe2056fd22746f39d86237201c706d93354fd0e8006efed", + x"c9b8b33f4fd9af038ad2ce7ea84c243040b98b598d39a42dac2d7cbe3577a5ab" + ]; + + // Signatures (2-of-3 quorum) + const SIGNATURES: vector> = vector[ + x"66749b3f53fd217171d4bdfcefe1b1e56118eaa0e2695a41c87f539591d7564647795449f6ada94eebebe77e4fe3123a0a9d86f5628b9e6d83bcd532a7ad07051c", + x"283e661f346ad7e4499438dae1d9fc8679277176cf520a61774cbea03474336c2f4cdb942ed4fad554e2a960972895863e3277038971c86b1d219ebda871edcc1c" + ]; + + // All operations target curse_mcms module and timelock_bypasser_execute_batch function + const TARGET_MODULE: vector = b"curse_mcms"; + const TARGET_FUNCTION: vector = b"timelock_bypasser_execute_batch"; + + // Operation 1: curse via timelock_bypasser_execute_batch + const OP1_NONCE: u64 = 0; + const OP1_DATA: vector = x"0110010000000000000000000000000000010105637572736501111001000000000000000000000000000001"; + const OP1_PROOF: vector> = vector[ + x"64bfab85ea2fb1f2f913a90b121fb9e4e15b5d48339e71df28e9a1017efca1ab", + x"c9b8b33f4fd9af038ad2ce7ea84c243040b98b598d39a42dac2d7cbe3577a5ab" + ]; + + // Operation 2: uncurse via timelock_bypasser_execute_batch + const OP2_NONCE: u64 = 1; + const OP2_DATA: vector = x"0110010000000000000000000000000000010107756e637572736501111001000000000000000000000000000001"; + const OP2_PROOF: vector> = vector[ + x"a7fc3240e76b26da0d6b30bc5f4e469c4fc254efc47ed41dc012d9aa8c71308d", + x"737d88b76c53ad293e80839343ac7d0ac10dc9b418b6789c88602e5405a4876b" + ]; + + // Operation 3: curse_multiple via timelock_bypasser_execute_batch + const OP3_NONCE: u64 = 2; + const OP3_DATA: vector = x"011001000000000000000000000000000001010e63757273655f6d756c7469706c6501230210010000000000000000000000000000011001000000000000000000000000000002"; + const OP3_PROOF: vector> = vector[ + x"11223fb4c818254222998ea8b00480666d63c5e92708c6d002f5d037522a3d32", + x"737d88b76c53ad293e80839343ac7d0ac10dc9b418b6789c88602e5405a4876b" + ]; + + // Curse Subjects + const GLOBAL_CURSE_SUBJECT: vector = x"01000000000000000000000000000001"; + const SUBJECT_2: vector = x"01000000000000000000000000000002"; + + // Use BYPASSER_ROLE for testing (role 0) + const BYPASSER_ROLE: u8 = 0; + + // Pre/Post op counts for the metadata + const PRE_OP_COUNT: u64 = 0; + const POST_OP_COUNT: u64 = 3; + + // ================================================================ + // | Test Setup Functions | + // ================================================================ + + /// Setup both CurseMCMS and RMN Remote for integration testing + fun setup_integration( + framework: &signer, + ccip: &signer, + ccip_owner: &signer, + curse_mcms_deployer: &signer, + curse_mcms_owner: &signer + ) { + // Initialize timestamp and chain_id + timestamp::set_time_has_started_for_testing(framework); + timestamp::update_global_time_for_test_secs(1000); + chain_id::initialize_for_test(framework, (CHAIN_ID as u8)); + + // Create accounts + account::create_account_for_test(signer::address_of(ccip)); + account::create_account_for_test(signer::address_of(ccip_owner)); + account::create_account_for_test(signer::address_of(curse_mcms_deployer)); + account::create_account_for_test(signer::address_of(curse_mcms_owner)); + + // Initialize CCIP (use initialize_v1 to test legacy migration path) + let _constructor_ref = object::create_named_object(ccip_owner, b"ccip"); + state_object::init_module_for_testing(ccip); + auth::test_init_module(ccip); + rmn_remote::initialize_v1(ccip_owner, CHAIN_SELECTOR); + + // Initialize CurseMCMS account (ownership) - stores at @curse_mcms + curse_mcms_account::init_module_for_testing(curse_mcms_deployer); + + // Initialize CurseMCMS (multisigs) + curse_mcms::init_module_for_testing(curse_mcms_deployer); + } + + /// Setup CurseMCMS as an allowed curser in RMN Remote + fun setup_curse_mcms_as_allowed_curser(ccip_owner: &signer) { + // Get CurseMCMS account signer address (used by timelock dispatch) + let curse_mcms_signer_addr = curse_mcms_account::get_address(); + + // Initialize AllowedCursersV2 with CurseMCMS address + rmn_remote::initialize_allowed_cursers_v2( + ccip_owner, vector[curse_mcms_signer_addr] + ); + } + + /// Setup CurseMCMS config with test signers + fun setup_curse_mcms_config(curse_mcms_owner: &signer) { + let signer_addresses = vector[SIGNER_1, SIGNER_2, SIGNER_3]; + let signer_groups = vector[0u8, 0u8, 0u8]; + let group_quorums = vector[ + 2u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0 + ]; + let group_parents = vector[ + 0u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0 + ]; + + curse_mcms::set_config( + curse_mcms_owner, + BYPASSER_ROLE, + signer_addresses, + signer_groups, + group_quorums, + group_parents, + false + ); + } + + // ================================================================ + // | Option A: Direct Dispatch Integration Tests | + // ================================================================ + + #[ + test( + framework = @aptos_framework, + ccip = @ccip, + ccip_owner = @mcms, + curse_mcms_deployer = @curse_mcms, + curse_mcms_owner = @curse_mcms_owner + ) + ] + fun test_dispatch_curse_success( + framework: &signer, + ccip: &signer, + ccip_owner: &signer, + curse_mcms_deployer: &signer, + curse_mcms_owner: &signer + ) { + setup_integration( + framework, + ccip, + ccip_owner, + curse_mcms_deployer, + curse_mcms_owner + ); + setup_curse_mcms_as_allowed_curser(ccip_owner); + + // Verify subject is not cursed initially + assert!(!rmn_remote::is_cursed(GLOBAL_CURSE_SUBJECT), 0); + + // Execute curse via CurseMCMS timelock dispatch + curse_mcms::test_timelock_dispatch_to_rmn_remote( + string::utf8(b"curse"), + bcs::to_bytes(&GLOBAL_CURSE_SUBJECT) + ); + + // Verify subject is now cursed + assert!(rmn_remote::is_cursed(GLOBAL_CURSE_SUBJECT), 1); + } + + #[ + test( + framework = @aptos_framework, + ccip = @ccip, + ccip_owner = @mcms, + curse_mcms_deployer = @curse_mcms, + curse_mcms_owner = @curse_mcms_owner + ) + ] + fun test_dispatch_uncurse_success( + framework: &signer, + ccip: &signer, + ccip_owner: &signer, + curse_mcms_deployer: &signer, + curse_mcms_owner: &signer + ) { + setup_integration( + framework, + ccip, + ccip_owner, + curse_mcms_deployer, + curse_mcms_owner + ); + setup_curse_mcms_as_allowed_curser(ccip_owner); + + // First curse the subject + rmn_remote::curse(ccip_owner, GLOBAL_CURSE_SUBJECT); + assert!(rmn_remote::is_cursed(GLOBAL_CURSE_SUBJECT), 0); + + // Execute uncurse via CurseMCMS timelock dispatch + curse_mcms::test_timelock_dispatch_to_rmn_remote( + string::utf8(b"uncurse"), + bcs::to_bytes(&GLOBAL_CURSE_SUBJECT) + ); + + // Verify subject is now uncursed + assert!(!rmn_remote::is_cursed(GLOBAL_CURSE_SUBJECT), 1); + } + + #[ + test( + framework = @aptos_framework, + ccip = @ccip, + ccip_owner = @mcms, + curse_mcms_deployer = @curse_mcms, + curse_mcms_owner = @curse_mcms_owner + ) + ] + fun test_dispatch_curse_multiple_success( + framework: &signer, + ccip: &signer, + ccip_owner: &signer, + curse_mcms_deployer: &signer, + curse_mcms_owner: &signer + ) { + setup_integration( + framework, + ccip, + ccip_owner, + curse_mcms_deployer, + curse_mcms_owner + ); + setup_curse_mcms_as_allowed_curser(ccip_owner); + + // Verify subjects are not cursed initially + assert!(!rmn_remote::is_cursed(GLOBAL_CURSE_SUBJECT), 0); + assert!(!rmn_remote::is_cursed(SUBJECT_2), 1); + + // Execute curse_multiple via CurseMCMS timelock dispatch + let subjects = vector[GLOBAL_CURSE_SUBJECT, SUBJECT_2]; + curse_mcms::test_timelock_dispatch_to_rmn_remote( + string::utf8(b"curse_multiple"), + bcs::to_bytes(&subjects) + ); + + // Verify both subjects are now cursed + assert!(rmn_remote::is_cursed(GLOBAL_CURSE_SUBJECT), 2); + assert!(rmn_remote::is_cursed(SUBJECT_2), 3); + } + + #[ + test( + framework = @aptos_framework, + ccip = @ccip, + ccip_owner = @mcms, + curse_mcms_deployer = @curse_mcms, + curse_mcms_owner = @curse_mcms_owner + ) + ] + fun test_dispatch_uncurse_multiple_success( + framework: &signer, + ccip: &signer, + ccip_owner: &signer, + curse_mcms_deployer: &signer, + curse_mcms_owner: &signer + ) { + setup_integration( + framework, + ccip, + ccip_owner, + curse_mcms_deployer, + curse_mcms_owner + ); + setup_curse_mcms_as_allowed_curser(ccip_owner); + + // First curse both subjects + rmn_remote::curse_multiple(ccip_owner, vector[GLOBAL_CURSE_SUBJECT, SUBJECT_2]); + assert!(rmn_remote::is_cursed(GLOBAL_CURSE_SUBJECT), 0); + assert!(rmn_remote::is_cursed(SUBJECT_2), 1); + + // Execute uncurse_multiple via CurseMCMS timelock dispatch + let subjects = vector[GLOBAL_CURSE_SUBJECT, SUBJECT_2]; + curse_mcms::test_timelock_dispatch_to_rmn_remote( + string::utf8(b"uncurse_multiple"), + bcs::to_bytes(&subjects) + ); + + // Verify both subjects are now uncursed + assert!(!rmn_remote::is_cursed(GLOBAL_CURSE_SUBJECT), 2); + assert!(!rmn_remote::is_cursed(SUBJECT_2), 3); + } + + #[ + test( + framework = @aptos_framework, + ccip = @ccip, + ccip_owner = @mcms, + curse_mcms_deployer = @curse_mcms, + curse_mcms_owner = @curse_mcms_owner + ) + ] + #[ + expected_failure( + abort_code = curse_mcms::curse_mcms::E_UNKNOWN_CURSE_MCMS_FUNCTION, + location = curse_mcms::curse_mcms + ) + ] + fun test_dispatch_invalid_function_rejected( + framework: &signer, + ccip: &signer, + ccip_owner: &signer, + curse_mcms_deployer: &signer, + curse_mcms_owner: &signer + ) { + setup_integration( + framework, + ccip, + ccip_owner, + curse_mcms_deployer, + curse_mcms_owner + ); + setup_curse_mcms_as_allowed_curser(ccip_owner); + + // Try to execute an invalid function - should fail with E_UNKNOWN_CURSE_MCMS_FUNCTION + curse_mcms::test_timelock_dispatch_to_rmn_remote( + string::utf8(b"set_config"), vector[] + ); + } + + #[ + test( + framework = @aptos_framework, + ccip = @ccip, + ccip_owner = @mcms, + curse_mcms_deployer = @curse_mcms, + curse_mcms_owner = @curse_mcms_owner + ) + ] + // 327699 = error::permission_denied(E_NOT_OWNER_OR_ALLOWED_CURSER) + // = (5 << 16) | 19 = 0x50013 + #[expected_failure(abort_code = 327699, location = ccip::rmn_remote)] + fun test_dispatch_without_allowed_curser_fails( + framework: &signer, + ccip: &signer, + ccip_owner: &signer, + curse_mcms_deployer: &signer, + curse_mcms_owner: &signer + ) { + setup_integration( + framework, + ccip, + ccip_owner, + curse_mcms_deployer, + curse_mcms_owner + ); + + // Initialize AllowedCursersV2 but DON'T add CurseMCMS as allowed curser + rmn_remote::initialize_allowed_cursers_v2(ccip_owner, vector[]); + + // CurseMCMS tries to curse but is not authorized + // Should fail with E_NOT_OWNER_OR_ALLOWED_CURSER (19) + curse_mcms::test_timelock_dispatch_to_rmn_remote( + string::utf8(b"curse"), + bcs::to_bytes(&GLOBAL_CURSE_SUBJECT) + ); + } + + // ================================================================ + // | CurseMCMS Signer Verification | + // ================================================================ + + #[ + test( + framework = @aptos_framework, + ccip = @ccip, + ccip_owner = @mcms, + curse_mcms_deployer = @curse_mcms, + curse_mcms_owner = @curse_mcms_owner + ) + ] + fun test_curse_mcms_signer_is_allowed_curser( + framework: &signer, + ccip: &signer, + ccip_owner: &signer, + curse_mcms_deployer: &signer, + curse_mcms_owner: &signer + ) { + setup_integration( + framework, + ccip, + ccip_owner, + curse_mcms_deployer, + curse_mcms_owner + ); + + // Get CurseMCMS account signer address (used by timelock dispatch) + let curse_mcms_signer_addr = curse_mcms_account::get_address(); + + // Add curse_mcms to allowed cursers + rmn_remote::initialize_allowed_cursers_v2( + ccip_owner, vector[curse_mcms_signer_addr] + ); + + // Verify curse_mcms is allowed + assert!(rmn_remote::is_allowed_curser(curse_mcms_signer_addr), 0); + + // Verify other addresses are not allowed + assert!(!rmn_remote::is_allowed_curser(@0x123), 1); + } + + // ================================================================ + // | Bypasser Execute Batch Integration Tests | + // ================================================================ + + #[ + test( + framework = @aptos_framework, + ccip = @ccip, + ccip_owner = @mcms, + curse_mcms_deployer = @curse_mcms, + curse_mcms_owner = @curse_mcms_owner + ) + ] + fun test_bypasser_execute_batch_curse( + framework: &signer, + ccip: &signer, + ccip_owner: &signer, + curse_mcms_deployer: &signer, + curse_mcms_owner: &signer + ) { + setup_integration( + framework, + ccip, + ccip_owner, + curse_mcms_deployer, + curse_mcms_owner + ); + setup_curse_mcms_as_allowed_curser(ccip_owner); + + // Verify subject is not cursed initially + assert!(!rmn_remote::is_cursed(GLOBAL_CURSE_SUBJECT), 0); + + // Execute bypasser execute batch with curse + let subjects = vector[GLOBAL_CURSE_SUBJECT]; + let function_names = vector[string::utf8(b"curse")]; + let datas = vector[bcs::to_bytes(&GLOBAL_CURSE_SUBJECT)]; + + curse_mcms::test_timelock_bypasser_execute_batch(subjects, function_names, datas); + + // Verify subject is now cursed + assert!(rmn_remote::is_cursed(GLOBAL_CURSE_SUBJECT), 1); + } + + #[ + test( + framework = @aptos_framework, + ccip = @ccip, + ccip_owner = @mcms, + curse_mcms_deployer = @curse_mcms, + curse_mcms_owner = @curse_mcms_owner + ) + ] + fun test_bypasser_execute_batch_multiple_calls( + framework: &signer, + ccip: &signer, + ccip_owner: &signer, + curse_mcms_deployer: &signer, + curse_mcms_owner: &signer + ) { + setup_integration( + framework, + ccip, + ccip_owner, + curse_mcms_deployer, + curse_mcms_owner + ); + setup_curse_mcms_as_allowed_curser(ccip_owner); + + // Verify subjects are not cursed initially + assert!(!rmn_remote::is_cursed(GLOBAL_CURSE_SUBJECT), 0); + assert!(!rmn_remote::is_cursed(SUBJECT_2), 1); + + // Execute bypasser execute batch with two curse operations + let subjects = vector[GLOBAL_CURSE_SUBJECT, SUBJECT_2]; + let function_names = vector[string::utf8(b"curse"), string::utf8(b"curse")]; + let datas = vector[bcs::to_bytes(&GLOBAL_CURSE_SUBJECT), bcs::to_bytes(&SUBJECT_2)]; + + curse_mcms::test_timelock_bypasser_execute_batch(subjects, function_names, datas); + + // Verify both subjects are now cursed + assert!(rmn_remote::is_cursed(GLOBAL_CURSE_SUBJECT), 2); + assert!(rmn_remote::is_cursed(SUBJECT_2), 3); + } + + // ================================================================ + // | Timelock Schedule and Execute Tests | + // ================================================================ + + #[ + test( + framework = @aptos_framework, + ccip = @ccip, + ccip_owner = @mcms, + curse_mcms_deployer = @curse_mcms, + curse_mcms_owner = @curse_mcms_owner + ) + ] + fun test_schedule_and_execute_batch_curse( + framework: &signer, + ccip: &signer, + ccip_owner: &signer, + curse_mcms_deployer: &signer, + curse_mcms_owner: &signer + ) { + setup_integration( + framework, + ccip, + ccip_owner, + curse_mcms_deployer, + curse_mcms_owner + ); + setup_curse_mcms_as_allowed_curser(ccip_owner); + + // Verify subject is not cursed initially + assert!(!rmn_remote::is_cursed(GLOBAL_CURSE_SUBJECT), 0); + + // Schedule a batch with delay 0 (since min_delay is 0) + let subjects = vector[GLOBAL_CURSE_SUBJECT]; + let function_names = vector[string::utf8(b"curse")]; + let datas = vector[bcs::to_bytes(&GLOBAL_CURSE_SUBJECT)]; + let predecessor = curse_mcms::zero_hash(); + let salt = x"0000000000000000000000000000000000000000000000000000000000000001"; + + curse_mcms::test_timelock_schedule_batch( + subjects, + function_names, + datas, + predecessor, + salt, + 0 // delay + ); + + // Get the operation ID + let calls = curse_mcms::create_calls(subjects, function_names, datas); + let id = curse_mcms::hash_operation_batch(calls, predecessor, salt); + + // Verify operation is ready (delay is 0) + assert!(curse_mcms::timelock_is_operation_ready(id), 1); + + // Execute the batch + curse_mcms::timelock_execute_batch( + subjects, + function_names, + datas, + predecessor, + salt + ); + + // Verify subject is now cursed + assert!(rmn_remote::is_cursed(GLOBAL_CURSE_SUBJECT), 2); + + // Verify operation is done + assert!(curse_mcms::timelock_is_operation_done(id), 3); + } + + // ================================================================ + // | Timelock Min Delay Tests | + // ================================================================ + + #[ + test( + framework = @aptos_framework, + ccip = @ccip, + ccip_owner = @mcms, + curse_mcms_deployer = @curse_mcms, + curse_mcms_owner = @curse_mcms_owner + ) + ] + fun test_timelock_min_delay_is_zero( + framework: &signer, + ccip: &signer, + ccip_owner: &signer, + curse_mcms_deployer: &signer, + curse_mcms_owner: &signer + ) { + setup_integration( + framework, + ccip, + ccip_owner, + curse_mcms_deployer, + curse_mcms_owner + ); + + // Verify min_delay is initialized to 0 + assert!(curse_mcms::timelock_min_delay() == 0, 0); + } + + // ================================================================ + // | Function Blocking Tests | + // ================================================================ + + #[ + test( + framework = @aptos_framework, + ccip = @ccip, + ccip_owner = @mcms, + curse_mcms_deployer = @curse_mcms, + curse_mcms_owner = @curse_mcms_owner + ) + ] + fun test_block_and_unblock_function( + framework: &signer, + ccip: &signer, + ccip_owner: &signer, + curse_mcms_deployer: &signer, + curse_mcms_owner: &signer + ) { + setup_integration( + framework, + ccip, + ccip_owner, + curse_mcms_deployer, + curse_mcms_owner + ); + + // Initially no functions are blocked + assert!(curse_mcms::timelock_get_blocked_functions_count() == 0, 0); + + // Block a function + curse_mcms::test_timelock_block_function(string::utf8(b"curse")); + + // Verify function is blocked + assert!(curse_mcms::timelock_get_blocked_functions_count() == 1, 1); + + // Unblock the function + curse_mcms::test_timelock_unblock_function(string::utf8(b"curse")); + + // Verify function is unblocked + assert!(curse_mcms::timelock_get_blocked_functions_count() == 0, 2); + } + + // ================================================================ + // | E2E Tests with Merkle Proofs and Signatures | + // ================================================================ + // These tests verify the full MCMS flow: + // 1. set_config - configure signers + // 2. set_root - commit merkle root with signatures + // 3. execute - execute operations with merkle proofs + // + // Note: set_root and execute are permissionless entry functions. + // The signatures provide authorization, not the signer. + + #[ + test( + framework = @aptos_framework, + ccip = @ccip, + ccip_owner = @mcms, + curse_mcms_deployer = @curse_mcms, + curse_mcms_owner = @curse_mcms_owner + ) + ] + fun test_e2e_set_root_and_execute_curse( + framework: &signer, + ccip: &signer, + ccip_owner: &signer, + curse_mcms_deployer: &signer, + curse_mcms_owner: &signer + ) { + setup_integration( + framework, + ccip, + ccip_owner, + curse_mcms_deployer, + curse_mcms_owner + ); + setup_curse_mcms_as_allowed_curser(ccip_owner); + setup_curse_mcms_config(curse_mcms_owner); + + // Verify subject is not cursed initially + assert!(!rmn_remote::is_cursed(GLOBAL_CURSE_SUBJECT), 0); + + // Set root with signatures (commits the merkle tree) + // set_root(role, root, valid_until, chain_id, multisig_addr, + // pre_op_count, post_op_count, override_previous_root, + // metadata_proof, signatures) + curse_mcms::set_root( + BYPASSER_ROLE, + ROOT, + VALID_UNTIL, + CHAIN_ID, + @curse_mcms, + PRE_OP_COUNT, + POST_OP_COUNT, + false, + METADATA_PROOF, + SIGNATURES + ); + + // Execute operation 1: curse via timelock_bypasser_execute_batch + // execute(role, chain_id, multisig_addr, nonce, to, module_name, function_name, data, proof) + curse_mcms::execute( + BYPASSER_ROLE, + CHAIN_ID, + @curse_mcms, + OP1_NONCE, + @curse_mcms, // Target is curse_mcms itself + string::utf8(TARGET_MODULE), + string::utf8(TARGET_FUNCTION), + OP1_DATA, + OP1_PROOF + ); + + // Verify subject is now cursed + assert!(rmn_remote::is_cursed(GLOBAL_CURSE_SUBJECT), 1); + } + + #[ + test( + framework = @aptos_framework, + ccip = @ccip, + ccip_owner = @mcms, + curse_mcms_deployer = @curse_mcms, + curse_mcms_owner = @curse_mcms_owner + ) + ] + fun test_e2e_set_root_and_execute_curse_then_uncurse( + framework: &signer, + ccip: &signer, + ccip_owner: &signer, + curse_mcms_deployer: &signer, + curse_mcms_owner: &signer + ) { + setup_integration( + framework, + ccip, + ccip_owner, + curse_mcms_deployer, + curse_mcms_owner + ); + setup_curse_mcms_as_allowed_curser(ccip_owner); + setup_curse_mcms_config(curse_mcms_owner); + + // Verify subject is not cursed initially + assert!(!rmn_remote::is_cursed(GLOBAL_CURSE_SUBJECT), 0); + + // Set root with signatures + curse_mcms::set_root( + BYPASSER_ROLE, + ROOT, + VALID_UNTIL, + CHAIN_ID, + @curse_mcms, + PRE_OP_COUNT, + POST_OP_COUNT, + false, + METADATA_PROOF, + SIGNATURES + ); + + // Execute operation 1: curse via timelock_bypasser_execute_batch + curse_mcms::execute( + BYPASSER_ROLE, + CHAIN_ID, + @curse_mcms, + OP1_NONCE, + @curse_mcms, + string::utf8(TARGET_MODULE), + string::utf8(TARGET_FUNCTION), + OP1_DATA, + OP1_PROOF + ); + + // Verify subject is cursed + assert!(rmn_remote::is_cursed(GLOBAL_CURSE_SUBJECT), 1); + + // Execute operation 2: uncurse via timelock_bypasser_execute_batch + curse_mcms::execute( + BYPASSER_ROLE, + CHAIN_ID, + @curse_mcms, + OP2_NONCE, + @curse_mcms, + string::utf8(TARGET_MODULE), + string::utf8(TARGET_FUNCTION), + OP2_DATA, + OP2_PROOF + ); + + // Verify subject is now uncursed + assert!(!rmn_remote::is_cursed(GLOBAL_CURSE_SUBJECT), 2); + } + + #[ + test( + framework = @aptos_framework, + ccip = @ccip, + ccip_owner = @mcms, + curse_mcms_deployer = @curse_mcms, + curse_mcms_owner = @curse_mcms_owner + ) + ] + fun test_e2e_set_root_and_execute_curse_multiple( + framework: &signer, + ccip: &signer, + ccip_owner: &signer, + curse_mcms_deployer: &signer, + curse_mcms_owner: &signer + ) { + setup_integration( + framework, + ccip, + ccip_owner, + curse_mcms_deployer, + curse_mcms_owner + ); + setup_curse_mcms_as_allowed_curser(ccip_owner); + setup_curse_mcms_config(curse_mcms_owner); + + // Verify subjects are not cursed initially + assert!(!rmn_remote::is_cursed(GLOBAL_CURSE_SUBJECT), 0); + assert!(!rmn_remote::is_cursed(SUBJECT_2), 1); + + // Set root with signatures + curse_mcms::set_root( + BYPASSER_ROLE, + ROOT, + VALID_UNTIL, + CHAIN_ID, + @curse_mcms, + PRE_OP_COUNT, + POST_OP_COUNT, + false, + METADATA_PROOF, + SIGNATURES + ); + + // Execute operation 1 and 2 first (need to execute in order) + curse_mcms::execute( + BYPASSER_ROLE, + CHAIN_ID, + @curse_mcms, + OP1_NONCE, + @curse_mcms, + string::utf8(TARGET_MODULE), + string::utf8(TARGET_FUNCTION), + OP1_DATA, + OP1_PROOF + ); + + curse_mcms::execute( + BYPASSER_ROLE, + CHAIN_ID, + @curse_mcms, + OP2_NONCE, + @curse_mcms, + string::utf8(TARGET_MODULE), + string::utf8(TARGET_FUNCTION), + OP2_DATA, + OP2_PROOF + ); + + // Uncurse first subject to reset state + assert!(!rmn_remote::is_cursed(GLOBAL_CURSE_SUBJECT), 2); + + // Execute operation 3: curse_multiple + curse_mcms::execute( + BYPASSER_ROLE, + CHAIN_ID, + @curse_mcms, + OP3_NONCE, + @curse_mcms, + string::utf8(TARGET_MODULE), + string::utf8(TARGET_FUNCTION), + OP3_DATA, + OP3_PROOF + ); + + // Verify both subjects are now cursed + assert!(rmn_remote::is_cursed(GLOBAL_CURSE_SUBJECT), 3); + assert!(rmn_remote::is_cursed(SUBJECT_2), 4); + } + + #[ + test( + framework = @aptos_framework, + ccip = @ccip, + ccip_owner = @mcms, + curse_mcms_deployer = @curse_mcms, + curse_mcms_owner = @curse_mcms_owner + ) + ] + #[ + expected_failure( + abort_code = curse_mcms::curse_mcms::E_POST_OP_COUNT_REACHED, + location = curse_mcms::curse_mcms + ) + ] + fun test_e2e_execute_fails_without_set_root( + framework: &signer, + ccip: &signer, + ccip_owner: &signer, + curse_mcms_deployer: &signer, + curse_mcms_owner: &signer + ) { + setup_integration( + framework, + ccip, + ccip_owner, + curse_mcms_deployer, + curse_mcms_owner + ); + setup_curse_mcms_as_allowed_curser(ccip_owner); + setup_curse_mcms_config(curse_mcms_owner); + + // Try to execute without setting root first - should fail + curse_mcms::execute( + BYPASSER_ROLE, + CHAIN_ID, + @curse_mcms, + OP1_NONCE, + @curse_mcms, + string::utf8(TARGET_MODULE), + string::utf8(TARGET_FUNCTION), + OP1_DATA, + OP1_PROOF + ); + } + + #[ + test( + framework = @aptos_framework, + ccip = @ccip, + ccip_owner = @mcms, + curse_mcms_deployer = @curse_mcms, + curse_mcms_owner = @curse_mcms_owner + ) + ] + #[ + expected_failure( + abort_code = curse_mcms::curse_mcms::E_PROOF_CANNOT_BE_VERIFIED, + location = curse_mcms::curse_mcms + ) + ] + fun test_e2e_execute_fails_with_wrong_proof( + framework: &signer, + ccip: &signer, + ccip_owner: &signer, + curse_mcms_deployer: &signer, + curse_mcms_owner: &signer + ) { + setup_integration( + framework, + ccip, + ccip_owner, + curse_mcms_deployer, + curse_mcms_owner + ); + setup_curse_mcms_as_allowed_curser(ccip_owner); + setup_curse_mcms_config(curse_mcms_owner); + + // Set root + curse_mcms::set_root( + BYPASSER_ROLE, + ROOT, + VALID_UNTIL, + CHAIN_ID, + @curse_mcms, + PRE_OP_COUNT, + POST_OP_COUNT, + false, + METADATA_PROOF, + SIGNATURES + ); + + // Try to execute with wrong proof - should fail with E_PROOF_CANNOT_BE_VERIFIED + let wrong_proof = vector[ + x"0000000000000000000000000000000000000000000000000000000000000000", + x"0000000000000000000000000000000000000000000000000000000000000000" + ]; + curse_mcms::execute( + BYPASSER_ROLE, + CHAIN_ID, + @curse_mcms, + OP1_NONCE, + @curse_mcms, + string::utf8(TARGET_MODULE), + string::utf8(TARGET_FUNCTION), + OP1_DATA, + wrong_proof + ); + } + + #[ + test( + framework = @aptos_framework, + ccip = @ccip, + ccip_owner = @mcms, + curse_mcms_deployer = @curse_mcms, + curse_mcms_owner = @curse_mcms_owner + ) + ] + #[ + expected_failure( + abort_code = curse_mcms::curse_mcms::E_WRONG_NONCE, + location = curse_mcms::curse_mcms + ) + ] + fun test_e2e_execute_fails_replay_attack( + framework: &signer, + ccip: &signer, + ccip_owner: &signer, + curse_mcms_deployer: &signer, + curse_mcms_owner: &signer + ) { + setup_integration( + framework, + ccip, + ccip_owner, + curse_mcms_deployer, + curse_mcms_owner + ); + setup_curse_mcms_as_allowed_curser(ccip_owner); + setup_curse_mcms_config(curse_mcms_owner); + + // Set root + curse_mcms::set_root( + BYPASSER_ROLE, + ROOT, + VALID_UNTIL, + CHAIN_ID, + @curse_mcms, + PRE_OP_COUNT, + POST_OP_COUNT, + false, + METADATA_PROOF, + SIGNATURES + ); + + // Execute operation 1 + curse_mcms::execute( + BYPASSER_ROLE, + CHAIN_ID, + @curse_mcms, + OP1_NONCE, + @curse_mcms, + string::utf8(TARGET_MODULE), + string::utf8(TARGET_FUNCTION), + OP1_DATA, + OP1_PROOF + ); + + // Try to replay the same operation - should fail with E_WRONG_NONCE + curse_mcms::execute( + BYPASSER_ROLE, + CHAIN_ID, + @curse_mcms, + OP1_NONCE, + @curse_mcms, + string::utf8(TARGET_MODULE), + string::utf8(TARGET_FUNCTION), + OP1_DATA, + OP1_PROOF + ); + } + + #[ + test( + framework = @aptos_framework, + ccip = @ccip, + ccip_owner = @mcms, + curse_mcms_deployer = @curse_mcms, + curse_mcms_owner = @curse_mcms_owner + ) + ] + #[ + expected_failure( + abort_code = curse_mcms::curse_mcms::E_WRONG_NONCE, + location = curse_mcms::curse_mcms + ) + ] + fun test_e2e_execute_fails_out_of_order( + framework: &signer, + ccip: &signer, + ccip_owner: &signer, + curse_mcms_deployer: &signer, + curse_mcms_owner: &signer + ) { + setup_integration( + framework, + ccip, + ccip_owner, + curse_mcms_deployer, + curse_mcms_owner + ); + setup_curse_mcms_as_allowed_curser(ccip_owner); + setup_curse_mcms_config(curse_mcms_owner); + + // Set root + curse_mcms::set_root( + BYPASSER_ROLE, + ROOT, + VALID_UNTIL, + CHAIN_ID, + @curse_mcms, + PRE_OP_COUNT, + POST_OP_COUNT, + false, + METADATA_PROOF, + SIGNATURES + ); + + // Try to execute operation 2 before operation 1 - should fail with E_WRONG_NONCE + curse_mcms::execute( + BYPASSER_ROLE, + CHAIN_ID, + @curse_mcms, + OP2_NONCE, + @curse_mcms, + string::utf8(TARGET_MODULE), + string::utf8(TARGET_FUNCTION), + OP2_DATA, + OP2_PROOF + ); + } + + #[ + test( + framework = @aptos_framework, + ccip = @ccip, + ccip_owner = @mcms, + curse_mcms_deployer = @curse_mcms, + curse_mcms_owner = @curse_mcms_owner + ) + ] + fun test_e2e_view_functions_after_set_root( + framework: &signer, + ccip: &signer, + ccip_owner: &signer, + curse_mcms_deployer: &signer, + curse_mcms_owner: &signer + ) { + setup_integration( + framework, + ccip, + ccip_owner, + curse_mcms_deployer, + curse_mcms_owner + ); + setup_curse_mcms_as_allowed_curser(ccip_owner); + setup_curse_mcms_config(curse_mcms_owner); + + // Set root + curse_mcms::set_root( + BYPASSER_ROLE, + ROOT, + VALID_UNTIL, + CHAIN_ID, + @curse_mcms, + PRE_OP_COUNT, + POST_OP_COUNT, + false, + METADATA_PROOF, + SIGNATURES + ); + + // Verify view functions - get_root returns (root, valid_until) + let (root, valid_until) = curse_mcms::get_root(BYPASSER_ROLE); + assert!(root == ROOT, 0); + assert!(valid_until == VALID_UNTIL, 1); + assert!(curse_mcms::get_op_count(BYPASSER_ROLE) == PRE_OP_COUNT, 2); + + // Execute one operation + curse_mcms::execute( + BYPASSER_ROLE, + CHAIN_ID, + @curse_mcms, + OP1_NONCE, + @curse_mcms, + string::utf8(TARGET_MODULE), + string::utf8(TARGET_FUNCTION), + OP1_DATA, + OP1_PROOF + ); + + // Verify op_count incremented + assert!(curse_mcms::get_op_count(BYPASSER_ROLE) == 1, 3); + } +} diff --git a/contracts/mcms/curse_mcms/tests/curse_mcms_test.move b/contracts/mcms/curse_mcms/tests/curse_mcms_test.move new file mode 100644 index 00000000..13e5ed55 --- /dev/null +++ b/contracts/mcms/curse_mcms/tests/curse_mcms_test.move @@ -0,0 +1,656 @@ +#[test_only] +module curse_mcms::curse_mcms_test { + use std::signer; + use std::vector; + use aptos_framework::account; + use aptos_framework::timestamp; + use aptos_framework::chain_id; + + use curse_mcms::curse_mcms; + use curse_mcms::curse_mcms_account; + + const SIGNER_1: vector = x"0000000000000000000000000000000000000001"; + const SIGNER_2: vector = x"0000000000000000000000000000000000000002"; + const SIGNER_3: vector = x"0000000000000000000000000000000000000003"; + + fun setup_test( + framework: &signer, deployer: &signer, owner: &signer + ) { + // Initialize timestamp for tests + timestamp::set_time_has_started_for_testing(framework); + timestamp::update_global_time_for_test_secs(1000); + + // Initialize chain_id + chain_id::initialize_for_test(framework, 4); + + // Create accounts + account::create_account_for_test(signer::address_of(deployer)); + account::create_account_for_test(signer::address_of(owner)); + + // Initialize CurseMCMS account (ownership) - stores AccountState at @curse_mcms + curse_mcms_account::init_module_for_testing(deployer); + + // Initialize CurseMCMS (multisigs) + curse_mcms::init_module_for_testing(deployer); + } + + // ================================================================ + // | Initialization Tests | + // ================================================================ + + #[test( + framework = @aptos_framework, deployer = @curse_mcms, owner = @curse_mcms_owner + )] + fun test_initialize_success( + framework: &signer, deployer: &signer, owner: &signer + ) { + setup_test(framework, deployer, owner); + + // Verify owner is set correctly via curse_mcms_account + assert!(curse_mcms_account::owner() == signer::address_of(owner), 1); + } + + #[test( + framework = @aptos_framework, deployer = @curse_mcms, owner = @curse_mcms_owner + )] + #[expected_failure(major_status = 4004, location = curse_mcms::curse_mcms)] + // RESOURCE_ALREADY_EXISTS + fun test_initialize_already_initialized( + framework: &signer, deployer: &signer, owner: &signer + ) { + setup_test(framework, deployer, owner); + + // Try to initialize again - should fail with RESOURCE_ALREADY_EXISTS + curse_mcms::init_module_for_testing(deployer); + } + + // ================================================================ + // | Ownership Tests | + // ================================================================ + + #[ + test( + framework = @aptos_framework, + deployer = @curse_mcms, + owner = @curse_mcms_owner, + new_owner = @0x123 + ) + ] + fun test_transfer_ownership_success( + framework: &signer, + deployer: &signer, + owner: &signer, + new_owner: &signer + ) { + setup_test(framework, deployer, owner); + account::create_account_for_test(signer::address_of(new_owner)); + + // Transfer ownership via curse_mcms_account + curse_mcms_account::transfer_ownership(owner, signer::address_of(new_owner)); + + // Verify owner hasn't changed yet + assert!(curse_mcms_account::owner() == signer::address_of(owner), 1); + + // Accept ownership + curse_mcms_account::accept_ownership(new_owner); + + // Verify new owner + assert!(curse_mcms_account::owner() == signer::address_of(new_owner), 2); + } + + #[ + test( + framework = @aptos_framework, + deployer = @curse_mcms, + owner = @curse_mcms_owner, + not_owner = @0x456 + ) + ] + // error::permission_denied(E_UNAUTHORIZED) = (5 << 16) | 3 = 327683 + #[expected_failure(abort_code = 327683, location = curse_mcms::curse_mcms_account)] + fun test_transfer_ownership_not_owner( + framework: &signer, + deployer: &signer, + owner: &signer, + not_owner: &signer + ) { + setup_test(framework, deployer, owner); + account::create_account_for_test(signer::address_of(not_owner)); + + // Non-owner tries to transfer - should fail + curse_mcms_account::transfer_ownership(not_owner, @0x999); + } + + #[ + test( + framework = @aptos_framework, + deployer = @curse_mcms, + owner = @curse_mcms_owner, + wrong_accepter = @0x789 + ) + ] + // error::permission_denied(E_MUST_BE_PROPOSED_OWNER) = (5 << 16) | 2 = 327682 + #[expected_failure(abort_code = 327682, location = curse_mcms::curse_mcms_account)] + fun test_accept_ownership_wrong_accepter( + framework: &signer, + deployer: &signer, + owner: &signer, + wrong_accepter: &signer + ) { + setup_test(framework, deployer, owner); + account::create_account_for_test(signer::address_of(wrong_accepter)); + + // Transfer to different address + curse_mcms_account::transfer_ownership(owner, @0x123); + + // Wrong address tries to accept - should fail + curse_mcms_account::accept_ownership(wrong_accepter); + } + + // ================================================================ + // | Set Config Tests | + // ================================================================ + + #[test( + framework = @aptos_framework, deployer = @curse_mcms, owner = @curse_mcms_owner + )] + fun test_set_config_success( + framework: &signer, deployer: &signer, owner: &signer + ) { + setup_test(framework, deployer, owner); + + let role = curse_mcms::bypasser_role(); + + // Set up signers + let signer_addresses = vector[SIGNER_1, SIGNER_2]; + let signer_groups = vector[0u8, 0u8]; + + // Set up group quorums (2 signatures required in group 0) + let group_quorums = vector[ + 2u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0 + ]; + + // Set up group parents (group 0 is root, points to itself) + let group_parents = vector[ + 0u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0 + ]; + + curse_mcms::set_config( + owner, + role, + signer_addresses, + signer_groups, + group_quorums, + group_parents, + false + ); + + // Verify config was set + let config = curse_mcms::get_config(role); + let _ = config; // Verify it was retrieved successfully + } + + #[ + test( + framework = @aptos_framework, + deployer = @curse_mcms, + owner = @curse_mcms_owner, + not_owner = @0x456 + ) + ] + // error::permission_denied(E_UNAUTHORIZED) = (5 << 16) | 3 = 327683 + #[expected_failure(abort_code = 327683, location = curse_mcms::curse_mcms_account)] + fun test_set_config_not_owner( + framework: &signer, + deployer: &signer, + owner: &signer, + not_owner: &signer + ) { + setup_test(framework, deployer, owner); + account::create_account_for_test(signer::address_of(not_owner)); + + let role = curse_mcms::bypasser_role(); + + let signer_addresses = vector[SIGNER_1]; + let signer_groups = vector[0u8]; + let group_quorums = vector[ + 1u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0 + ]; + let group_parents = vector[ + 0u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0 + ]; + + // Non-owner tries to set config - should fail + curse_mcms::set_config( + not_owner, + role, + signer_addresses, + signer_groups, + group_quorums, + group_parents, + false + ); + } + + #[test( + framework = @aptos_framework, deployer = @curse_mcms, owner = @curse_mcms_owner + )] + #[ + expected_failure( + abort_code = curse_mcms::curse_mcms::E_INVALID_NUM_SIGNERS, + location = curse_mcms::curse_mcms + ) + ] + fun test_set_config_empty_signers( + framework: &signer, deployer: &signer, owner: &signer + ) { + setup_test(framework, deployer, owner); + + let role = curse_mcms::bypasser_role(); + + // Empty signers should fail + let signer_addresses = vector[]; + let signer_groups = vector[]; + let group_quorums = vector[ + 0u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0 + ]; + let group_parents = vector[ + 0u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0 + ]; + + curse_mcms::set_config( + owner, + role, + signer_addresses, + signer_groups, + group_quorums, + group_parents, + false + ); + } + + #[test( + framework = @aptos_framework, deployer = @curse_mcms, owner = @curse_mcms_owner + )] + #[ + expected_failure( + abort_code = curse_mcms::curse_mcms::E_SIGNER_GROUPS_LEN_MISMATCH, + location = curse_mcms::curse_mcms + ) + ] + fun test_set_config_mismatched_lengths( + framework: &signer, deployer: &signer, owner: &signer + ) { + setup_test(framework, deployer, owner); + + let role = curse_mcms::bypasser_role(); + + // Mismatched signer_addresses and signer_groups + let signer_addresses = vector[SIGNER_1, SIGNER_2]; + let signer_groups = vector[0u8]; // Only 1 group for 2 addresses + let group_quorums = vector[ + 1u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0 + ]; + let group_parents = vector[ + 0u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0 + ]; + + curse_mcms::set_config( + owner, + role, + signer_addresses, + signer_groups, + group_quorums, + group_parents, + false + ); + } + + #[test( + framework = @aptos_framework, deployer = @curse_mcms, owner = @curse_mcms_owner + )] + #[ + expected_failure( + abort_code = curse_mcms::curse_mcms::E_SIGNER_ADDR_MUST_BE_INCREASING, + location = curse_mcms::curse_mcms + ) + ] + fun test_set_config_signers_not_increasing( + framework: &signer, deployer: &signer, owner: &signer + ) { + setup_test(framework, deployer, owner); + + let role = curse_mcms::bypasser_role(); + + // Signers not in increasing order (SIGNER_2 < SIGNER_1 lexicographically) + let signer_addresses = vector[SIGNER_2, SIGNER_1]; // Wrong order + let signer_groups = vector[0u8, 0u8]; + let group_quorums = vector[ + 2u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0 + ]; + let group_parents = vector[ + 0u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0 + ]; + + curse_mcms::set_config( + owner, + role, + signer_addresses, + signer_groups, + group_quorums, + group_parents, + false + ); + } + + #[test( + framework = @aptos_framework, deployer = @curse_mcms, owner = @curse_mcms_owner + )] + #[ + expected_failure( + abort_code = curse_mcms::curse_mcms::E_INVALID_SIGNER_ADDR_LEN, + location = curse_mcms::curse_mcms + ) + ] + fun test_set_config_invalid_signer_length( + framework: &signer, deployer: &signer, owner: &signer + ) { + setup_test(framework, deployer, owner); + + let role = curse_mcms::bypasser_role(); + + // Invalid signer address length (not 20 bytes) + let signer_addresses = vector[x"0000000000000000000000000000000000000001AA"]; // 21 bytes + let signer_groups = vector[0u8]; + let group_quorums = vector[ + 1u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0 + ]; + let group_parents = vector[ + 0u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0 + ]; + + curse_mcms::set_config( + owner, + role, + signer_addresses, + signer_groups, + group_quorums, + group_parents, + false + ); + } + + // ================================================================ + // | View Function Tests | + // ================================================================ + + #[test( + framework = @aptos_framework, deployer = @curse_mcms, owner = @curse_mcms_owner + )] + fun test_get_op_count( + framework: &signer, deployer: &signer, owner: &signer + ) { + setup_test(framework, deployer, owner); + + let role = curse_mcms::bypasser_role(); + + // Initial op count should be 0 + assert!(curse_mcms::get_op_count(role) == 0, 1); + } + + #[test( + framework = @aptos_framework, deployer = @curse_mcms, owner = @curse_mcms_owner + )] + fun test_get_root( + framework: &signer, deployer: &signer, owner: &signer + ) { + setup_test(framework, deployer, owner); + + let role = curse_mcms::bypasser_role(); + + let (root, valid_until) = curse_mcms::get_root(role); + assert!(vector::length(&root) == 0, 1); + assert!(valid_until == 0, 2); + } + + #[test( + framework = @aptos_framework, deployer = @curse_mcms, owner = @curse_mcms_owner + )] + fun test_role_constants( + framework: &signer, deployer: &signer, owner: &signer + ) { + setup_test(framework, deployer, owner); + + assert!(curse_mcms::bypasser_role() == 0, 1); + assert!(curse_mcms::canceller_role() == 1, 2); + assert!(curse_mcms::proposer_role() == 2, 3); + assert!(curse_mcms::timelock_role() == 3, 4); + } + + #[test( + framework = @aptos_framework, deployer = @curse_mcms, owner = @curse_mcms_owner + )] + fun test_is_valid_role( + framework: &signer, deployer: &signer, owner: &signer + ) { + setup_test(framework, deployer, owner); + + assert!(curse_mcms::is_valid_role(0), 1); + assert!(curse_mcms::is_valid_role(1), 2); + assert!(curse_mcms::is_valid_role(2), 3); + assert!(curse_mcms::is_valid_role(3), 4); + assert!(!curse_mcms::is_valid_role(4), 5); + assert!(!curse_mcms::is_valid_role(255), 6); + } + + // ================================================================ + // | Merkle Proof Tests | + // ================================================================ + + #[test] + fun test_verify_merkle_proof_single_leaf() { + // For a single leaf tree, the root equals the leaf + let leaf = x"abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"; + let root = leaf; + let proof = vector[]; + + assert!( + curse_mcms::verify_merkle_proof(proof, root, leaf), + 1 + ); + } + + #[test] + fun test_compute_eth_message_hash() { + let root = x"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; + let valid_until = 1000000u64; + + // Should not abort + let _hash = curse_mcms::compute_eth_message_hash(root, valid_until); + } + + // ================================================================ + // | Hash Tests | + // ================================================================ + + #[test( + framework = @aptos_framework, deployer = @curse_mcms, owner = @curse_mcms_owner + )] + fun test_hash_metadata_leaf( + framework: &signer, deployer: &signer, owner: &signer + ) { + setup_test(framework, deployer, owner); + + let role = curse_mcms::bypasser_role(); + + let hash = + curse_mcms::test_hash_metadata_leaf( + role, + 4u256, // chain_id + @curse_mcms, // multisig + 0u64, // pre_op_count + 1u64, // post_op_count + false // override_previous_root + ); + + // Should produce a 32-byte hash + assert!(vector::length(&hash) == 32, 1); + } + + // ================================================================ + // | Set Config with Clear Root Tests | + // ================================================================ + + #[test( + framework = @aptos_framework, deployer = @curse_mcms, owner = @curse_mcms_owner + )] + fun test_set_config_with_clear_root( + framework: &signer, deployer: &signer, owner: &signer + ) { + setup_test(framework, deployer, owner); + + let role = curse_mcms::bypasser_role(); + + let signer_addresses = vector[SIGNER_1, SIGNER_2]; + let signer_groups = vector[0u8, 0u8]; + let group_quorums = vector[ + 2u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0 + ]; + let group_parents = vector[ + 0u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0 + ]; + + // Set config with clear_root = true + curse_mcms::set_config( + owner, + role, + signer_addresses, + signer_groups, + group_quorums, + group_parents, + true // clear_root + ); + + // Verify root was cleared + let (root, valid_until) = curse_mcms::get_root(role); + assert!(vector::length(&root) == 0, 1); + assert!(valid_until == 0, 2); + + // Verify metadata was updated + let metadata = curse_mcms::get_root_metadata(role); + let _ = metadata; + } + + // ================================================================ + // | Hierarchical Group Tests | + // ================================================================ + + #[test( + framework = @aptos_framework, deployer = @curse_mcms, owner = @curse_mcms_owner + )] + fun test_set_config_hierarchical_groups( + framework: &signer, deployer: &signer, owner: &signer + ) { + setup_test(framework, deployer, owner); + + let role = curse_mcms::proposer_role(); + + // Set up 3 signers in 2 groups + // Group 1: SIGNER_1, SIGNER_2 (quorum 2) + // Group 0 (root): gets 1 vote from group 1 (quorum 1) + let signer_addresses = vector[SIGNER_1, SIGNER_2, SIGNER_3]; + let signer_groups = vector[1u8, 1u8, 0u8]; // 2 in group 1, 1 in group 0 + + // Group 0 needs quorum 2 (1 from group 1 passing + 1 from SIGNER_3) + // Group 1 needs quorum 2 (both SIGNER_1 and SIGNER_2) + let group_quorums = vector[ + 2u8, 2u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0 + ]; + + // Group 1 reports to group 0 + let group_parents = vector[ + 0u8, 0u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0 + ]; + + curse_mcms::set_config( + owner, + role, + signer_addresses, + signer_groups, + group_quorums, + group_parents, + false + ); + + // Config should be set successfully + let config = curse_mcms::get_config(role); + let _ = config; + } + + // ================================================================ + // | Multiple Roles Tests | + // ================================================================ + + #[test( + framework = @aptos_framework, deployer = @curse_mcms, owner = @curse_mcms_owner + )] + fun test_set_config_multiple_roles( + framework: &signer, deployer: &signer, owner: &signer + ) { + setup_test(framework, deployer, owner); + + let signer_addresses = vector[SIGNER_1]; + let signer_groups = vector[0u8]; + let group_quorums = vector[ + 1u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0 + ]; + let group_parents = vector[ + 0u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0 + ]; + + // Set config for bypasser + curse_mcms::set_config( + owner, + curse_mcms::bypasser_role(), + signer_addresses, + signer_groups, + group_quorums, + group_parents, + false + ); + + // Set different config for proposer + let signer_addresses_2 = vector[SIGNER_2]; + curse_mcms::set_config( + owner, + curse_mcms::proposer_role(), + signer_addresses_2, + signer_groups, + group_quorums, + group_parents, + false + ); + + // Verify configs are independent + let _config_bypasser = curse_mcms::get_config(curse_mcms::bypasser_role()); + let _config_proposer = curse_mcms::get_config(curse_mcms::proposer_role()); + } +} diff --git a/gen.go b/gen.go index 843884c7..2df5db7d 100644 --- a/gen.go +++ b/gen.go @@ -38,6 +38,8 @@ package aptos //go:generate go run ./cmd/bindgen --input ./contracts/mcms/mcms/sources/mcms_executor.move --output ./bindings/mcms/mcms_executor //go:generate go run ./cmd/bindgen --input ./contracts/mcms/mcms/sources/mcms_registry.move --output ./bindings/mcms/mcms_registry +//go:generate go run ./cmd/bindgen --input ./contracts/mcms/curse_mcms/sources/curse_mcms.move --output ./bindings/curse_mcms/curse_mcms + //go:generate go run ./cmd/bindgen --input ./contracts/mcms/mcms_test/sources/mcms_user.move --output ./bindings/mcms_test/mcms_user //go:generate go run ./cmd/bindgen --input ./contracts/data-feeds/sources/registry.move --output ./bindings/data_feeds/registry diff --git a/relayer/txm/curse_mcms_test.go b/relayer/txm/curse_mcms_test.go new file mode 100644 index 00000000..3ab8c840 --- /dev/null +++ b/relayer/txm/curse_mcms_test.go @@ -0,0 +1,428 @@ +package txm + +import ( + "bytes" + "encoding/hex" + "fmt" + "math/big" + "sort" + "testing" + + "github.com/aptos-labs/aptos-go-sdk" + "github.com/aptos-labs/aptos-go-sdk/bcs" + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/require" +) + +// CurseMCMS uses the same domain separators as the full MCMS +// These are keccak256 hashes of the domain separator strings +var ( + // keccak256("MANY_CHAIN_MULTI_SIG_DOMAIN_SEPARATOR_METADATA_APTOS") + MANY_CHAIN_MULTI_SIG_DOMAIN_SEPARATOR_METADATA = crypto.Keccak256([]byte("MANY_CHAIN_MULTI_SIG_DOMAIN_SEPARATOR_METADATA_APTOS")) + // keccak256("MANY_CHAIN_MULTI_SIG_DOMAIN_SEPARATOR_OP_APTOS") + MANY_CHAIN_MULTI_SIG_DOMAIN_SEPARATOR_OP = crypto.Keccak256([]byte("MANY_CHAIN_MULTI_SIG_DOMAIN_SEPARATOR_OP_APTOS")) +) + +// CurseMCMS RootMetadata - same as full MCMS +type CurseMCMSRootMetadata struct { + Role uint8 + ChainID *big.Int + MultiSig aptos.AccountAddress + PreOpCount uint64 + PostOpCount uint64 + OverridePreviousRoot bool +} + +// CurseMCMS Op - same as full MCMS +type CurseMCMSOp struct { + Role uint8 + ChainID *big.Int + MultiSig aptos.AccountAddress + Nonce uint64 + To aptos.AccountAddress + ModuleName string + FunctionName string + Data []byte +} + +// HashCurseMCMSRootMetadata computes the hash of CurseMCMS root metadata +func HashCurseMCMSRootMetadata(metadata CurseMCMSRootMetadata) ([32]byte, error) { + ser := bcs.Serializer{} + ser.FixedBytes(MANY_CHAIN_MULTI_SIG_DOMAIN_SEPARATOR_METADATA) + ser.U8(metadata.Role) + ser.U256(*metadata.ChainID) + ser.Struct(&metadata.MultiSig) + ser.U64(metadata.PreOpCount) + ser.U64(metadata.PostOpCount) + ser.Bool(metadata.OverridePreviousRoot) + + if err := ser.Error(); err != nil { + return [32]byte{}, err + } + + return crypto.Keccak256Hash(ser.ToBytes()), nil +} + +// HashCurseMCMSOp computes the hash of a CurseMCMS operation +func HashCurseMCMSOp(op *CurseMCMSOp) ([32]byte, error) { + ser := bcs.Serializer{} + ser.FixedBytes(MANY_CHAIN_MULTI_SIG_DOMAIN_SEPARATOR_OP) + ser.U8(op.Role) + ser.U256(*op.ChainID) + ser.Struct(&op.MultiSig) + ser.U64(op.Nonce) + ser.Struct(&op.To) + ser.WriteString(op.ModuleName) + ser.WriteString(op.FunctionName) + ser.WriteBytes(op.Data) + + if err := ser.Error(); err != nil { + return [32]byte{}, err + } + + return crypto.Keccak256Hash(ser.ToBytes()), nil +} + +// GenerateCurseMCMSMerkleTree creates a merkle tree for CurseMCMS operations +func GenerateCurseMCMSMerkleTree(ops []CurseMCMSOp, metadata CurseMCMSRootMetadata) (MerkleTree, error) { + leaves := make([][32]byte, len(ops)+1) + rootHash, err := HashCurseMCMSRootMetadata(metadata) + if err != nil { + return nil, fmt.Errorf("failed to hash root metadata: %w", err) + } + leaves[0] = rootHash + for i, op := range ops { + hashOp, err := HashCurseMCMSOp(&op) + if err != nil { + return nil, fmt.Errorf("failed to hash operation: %w", err) + } + leaves[i+1] = hashOp + } + return NewMerkleTree(leaves) +} + +// SerializeCurseSubject serializes a single curse subject (vector) +func SerializeCurseSubject(subject []byte) ([]byte, error) { + return bcs.SerializeSingle(func(ser *bcs.Serializer) { + ser.WriteBytes(subject) + }) +} + +// SerializeCurseMultipleSubjects serializes multiple curse subjects (vector>) +func SerializeCurseMultipleSubjects(subjects [][]byte) ([]byte, error) { + return bcs.SerializeSingle(func(ser *bcs.Serializer) { + ser.Uleb128(uint32(len(subjects))) + for _, subject := range subjects { + ser.WriteBytes(subject) + } + }) +} + +// SerializeBypasserExecuteBatch serializes the data for timelock_bypasser_execute_batch +// Parameters: subjects: vector>, function_names: vector, datas: vector> +func SerializeBypasserExecuteBatch(subjects [][]byte, functionNames []string, datas [][]byte) ([]byte, error) { + return bcs.SerializeSingle(func(ser *bcs.Serializer) { + // subjects: vector> + ser.Uleb128(uint32(len(subjects))) + for _, subject := range subjects { + ser.WriteBytes(subject) + } + // function_names: vector + ser.Uleb128(uint32(len(functionNames))) + for _, fn := range functionNames { + ser.WriteString(fn) + } + // datas: vector> + ser.Uleb128(uint32(len(datas))) + for _, data := range datas { + ser.WriteBytes(data) + } + }) +} + +// GenerateDeterministicSigners creates signers with fixed private keys for reproducible tests +func GenerateDeterministicSigners(t *testing.T) []Signer { + // Fixed private keys for deterministic test data + // These are test keys ONLY - never use in production + privateKeyHexes := []string{ + "0000000000000000000000000000000000000000000000000000000000000001", + "0000000000000000000000000000000000000000000000000000000000000002", + "0000000000000000000000000000000000000000000000000000000000000003", + } + + signers := make([]Signer, len(privateKeyHexes)) + for i, hexKey := range privateKeyHexes { + keyBytes, err := hex.DecodeString(hexKey) + require.NoError(t, err) + privateKey, err := crypto.ToECDSA(keyBytes) + require.NoError(t, err) + signers[i] = Signer{ + Address: crypto.PubkeyToAddress(privateKey.PublicKey).Bytes(), + PrivateKey: privateKey, + } + } + + // Sort signers by address as required by the module + sort.Slice(signers, func(i, j int) bool { + return bytes.Compare(signers[i].Address, signers[j].Address) < 0 + }) + + return signers +} + +// TestGenerateCurseMCMSTestData generates test data for CurseMCMS Move tests +// Run with: go test -v -run TestGenerateCurseMCMSTestData ./relayer/txm/ +func TestGenerateCurseMCMSTestData(t *testing.T) { + // Use fixed chain ID (same as Move tests) + chainID := big.NewInt(4) + + // Use BYPASSER_ROLE for testing (role 0) + role := uint8(0) + + // Use a deterministic CurseMCMS address for testing + // This should match the dev-address in Move.toml: 0xCCC + curseMCMSAddr := aptos.AccountAddress{} + err := curseMCMSAddr.ParseStringRelaxed("0x0000000000000000000000000000000000000000000000000000000000000CCC") + require.NoError(t, err) + + // Use deterministic signers for reproducible test data + signers := GenerateDeterministicSigners(t) + + // Test curse subject (GLOBAL_CURSE_SUBJECT from rmn_remote.move) + globalCurseSubject := []byte{0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01} + subject2 := []byte{0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02} + + // Create curse operation data (for rmn_remote::curse) + curseData, err := SerializeCurseSubject(globalCurseSubject) + require.NoError(t, err) + + // Create uncurse operation data (for rmn_remote::uncurse) + uncurseData, err := SerializeCurseSubject(globalCurseSubject) + require.NoError(t, err) + + // Create curse_multiple operation data (for rmn_remote::curse_multiple) + curseMultipleData, err := SerializeCurseMultipleSubjects([][]byte{globalCurseSubject, subject2}) + require.NoError(t, err) + + // Operation 1: timelock_bypasser_execute_batch with a single curse call + op1Data, err := SerializeBypasserExecuteBatch( + [][]byte{globalCurseSubject}, // subjects (used as identifiers) + []string{"curse"}, // function_names + [][]byte{curseData}, // datas + ) + require.NoError(t, err) + + // Operation 2: timelock_bypasser_execute_batch with a single uncurse call + op2Data, err := SerializeBypasserExecuteBatch( + [][]byte{globalCurseSubject}, // subjects + []string{"uncurse"}, // function_names + [][]byte{uncurseData}, // datas + ) + require.NoError(t, err) + + // Operation 3: timelock_bypasser_execute_batch with curse_multiple call + // Note: all three vectors must have the same length + op3Data, err := SerializeBypasserExecuteBatch( + [][]byte{globalCurseSubject}, // subjects (one identifier) + []string{"curse_multiple"}, // function_names + [][]byte{curseMultipleData}, // datas (contains both subjects) + ) + require.NoError(t, err) + + // Create operations - all target curse_mcms module (timelock dispatch) + ops := []CurseMCMSOp{ + { + Role: role, + ChainID: chainID, + MultiSig: curseMCMSAddr, + Nonce: 0, + To: curseMCMSAddr, // Target is curse_mcms itself + ModuleName: "curse_mcms", // Dispatches to curse_mcms module + FunctionName: "timelock_bypasser_execute_batch", + Data: op1Data, + }, + { + Role: role, + ChainID: chainID, + MultiSig: curseMCMSAddr, + Nonce: 1, + To: curseMCMSAddr, + ModuleName: "curse_mcms", + FunctionName: "timelock_bypasser_execute_batch", + Data: op2Data, + }, + { + Role: role, + ChainID: chainID, + MultiSig: curseMCMSAddr, + Nonce: 2, + To: curseMCMSAddr, + ModuleName: "curse_mcms", + FunctionName: "timelock_bypasser_execute_batch", + Data: op3Data, + }, + } + + // Create root metadata + metadata := CurseMCMSRootMetadata{ + Role: role, + ChainID: chainID, + MultiSig: curseMCMSAddr, + PreOpCount: 0, + PostOpCount: uint64(len(ops)), + OverridePreviousRoot: false, + } + + // Generate merkle tree + merkleTree, err := GenerateCurseMCMSMerkleTree(ops, metadata) + require.NoError(t, err) + + // Get root hash + rootHash := merkleTree.GetRoot() + + // Use a fixed validUntil timestamp for deterministic tests + // This should be far in the future for the Move tests to work + validUntil := uint64(1893456000) // Jan 1, 2030 + + // Calculate signed hash + signedHash := CalculateSignedHash(rootHash, validUntil) + + // Generate signatures (only need 2 for quorum) + signatures := GenerateSignatures(t, signers[:2], signedHash) + + // Get metadata proof + metadataProof := merkleTree.GetProof(0) + + // Get operation proofs + op1Proof := merkleTree.GetProof(1) + op2Proof := merkleTree.GetProof(2) + op3Proof := merkleTree.GetProof(3) + + // Print all values for Move tests + fmt.Println("============= BEGIN CURSE_MCMS MOVE TEST CONSTANTS =============") + fmt.Println() + + fmt.Println("// Domain Separators (same as full MCMS)") + fmt.Printf("const MANY_CHAIN_MULTI_SIG_DOMAIN_SEPARATOR_METADATA: vector = x\"%s\";\n", hex.EncodeToString(MANY_CHAIN_MULTI_SIG_DOMAIN_SEPARATOR_METADATA)) + fmt.Printf("const MANY_CHAIN_MULTI_SIG_DOMAIN_SEPARATOR_OP: vector = x\"%s\";\n", hex.EncodeToString(MANY_CHAIN_MULTI_SIG_DOMAIN_SEPARATOR_OP)) + fmt.Println() + + fmt.Println("// Test Configuration") + fmt.Printf("const CHAIN_ID: u256 = %d;\n", chainID) + fmt.Printf("const VALID_UNTIL: u64 = %d;\n", validUntil) + fmt.Printf("const TEST_ROLE: u8 = %d;\n", role) + fmt.Println() + + fmt.Println("// Contract Addresses") + fmt.Printf("// CURSE_MCMS_ADDR: @0x%s\n", hex.EncodeToString(curseMCMSAddr[:])) + fmt.Println() + + fmt.Println("// Signer Addresses (sorted, 20 bytes each)") + for i, signer := range signers { + fmt.Printf("const SIGNER_%d: vector = x\"%s\";\n", i+1, hex.EncodeToString(signer.Address)) + } + fmt.Println() + + fmt.Println("// Root and Metadata") + fmt.Printf("const ROOT: vector = x\"%s\";\n", hex.EncodeToString(rootHash[:])) + fmt.Println() + + fmt.Println("// Metadata Proof") + fmt.Print("const METADATA_PROOF: vector> = vector[\n") + for i, p := range metadataProof { + if i < len(metadataProof)-1 { + fmt.Printf(" x\"%s\",\n", hex.EncodeToString(p[:])) + } else { + fmt.Printf(" x\"%s\"\n", hex.EncodeToString(p[:])) + } + } + fmt.Println("];") + fmt.Println() + + fmt.Println("// Signatures (2-of-3 quorum)") + fmt.Print("const SIGNATURES: vector> = vector[\n") + for i, sig := range signatures { + if i < len(signatures)-1 { + fmt.Printf(" x\"%s\",\n", hex.EncodeToString(sig)) + } else { + fmt.Printf(" x\"%s\"\n", hex.EncodeToString(sig)) + } + } + fmt.Println("];") + fmt.Println() + + fmt.Println("// All operations target curse_mcms module and timelock_bypasser_execute_batch function") + fmt.Printf("const TARGET_MODULE: vector = b\"%s\";\n", ops[0].ModuleName) + fmt.Printf("const TARGET_FUNCTION: vector = b\"%s\";\n", ops[0].FunctionName) + fmt.Println() + + fmt.Println("// Operation 1: curse via timelock_bypasser_execute_batch") + fmt.Printf("const OP1_NONCE: u64 = %d;\n", ops[0].Nonce) + fmt.Printf("const OP1_DATA: vector = x\"%s\";\n", hex.EncodeToString(ops[0].Data)) + fmt.Print("const OP1_PROOF: vector> = vector[\n") + for i, p := range op1Proof { + if i < len(op1Proof)-1 { + fmt.Printf(" x\"%s\",\n", hex.EncodeToString(p[:])) + } else { + fmt.Printf(" x\"%s\"\n", hex.EncodeToString(p[:])) + } + } + fmt.Println("];") + fmt.Println() + + fmt.Println("// Operation 2: uncurse via timelock_bypasser_execute_batch") + fmt.Printf("const OP2_NONCE: u64 = %d;\n", ops[1].Nonce) + fmt.Printf("const OP2_DATA: vector = x\"%s\";\n", hex.EncodeToString(ops[1].Data)) + fmt.Print("const OP2_PROOF: vector> = vector[\n") + for i, p := range op2Proof { + if i < len(op2Proof)-1 { + fmt.Printf(" x\"%s\",\n", hex.EncodeToString(p[:])) + } else { + fmt.Printf(" x\"%s\"\n", hex.EncodeToString(p[:])) + } + } + fmt.Println("];") + fmt.Println() + + fmt.Println("// Operation 3: curse_multiple via timelock_bypasser_execute_batch") + fmt.Printf("const OP3_NONCE: u64 = %d;\n", ops[2].Nonce) + fmt.Printf("const OP3_DATA: vector = x\"%s\";\n", hex.EncodeToString(ops[2].Data)) + fmt.Print("const OP3_PROOF: vector> = vector[\n") + for i, p := range op3Proof { + if i < len(op3Proof)-1 { + fmt.Printf(" x\"%s\",\n", hex.EncodeToString(p[:])) + } else { + fmt.Printf(" x\"%s\"\n", hex.EncodeToString(p[:])) + } + } + fmt.Println("];") + fmt.Println() + + fmt.Println("// Curse Subjects") + fmt.Printf("const GLOBAL_CURSE_SUBJECT: vector = x\"%s\";\n", hex.EncodeToString(globalCurseSubject)) + fmt.Printf("const SUBJECT_2: vector = x\"%s\";\n", hex.EncodeToString(subject2)) + fmt.Println() + + fmt.Println("============= END CURSE_MCMS MOVE TEST CONSTANTS =============") + + // Verify the merkle tree + hashedMetadata, err := HashCurseMCMSRootMetadata(metadata) + require.NoError(t, err) + require.True(t, merkleTree.VerifyProof(metadataProof, hashedMetadata), "Metadata proof verification failed") + + for i, op := range ops { + hashedOp, err := HashCurseMCMSOp(&op) + require.NoError(t, err) + proof := merkleTree.GetProof(i + 1) + require.True(t, merkleTree.VerifyProof(proof, hashedOp), "Op %d proof verification failed", i) + } +} + +// TestCurseMCMSDomainSeparators prints the domain separators for verification +func TestCurseMCMSDomainSeparators(t *testing.T) { + metadataSep := crypto.Keccak256([]byte("MANY_CHAIN_MULTI_SIG_DOMAIN_SEPARATOR_METADATA_APTOS")) + opSep := crypto.Keccak256([]byte("MANY_CHAIN_MULTI_SIG_DOMAIN_SEPARATOR_OP_APTOS")) + + fmt.Printf("MANY_CHAIN_MULTI_SIG_DOMAIN_SEPARATOR_METADATA: %s\n", hex.EncodeToString(metadataSep)) + fmt.Printf("MANY_CHAIN_MULTI_SIG_DOMAIN_SEPARATOR_OP: %s\n", hex.EncodeToString(opSep)) +}