diff --git a/packages/layerzero-v2/aptos/contracts/deployer/Move.toml b/packages/layerzero-v2/aptos/contracts/deployer/Move.toml new file mode 100644 index 00000000..098151f9 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/deployer/Move.toml @@ -0,0 +1,17 @@ +[package] +name = "deployer" +version = "1.0.0" +authors = [] + +[addresses] +deployer = "_" + +[dev-addresses] +deployer = "0x90909090" + +[dependencies.AptosFramework] +git = "https://github.com/aptos-labs/aptos-framework.git" +rev = "mainnet" +subdir = "aptos-framework" + +[dev-dependencies] diff --git a/packages/layerzero-v2/aptos/contracts/deployer/sources/object_code_deployment.move b/packages/layerzero-v2/aptos/contracts/deployer/sources/object_code_deployment.move new file mode 100644 index 00000000..22f8d03c --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/deployer/sources/object_code_deployment.move @@ -0,0 +1,206 @@ +/// This is a fork of the original code from the Move standard library. +/// The original code can be found at aptos_framework::object_code_deployment. +/// This has been modified for the following: +/// 1) Accept a seed for the object creation to provide deterministic addresses +/// 2) Convenience view function to generate the object address given the publisher address and the object seed. +/// 3) Function to generate a signer for the code object to be used in scripts +/// 4) Function to destroy the ExtendRef for the code object +/// 5) Extract repeated code into a function to assert the owner of the code object +/// +/// =================================================================================================================== +/// +/// This module allows users to deploy, upgrade and freeze modules deployed to objects on-chain. +/// This enables users to deploy modules to an object with a unique address each time they are published. +/// This modules provides an alternative method to publish code on-chain, where code is deployed to objects rather than accounts. +/// This is encouraged as it abstracts the necessary resources needed for deploying modules, +/// along with the required authorization to upgrade and freeze modules. +/// +/// The functionalities of this module are as follows. +/// +/// Publishing modules flow: +/// 1. Create a new object with the address derived from the publisher address and the object seed. +/// 2. Publish the module passed in the function via `metadata_serialized` and `code` to the newly created object. +/// 3. Emits 'Publish' event with the address of the newly created object. +/// 4. Create a `ManagingRefs` which stores the extend ref of the newly created object. +/// Note: This is needed to upgrade the code as the signer must be generated to upgrade the existing code in an object. +/// +/// Upgrading modules flow: +/// 1. Assert the `code_object` passed in the function is owned by the `publisher`. +/// 2. Assert the `code_object` passed in the function exists in global storage. +/// 2. Retrieve the `ExtendRef` from the `code_object` and generate the signer from this. +/// 3. Upgrade the module with the `metadata_serialized` and `code` passed in the function. +/// 4. Emits 'Upgrade' event with the address of the object with the upgraded code. +/// Note: If the modules were deployed as immutable when calling `publish`, the upgrade will fail. +/// +/// Freezing modules flow: +/// 1. Assert the `code_object` passed in the function exists in global storage. +/// 2. Assert the `code_object` passed in the function is owned by the `publisher`. +/// 3. Mark all the modules in the `code_object` as immutable. +/// 4. Emits 'Freeze' event with the address of the object with the frozen code. +/// Note: There is no unfreeze function as this gives no benefit if the user can freeze/unfreeze modules at will. +/// Once modules are marked as immutable, they cannot be made mutable again. +module deployer::object_code_deployment { + use std::error; + use std::signer; + use std::code; + use std::event; + use std::object::{Self, address_to_object, ExtendRef, ObjectCore}; + + /// Object code deployment feature not supported. + const EOBJECT_CODE_DEPLOYMENT_NOT_SUPPORTED: u64 = 1; + /// Not the owner of the `code_object` + const ENOT_CODE_OBJECT_OWNER: u64 = 2; + /// `code_object` does not exist. + const ECODE_OBJECT_DOES_NOT_EXIST: u64 = 3; + /// Arbitrary signer generation is disabled for the code object. + const EARBITRARY_SIGNER_DISABLED: u64 = 4; + + const OBJECT_CODE_DEPLOYMENT_DOMAIN_SEPARATOR: vector = b"aptos_framework::object_code_deployment"; + + #[resource_group_member(group = aptos_framework::object::ObjectGroup)] + /// Internal struct, attached to the object, that holds Refs we need to manage the code deployment (i.e. upgrades). + struct ManagingRefs has key { + /// We need to keep the extend ref to be able to generate the signer to upgrade existing code. + extend_ref: ExtendRef, + /// This flag controls whether this module can release a signer for the code object. + arbitrary_signer_enabled: bool, + } + + #[event] + /// Event emitted when code is published to an object. + struct Publish has drop, store { + object_address: address, + } + + #[event] + /// Event emitted when code in an existing object is upgraded. + struct Upgrade has drop, store { + object_address: address, + } + + #[event] + /// Event emitted when code in an existing object is made immutable. + struct Freeze has drop, store { + object_address: address, + } + + #[event] + struct DisableArbitrarySigner has drop, store { + object_address: address, + } + + #[event] + struct DestroyRefs has drop, store { + object_address: address, + } + + #[view] + /// Gets the object address given the publisher address and the object seed + public fun compute_object_address(publisher: address, object_seed: vector): address { + object::create_object_address(&publisher, object_seed) + } + + /// Creates a new object with a unique address derived from the publisher address and the object seed. + /// Publishes the code passed in the function to the newly created object. + /// The caller must provide package metadata describing the package via `metadata_serialized` and + /// the code to be published via `code`. This contains a vector of modules to be deployed on-chain. + public entry fun publish( + publisher: &signer, + object_seed: vector, + metadata_serialized: vector, + code: vector>, + ) { + let constructor_ref = &object::create_named_object(publisher, object_seed); + let code_signer = &object::generate_signer(constructor_ref); + code::publish_package_txn(code_signer, metadata_serialized, code); + + event::emit(Publish { object_address: signer::address_of(code_signer) }); + + move_to(code_signer, ManagingRefs { + extend_ref: object::generate_extend_ref(constructor_ref), + arbitrary_signer_enabled: true, + }); + } + + /// Upgrades the existing modules at the `code_object` address with the new modules passed in `code`, + /// along with the metadata `metadata_serialized`. + /// Note: If the modules were deployed as immutable when calling `publish`, the upgrade will fail. + /// Requires the publisher to be the owner of the `code_object`. + public entry fun upgrade( + publisher: &signer, + metadata_serialized: vector, + code: vector>, + code_object: address, + ) acquires ManagingRefs { + assert_owner_and_code_object(signer::address_of(move publisher), code_object); + + let extend_ref = &borrow_global(code_object).extend_ref; + let code_signer = &object::generate_signer_for_extending(extend_ref); + code::publish_package_txn(code_signer, metadata_serialized, code); + + event::emit(Upgrade { object_address: signer::address_of(code_signer) }); + } + + /// Get an arbitrary signer for the code object, which can be used in scripts or other transactions. + public fun get_code_object_signer( + publisher: &signer, + code_object: address, + ): signer acquires ManagingRefs { + assert_owner_and_code_object(signer::address_of(move publisher), code_object); + + // Check if arbitrary signer generation is enabled for the code object + assert!( + borrow_global(code_object).arbitrary_signer_enabled, + EARBITRARY_SIGNER_DISABLED, + ); + + let extend_ref = &borrow_global(code_object).extend_ref; + object::generate_signer_for_extending(extend_ref) + } + + /// Disable the ability to generate arbitrary signers for the code object. + public entry fun disable_arbitrary_signer(publisher: &signer, code_object: address) acquires ManagingRefs { + let publisher_address = signer::address_of(move publisher); + assert_owner_and_code_object(publisher_address, code_object); + + // Permanently disable the ability to generate arbitrary signers for the code object + borrow_global_mut(code_object).arbitrary_signer_enabled = false; + + event::emit(DisableArbitrarySigner { object_address: code_object }); + } + + /// Make an existing upgradable package immutable. Once this is called, the package cannot be made upgradable again. + /// Each `code_object` should only have one package, as one package is deployed per object in this module. + /// Requires the `publisher` to be the owner of the `code_object`. + public entry fun freeze_code_object(publisher: &signer, code_object: address) { + code::freeze_code_object(publisher, address_to_object(code_object)); + + event::emit(Freeze { object_address: code_object }); + } + + /// This permanently destroys the ability to upgrade or sign on behalf of the code object. + /// This is effectively equivalent to transferring the code object to a burn object, but is more explicit in + /// destroying the ExtendRef. + public entry fun destroy_refs(publisher: &signer, code_object: address) acquires ManagingRefs { + let publisher_address = signer::address_of(move publisher); + assert_owner_and_code_object(publisher_address, code_object); + + let ManagingRefs { + extend_ref: _, + arbitrary_signer_enabled: _, + } = move_from(publisher_address); + + event::emit(DestroyRefs { object_address: code_object }); + } + + /// Internal function to assert the owner of the `code_object` and asset that there is a code object at the location + fun assert_owner_and_code_object(publisher_address: address, code_object: address) { + assert!( + object::is_owner(address_to_object(code_object), publisher_address), + error::permission_denied(ENOT_CODE_OBJECT_OWNER), + ); + + let code_object_address = code_object; + assert!(exists(code_object_address), error::not_found(ECODE_OBJECT_DOES_NOT_EXIST)); + } +} diff --git a/packages/layerzero-v2/aptos/contracts/endpoint_v2/Move.toml b/packages/layerzero-v2/aptos/contracts/endpoint_v2/Move.toml new file mode 100644 index 00000000..6a4ec462 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/endpoint_v2/Move.toml @@ -0,0 +1,67 @@ +[package] +name = "endpoint_v2" +version = "1.0.0" +authors = [] + +[addresses] +router_node_0 = "_" +simple_msglib = "_" +blocked_msglib = "_" +uln_302 = "_" +router_node_1 = "_" +endpoint_v2_common = "_" +endpoint_v2 = "_" +layerzero_admin = "_" +layerzero_treasury_admin = "_" +msglib_types = "_" +treasury = "_" +executor_fee_lib_router_0 = "_" +executor_fee_lib_router_1 = "_" +dvn_fee_lib_router_0 = "_" +dvn_fee_lib_router_1 = "_" +price_feed_router_0 = "_" +price_feed_router_1 = "_" +price_feed_module_0 = "_" +worker_common = "_" +executor_fee_lib_0 = "_" +dvn_fee_lib_0 = "_" +dvn = "_" +native_token_metadata_address = "0xa" +# For Initia: "0x8e4733bdabcf7d4afc3d14f0dd46c9bf52fb0fce9e4b996c939e195b8bc891d9" + +[dev-addresses] +router_node_0 = "0x9001" +simple_msglib = "0x9002" +blocked_msglib = "0x9003" +uln_302 = "0x9005" +router_node_1 = "0x9007" +endpoint_v2_common = "0x9098" +endpoint_v2 = "0x12345678" +layerzero_admin = "0x18943124" +layerzero_treasury_admin = "0x1894312499" +msglib_types = "0x5211234" +treasury = "0x123432432" +executor_fee_lib_router_0 = "0x30001" +executor_fee_lib_router_1 = "0x3465342143" +dvn_fee_lib_router_0 = "0x30001a" +dvn_fee_lib_router_1 = "0x3465342143a" +price_feed_router_0 = "0x65DD71A" +price_feed_router_1 = "0x65DD71AB" +price_feed_module_0 = "0x65DD71" +worker_common = "0x3999" +executor_fee_lib_0 = "0x3000" +dvn_fee_lib_0 = "0x3000a" +dvn = "0x22314321" + +[dev-dependencies] +simple_msglib = { local = "../msglib/libs/simple_msglib" } +uln_302 = { local = "../msglib/libs/uln_302" } +worker_common = { local = "../worker_peripherals/worker_common" } +executor_fee_lib_0 = { local = "../worker_peripherals/fee_libs/executor_fee_lib_0" } +dvn_fee_lib_0 = { local = "../worker_peripherals/fee_libs/dvn_fee_lib_0" } + +[dependencies] +msglib_types = { local = "../msglib/msglib_types" } +router_node_0 = { local = "../msglib/routers/router_node_0" } +endpoint_v2_common = { local = "../endpoint_v2_common" } +price_feed_module_0 = { local = "../worker_peripherals/price_feed_modules/price_feed_module_0" } \ No newline at end of file diff --git a/packages/layerzero-v2/aptos/contracts/endpoint_v2/sources/admin.move b/packages/layerzero-v2/aptos/contracts/endpoint_v2/sources/admin.move new file mode 100644 index 00000000..89972ffe --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/endpoint_v2/sources/admin.move @@ -0,0 +1,61 @@ +/// This module isolates functions related to Endpoint administration +module endpoint_v2::admin { + use std::signer::address_of; + + use endpoint_v2::msglib_manager; + + /// Register new message library + /// A library must be registered to be called by this endpoint. Once registered, it cannot be unregistered. The + /// library must be connected to the router node before it can be registered + public entry fun register_library(account: &signer, lib: address) { + assert_admin(address_of(move account)); + msglib_manager::register_library(lib); + } + + /// Sets the default sending message library for the given destination EID + public entry fun set_default_send_library(account: &signer, dst_eid: u32, lib: address) { + assert_admin(address_of(move account)); + msglib_manager::set_default_send_library(dst_eid, lib); + } + + /// Set the default receive message library for the given source EID + public entry fun set_default_receive_library( + account: &signer, + src_eid: u32, + lib: address, + grace_period: u64, + ) { + assert_admin(address_of(move account)); + msglib_manager::set_default_receive_library(src_eid, lib, grace_period); + } + + /// Updates the default receive library timeout for the given source EID + /// The provided expiry is in a specific block number. The fallback library will be disabled once the block height + /// equals this block number + public entry fun set_default_receive_library_timeout( + account: &signer, + src_eid: u32, + fallback_lib: address, + expiry: u64, + ) { + assert_admin(address_of(move account)); + msglib_manager::set_default_receive_library_timeout(src_eid, fallback_lib, expiry); + } + + // ==================================================== Helpers =================================================== + + /// Internal function to assert that the caller is the endpoint admin + inline fun assert_admin(admin: address) { + assert!(admin == @layerzero_admin, EUNAUTHORIZED); + } + + #[test_only] + /// Test-only function to initialize the endpoint and EID + public fun initialize_endpoint_for_test() { + endpoint_v2::store::init_module_for_test(); + } + + // ================================================== Error Codes ================================================= + + const EUNAUTHORIZED: u64 = 1; +} diff --git a/packages/layerzero-v2/aptos/contracts/endpoint_v2/sources/endpoint.move b/packages/layerzero-v2/aptos/contracts/endpoint_v2/sources/endpoint.move new file mode 100644 index 00000000..7813b710 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/endpoint_v2/sources/endpoint.move @@ -0,0 +1,597 @@ +/// Primary entrypoint for all OApps to interact with the LayerZero protocol +module endpoint_v2::endpoint { + use std::fungible_asset::FungibleAsset; + use std::option::Option; + use std::signer::address_of; + use std::string::String; + + use endpoint_v2::channels; + use endpoint_v2::messaging_composer; + use endpoint_v2::messaging_receipt::MessagingReceipt; + use endpoint_v2::msglib_manager; + use endpoint_v2::registration; + use endpoint_v2::timeout; + use endpoint_v2_common::bytes32::{Self, Bytes32, from_bytes32}; + use endpoint_v2_common::contract_identity::{CallRef, get_call_ref_caller}; + use endpoint_v2_common::packet_raw; + use endpoint_v2_common::packet_v1_codec::compute_payload; + + #[test_only] + friend endpoint_v2::endpoint_tests; + + // ============================================== OApp Administration ============================================= + + struct EndpointOAppConfigTarget {} + + /// Initialize the OApp while registering the lz_receive module name + /// + /// The lz_receive module selection is permanent and cannot be changed + /// When delivering a message, the offchain entity will call `::::lz_receive()` + public fun register_oapp(oapp: &signer, lz_receive_module: String) { + let oapp_address = address_of(move oapp); + registration::register_oapp(oapp_address, lz_receive_module); + } + + /// Registers the Composer with the lz_compose function + /// + /// This function must be called before the Composer can start to receive composer messages + /// The lz_compose module selection is permanent and cannot be changed + public fun register_composer(composer: &signer, lz_compose_module: String) { + let composer_address = address_of(move composer); + registration::register_composer(composer_address, lz_compose_module); + } + + /// Register a Receive pathway for the given remote EID + /// + /// This function must be called before any message can be verified + /// After a message is verified, the lz_receive() function call is permissionless; therefore, this provides + /// important protection against the verification of messages from an EID, whose security configuration has not been + /// accepted by the OApp. + /// + /// Once a pathway is registered, receives can happen under either the default configuration or the configuration + /// provided by the OApp. + public fun register_receive_pathway(call_ref: &CallRef, src_eid: u32, sender: Bytes32) { + let oapp = get_oapp_caller(call_ref); + channels::register_receive_pathway(oapp, src_eid, sender) + } + + /// Set the send library for a given EID + /// + /// Setting to a @0x0 msglib address will unset the send library and cause the OApp to use the default instead + public fun set_send_library(call_ref: &CallRef, remote_eid: u32, msglib: address) { + let oapp = get_oapp_caller(call_ref); + msglib_manager::set_send_library(oapp, remote_eid, msglib); + } + + /// Set the receiving message library for the given remote EID + /// + /// Setting to a @0x0 msglib address will unset the receive library and cause the OApp to use the default instead + /// The `grace_period` is the maximum number of blocks that the OApp will continue to accept a message from the + /// prior receive library. + /// + /// A grace period cannot be set when switching to or from the unset / default setting. + /// + /// To emulate a grace period when switching from default, first set the receive library explicitly to the default + /// with no grace period, then set to the desired new library with a grace period. + /// To emulate a grace period when switching to default, first set the receive library explicitly to the default + /// library address with a grace period, then when the grace period expires, unset the receive library (set to @0x0) + /// without a grace period + public fun set_receive_library( + call_ref: &CallRef, + remote_eid: u32, + msglib: address, + grace_period: u64, + ) { + let oapp = get_oapp_caller(call_ref); + msglib_manager::set_receive_library(oapp, remote_eid, msglib, grace_period); + } + + /// Update the timeout settings for the receive library + /// + /// The `expiry` is the block number at which the OApp will no longer confirm message using the provided + /// fallback receive library. + /// + /// The timeout cannot be set to fall back to the default receive library (@0x0) or when the current receive library + /// is unset (uses the default). This will also revert if the expiry is not set to a future block number. + public fun set_receive_library_timeout( + call_ref: &CallRef, + remote_eid: u32, + msglib: address, + expiry: u64, + ) { + let oapp = get_oapp_caller(call_ref); + msglib_manager::set_receive_library_timeout(oapp, remote_eid, msglib, expiry); + } + + /// Set the serialized message Library configuration using the serialized format for a specific message library + public fun set_config( + call_ref: &CallRef, + msglib: address, + eid: u32, + config_type: u32, + config: vector, + ) { + let oapp = get_oapp_caller(call_ref); + msglib_manager::set_config(oapp, msglib, eid, config_type, config); + } + + // ==================================================== Sending =================================================== + + struct EndpointV2SendingTarget {} + + /// Gets a quote for a message given all the send parameters + /// + /// The response is the (Native fee, ZRO fee). If `pay_in_zro` is true, than whatever portion of the fee that + /// can be paid in ZRO wil be returned as the ZRO fee, and the remaining portion will be returned as the Native fee. + /// If `pay_in_zro` is false, the entire fee will be returned as the Native fee, and 0 will be returned as the ZRO + /// fee. + public fun quote( + sender: address, + dst_eid: u32, + receiver: Bytes32, + message: vector, + options: vector, + pay_in_zro: bool, + ): (u64, u64) { + channels::quote(sender, dst_eid, receiver, message, options, pay_in_zro) + } + + #[view] + /// View function to get a quote for a message given all the send parameters + public fun quote_view( + sender: address, + dst_eid: u32, + receiver: vector, + message: vector, + options: vector, + pay_in_zro: bool, + ): (u64, u64) { + channels::quote(sender, dst_eid, bytes32::to_bytes32(receiver), message, options, pay_in_zro) + } + + /// Send a message through the LayerZero protocol + /// + /// This will be passed to the Message Library to calculate the fee of the message and emit messages that trigger + /// offchain entities to deliver and verify the message. If `zro_token` is not option::none, whatever component of + /// the fee that can be paid in ZRO will be paid in ZRO. + /// If the amounts provided are insuffient, the transaction will revert + public fun send( + call_ref: &CallRef, + dst_eid: u32, + receiver: Bytes32, + message: vector, + options: vector, + native_token: &mut FungibleAsset, + zro_token: &mut Option, + ): MessagingReceipt { + let sender = get_oapp_caller(call_ref); + channels::send( + sender, + dst_eid, + receiver, + message, + options, + native_token, + zro_token, + ) + } + + // ============================================= Clearing "Hot Potato" ============================================ + + /// Non-droppable guid that requires a call to `clear()` or `clear_compose()` to delete + struct WrappedGuid { guid: Bytes32 } + + struct WrappedGuidAndIndex { guid: Bytes32, index: u16 } + + /// Wrap a guid (lz_receive), so that it can only be unwrapped (and disposed of) by the endpoint + public fun wrap_guid(guid: Bytes32): WrappedGuid { WrappedGuid { guid } } + + /// Wrap a guid and index (lz_compose), so that it can only be unwrapped (and disposed of) by the endpoint + public fun wrap_guid_and_index(guid: Bytes32, index: u16): WrappedGuidAndIndex { + WrappedGuidAndIndex { guid, index } + } + + /// Get the guid from a WrappedGuid without destorying the WrappedGuid + public fun get_guid_from_wrapped(wrapped: &WrappedGuid): Bytes32 { wrapped.guid } + + /// Get the guid and index from a WrappedGuidAndIndex without destorying the WrappedGuidAndIndex + public fun get_guid_and_index_from_wrapped(wrapped: &WrappedGuidAndIndex): (Bytes32, u16) { + (wrapped.guid, wrapped.index) + } + + /// Endpoint-only call to destroy a WrappedGuid and provide its value; only called by `clear()` + fun unwrap_guid(wrapped_guid: WrappedGuid): Bytes32 { + let WrappedGuid { guid } = wrapped_guid; + guid + } + + /// Endpoint-only call to destroy a WrappedGuidAndIndex and provide its values; only called by `clear_compose()` + fun unwrap_guid_and_index(wrapped_guid_and_index: WrappedGuidAndIndex): (Bytes32, u16) { + let WrappedGuidAndIndex { guid, index } = wrapped_guid_and_index; + (guid, index) + } + + // ==================================================== Receiving ================================================= + + struct EndpointV2ReceivingTarget {} + + /// Validate and Clear a message received through the LZ Receive protocol + /// + /// This should be called by the lz_receive function on the OApp. + /// In this "pull model", the off-chain entity will call the OApp's lz_receive function. The lz_receive function + /// must call `clear()` to validate and clear the message. If `clear()` is not called, lz_receive may receive + /// messages but will not be able to ensure the validity of the messages. + public fun clear( + call_ref: &CallRef, + src_eid: u32, + sender: Bytes32, + nonce: u64, + guid: WrappedGuid, + message: vector, + ) { + let oapp = get_oapp_caller(call_ref); + channels::clear_payload( + oapp, + src_eid, + sender, + nonce, + compute_payload(unwrap_guid(guid), message) + ); + } + + /// Function to alert that the lz_receive function has reverted + /// + /// This may be used to create a record that the offchain entity has made an unsuccessful attempt to deliver a + /// message. + public entry fun lz_receive_alert( + executor: &signer, + receiver: address, + src_eid: u32, + sender: vector, + nonce: u64, + guid: vector, + gas: u64, + value: u64, + message: vector, + extra_data: vector, + reason: String, + ) { + channels::lz_receive_alert( + receiver, + address_of(executor), + src_eid, + bytes32::to_bytes32(sender), + nonce, + bytes32::to_bytes32(guid), + gas, + value, + message, + extra_data, + reason, + ); + } + + /// This prevents verification of the next message in the sequence. An example use case is to skip + /// a message when precrime throws an alert. The nonce must be provided to avoid the possibility of skipping the + /// unintended nonce. + public fun skip( + call_ref: &CallRef, + src_eid: u32, + sender: Bytes32, + nonce: u64, + ) { + let oapp = get_oapp_caller(call_ref); + channels::skip(oapp, src_eid, sender, nonce); + } + + /// This prevents delivery and any possible reverification of an already verified message + public fun burn( + call_ref: &CallRef, + src_eid: u32, + sender: Bytes32, + nonce: u64, + payload_hash: Bytes32, + ) { + let oapp = get_oapp_caller(call_ref); + channels::burn(oapp, src_eid, sender, nonce, payload_hash); + } + + /// This maintains a packet's status as verified but prevents delivery until the packet is verified again + /// A non-verified nonce can be nilified by passing 0x00*32 as the payload hashs + public fun nilify( + call_ref: &CallRef, + src_eid: u32, + sender: Bytes32, + nonce: u64, + payload_hash: Bytes32, + ) { + let oapp = get_oapp_caller(call_ref); + channels::nilify(oapp, src_eid, sender, nonce, payload_hash); + } + + + // ==================================================== Compose =================================================== + + struct EndpointV2SendComposeTarget {} + + struct EndpointV2ComposerTarget {} + + /// Initiates a compose message + /// + /// This should be called by the lz_receive function on the OApp when it receives a message that indicates that + /// a compose is required. The composer will then call the target Composer's lz_compose with the message function to + /// complete the compose + public fun send_compose( + call_ref: &CallRef, + to: address, + index: u16, + guid: Bytes32, + message: vector, + ) { + let caller = get_call_ref_caller(call_ref); + messaging_composer::send_compose(caller, to, index, guid, message); + } + + /// Internal function to get the address of the OApp caller, and assert that the caller is a registered OApp and + /// the endpoint is the intended recipient + public(friend) fun get_oapp_caller(call_ref: &CallRef): address { + let caller = get_call_ref_caller(call_ref); + assert!(registration::is_registered_oapp(caller), EUNREGISTERED); + caller + } + + // =================================================== Composer =================================================== + + /// Function to clear the compose message + /// This should be called from the Composer's lz_compose function. This will both check the validity of the message + /// and clear the message from the Composer's compose queue. If this is not called, the Composer will not be able to + /// ensure the validity of received messages + public fun clear_compose( + call_ref: &CallRef, + from: address, + guid_and_index: WrappedGuidAndIndex, + message: vector, + ) { + let composer = get_compose_caller(call_ref); + let (guid, index) = unwrap_guid_and_index(guid_and_index); + messaging_composer::clear_compose(from, composer, guid, index, message); + } + + /// Function to alert that the lz_compose function has reverted + /// + /// This may be used to create a record that the offchain composer has made an unsuccessful attempt to deliver a + /// compose message + public entry fun lz_compose_alert( + executor: &signer, + from: address, + to: address, + guid: vector, + index: u16, + gas: u64, + value: u64, + message: vector, + extra_data: vector, + reason: String, + ) { + messaging_composer::lz_compose_alert( + address_of(move executor), + from, + to, + bytes32::to_bytes32(guid), + index, + gas, + value, + message, + extra_data, + reason, + ); + } + + /// Internal function to get the address of the Composer caller, and assert that the caller is a registered Composer + /// and that the endpoint is the intended recipient + public(friend) fun get_compose_caller(call_ref: &CallRef): address { + let caller = get_call_ref_caller(call_ref); + assert!(registration::is_registered_composer(caller), EUNREGISTERED); + caller + } + + // ================================================= Verification ================================================= + + + /// This verifies a message by storing the payload hash on the receive channel + /// + /// Once a message has been verified, it can be permissionlessly delivered. This will revert if the message library + /// has not committed the verification. + public entry fun verify( + receive_lib: address, + packet_header: vector, + payload_hash: vector, + extra_data: vector, + ) { + let packet_header = packet_raw::bytes_to_raw_packet(packet_header); + channels::verify(receive_lib, packet_header, bytes32::to_bytes32(payload_hash), extra_data); + } + + /// Confirms that a message has been verified by the message library, and is ready to be verified (confirmed) on the + /// endpoint. + /// + /// This can be called prior to calling `verify` to ensure that the message is ready to be verified. + /// This is also exposed as a view function below called `verifiable_view()` + public fun verifiable( + src_eid: u32, + sender: Bytes32, + nonce: u64, + receiver: address, + ): bool { + channels::verifiable(receiver, src_eid, sender, nonce) + } + + /// Confirms that a message has a payload hash (has been verified). + /// + /// This generally suggests that the message can be delievered, with the exeption of the case where the message has + /// been nilified + public fun has_payload_hash( + src_eid: u32, + sender: Bytes32, + nonce: u64, + receiver: address, + ): bool { + channels::has_payload_hash(receiver, src_eid, sender, nonce) + } + + /// Gets the payload hash for a given message + public fun payload_hash(receiver: address, src_eid: u32, sender: Bytes32, nonce: u64): Bytes32 { + channels::get_payload_hash(receiver, src_eid, sender, nonce) + } + + // ===================================================== View ===================================================== + + // Get the serialized message Library configuration using the serialized format for a specific message library + #[view] + public fun get_config(oapp: address, msglib: address, eid: u32, config_type: u32): vector { + msglib_manager::get_config(oapp, msglib, eid, config_type) + } + + #[view] + public fun get_quote( + sender: address, + dst_eid: u32, + receiver: vector, + message: vector, + options: vector, + pay_in_zro: bool, + ): (u64, u64) { + quote(sender, dst_eid, bytes32::to_bytes32(receiver), message, options, pay_in_zro) + } + + #[view] + public fun get_payload_hash(receiver: address, src_eid: u32, sender: vector, nonce: u64): vector { + from_bytes32(payload_hash(receiver, src_eid, bytes32::to_bytes32(sender), nonce)) + } + + #[view] + /// This is called by offchain entities to know what function to call on the OApp for lz_receive + public fun get_lz_receive_module(oapp: address): String { + registration::lz_receive_module(oapp) + } + + #[view] + /// This is called by offchain entities to know what function to call on the Composer for lz_compose + public fun get_lz_compose_module(composer: address): String { + registration::lz_compose_module(composer) + } + + #[view] + /// Gets the effective receive library for the given source EID, returns both the library and a flag indicating if + /// this is a fallback to the default (meaning the library is not configured for the oapp) + public fun get_effective_receive_library(oapp_address: address, src_eid: u32): (address, bool) { + msglib_manager::get_effective_receive_library(oapp_address, src_eid) + } + + #[view] + public fun get_effective_send_library(oapp_address: address, dst_eid: u32): (address, bool) { + msglib_manager::get_effective_send_library(oapp_address, dst_eid) + } + + #[view] + public fun get_outbound_nonce(sender: address, dst_eid: u32, receiver: vector): u64 { + channels::outbound_nonce(sender, dst_eid, bytes32::to_bytes32(receiver)) + } + + #[view] + public fun get_lazy_inbound_nonce(receiver: address, src_eid: u32, sender: vector): u64 { + channels::lazy_inbound_nonce(receiver, src_eid, bytes32::to_bytes32(sender)) + } + + #[view] + public fun get_inbound_nonce(receiver: address, src_eid: u32, sender: vector): u64 { + channels::inbound_nonce(receiver, src_eid, bytes32::to_bytes32(sender)) + } + + #[view] + public fun is_registered_library(msglib: address): bool { + msglib_manager::is_registered_library(msglib) + } + + #[view] + public fun is_valid_receive_library_for_oapp(oapp: address, src_eid: u32, msglib: address): bool { + msglib_manager::is_valid_receive_library_for_oapp(oapp, src_eid, msglib) + } + + #[view] + public fun has_payload_hash_view(src_eid: u32, sender: vector, nonce: u64, receiver: address): bool { + has_payload_hash(src_eid, bytes32::to_bytes32(sender), nonce, receiver) + } + + #[view] + /// Get the hash of the composed message with the given parameters with the following special case values: + /// NOT SENT: 0x00(*32) + /// SENT: hash value + /// RECEIVED / CLEARED: 0xff(*32) + public fun get_compose_message_hash(from: address, to: address, guid: vector, index: u16): vector { + from_bytes32( + messaging_composer::get_compose_message_hash(from, to, bytes32::to_bytes32(guid), index) + ) + } + + #[view] + public fun get_next_guid(oapp: address, dst_eid: u32, receiver: vector): vector { + from_bytes32(channels::next_guid(oapp, dst_eid, bytes32::to_bytes32(receiver))) + } + + #[view] + public fun initializable(src_eid: u32, sender: vector, receiver: address): bool { + channels::receive_pathway_registered(receiver, src_eid, bytes32::to_bytes32(sender)) + } + + #[view] + public fun verifiable_view( + src_eid: u32, + sender: vector, + nonce: u64, + receiver: address, + ): bool { + verifiable(src_eid, bytes32::to_bytes32(sender), nonce, receiver) + } + + #[view] + /// Get the default send library for the given destination EID + public fun get_default_send_library(remote_eid: u32): address { + msglib_manager::get_default_send_library(remote_eid) + } + + #[view] + /// Get the default receive library for the given source EID + public fun get_default_receive_library(remote_eid: u32): address { + msglib_manager::get_default_receive_library(remote_eid) + } + + #[view] + public fun get_registered_libraries(start_index: u64, max_entries: u64): vector
{ + msglib_manager::get_registered_libraries(start_index, max_entries) + } + + #[view] + public fun get_receive_library_timeout(oapp: address, remote_eid: u32): (u64, address) { + timeout::unpack_timeout(msglib_manager::get_receive_library_timeout(oapp, remote_eid)) + } + + #[view] + public fun get_default_receive_library_timeout(remote_eid: u32): (u64, address) { + timeout::unpack_timeout(msglib_manager::get_default_receive_library_timeout(remote_eid)) + } + + #[view] + public fun is_registered_oapp(oapp: address): bool { + registration::is_registered_oapp(oapp) + } + + #[view] + public fun is_registered_composer(composer: address): bool { + registration::is_registered_composer(composer) + } + + // ================================================== Error Codes ================================================= + + const EUNREGISTERED: u64 = 1; +} diff --git a/packages/layerzero-v2/aptos/contracts/endpoint_v2/sources/internal/channels.move b/packages/layerzero-v2/aptos/contracts/endpoint_v2/sources/internal/channels.move new file mode 100644 index 00000000..27a554ec --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/endpoint_v2/sources/internal/channels.move @@ -0,0 +1,610 @@ +module endpoint_v2::channels { + use std::event::emit; + use std::fungible_asset::{Self, FungibleAsset, Metadata}; + use std::object::{address_to_object, Object}; + use std::option::{Self, Option}; + use std::string::String; + + use endpoint_v2::messaging_receipt::{Self, MessagingReceipt}; + use endpoint_v2::msglib_manager; + use endpoint_v2::store; + use endpoint_v2_common::bytes32::{Self, Bytes32, from_bytes32, to_bytes32}; + use endpoint_v2_common::guid::compute_guid; + use endpoint_v2_common::packet_raw::{get_packet_bytes, RawPacket}; + use endpoint_v2_common::send_packet; + use endpoint_v2_common::send_packet::{new_send_packet, SendPacket}; + use endpoint_v2_common::universal_config; + use router_node_0::router_node as msglib_router; + + friend endpoint_v2::endpoint; + + #[test_only] + friend endpoint_v2::channels_tests; + + const NIL_PAYLOAD_HASH: vector = x"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; + const EMPTY_PAYLOAD_HASH: vector = x"0000000000000000000000000000000000000000000000000000000000000000"; + + // ==================================================== Helpers =================================================== + + inline fun is_native_token(native_metadata: Object): bool { + native_metadata == address_to_object(@native_token_metadata_address) + } + + // ==================================================== Sending =================================================== + + /// Generate the next GUID for the given destination EID + public(friend) fun next_guid(sender: address, dst_eid: u32, receiver: Bytes32): Bytes32 { + let next_nonce = store::outbound_nonce(sender, dst_eid, receiver) + 1; + compute_guid(next_nonce, universal_config::eid(), bytes32::from_address(sender), dst_eid, receiver) + } + + public(friend) fun outbound_nonce(sender: address, dst_eid: u32, receiver: Bytes32): u64 { + store::outbound_nonce(sender, dst_eid, receiver) + } + + /// Send a message to the given destination EID + public(friend) fun send( + sender: address, + dst_eid: u32, + receiver: Bytes32, + message: vector, + options: vector, + native_token: &mut FungibleAsset, + zro_token: &mut Option, + ): MessagingReceipt { + send_internal( + sender, + dst_eid, + receiver, + message, + options, + native_token, + zro_token, + // Message Library Router Send Function + |send_lib, send_packet| msglib_router::send( + send_lib, + &store::make_dynamic_call_ref(send_lib, b"send"), + send_packet, + options, + native_token, + zro_token, + ), + ) + } + + /// This send function receives the send message library call as a lambda function + /// + /// @param sender: The address of the sender + /// @param dst_eid: The destination EID + /// @param receiver: The receiver's address + /// @param message: The message to be sent + /// @param options: The options to be sent with the message + /// @param native_token: The native token to be sent with the message + /// @param zro_token: The optional ZRO token to be sent with the message + /// @param msglib_send: The lambda function that calls the send library + /// |send_lib, send_packet| (native_fee, zro_fee, encoded_packet): The lambda function that calls the send + /// library + public(friend) inline fun send_internal( + sender: address, + dst_eid: u32, + receiver: Bytes32, + message: vector, + options: vector, + native_token: &mut FungibleAsset, + zro_token: &mut Option, + msglib_send: |address, SendPacket| (u64, u64, RawPacket), + ): MessagingReceipt { + let (send_lib, _) = msglib_manager::get_effective_send_library(sender, dst_eid); + + // increment the outbound nonce and get the next nonce + let nonce = store::increment_outbound_nonce(sender, dst_eid, receiver); + + // check token metadatas + assert!(is_native_token(fungible_asset::asset_metadata(native_token)), err_EUNSUPPORTED_PAYMENT()); + if (option::is_some(zro_token)) { + assert!(universal_config::is_zro(option::borrow(zro_token)), err_EUNSUPPORTED_PAYMENT()); + }; + + let packet = new_send_packet( + nonce, + universal_config::eid(), + bytes32::from_address(sender), + dst_eid, + receiver, + message, + ); + let guid = send_packet::get_guid(&packet); + + let (native_fee, zro_fee, encoded_packet) = msglib_send(send_lib, packet); + emit_packet_sent_event(encoded_packet, options, send_lib); + + messaging_receipt::new_messaging_receipt( + guid, + nonce, + native_fee, + zro_fee, + ) + } + + /// This function provides the cost of sending a message in native and ZRO tokens (if pay_in_zro = true) without + /// sending the message + public(friend) fun quote( + sender: address, + dst_eid: u32, + receiver: Bytes32, + message: vector, + options: vector, + pay_in_zro: bool, + ): (u64, u64) { + quote_internal( + sender, + dst_eid, + receiver, + message, + // Message Library Router Quote Function + |send_lib, send_packet| msglib_router::quote( + send_lib, + send_packet, + options, + pay_in_zro, + ), + ) + } + + /// This is the internal portion of the quote function with the message library call provided as a lambda function + /// + /// @param sender: The address of the sender + /// @param dst_eid: The destination EID + /// @param receiver: The receiver's address + /// @param message: The message to be sent + /// @param msglib_quote: The lambda function that calls the send library's quote function + /// |send_lib, send_packet| (native_fee, zro_fee) + public(friend) inline fun quote_internal( + sender: address, + dst_eid: u32, + receiver: Bytes32, + message: vector, + msglib_quote: |address, SendPacket| (u64, u64) + ): (u64, u64) { + let nonce = store::outbound_nonce(sender, dst_eid, receiver) + 1; + let (send_lib, _) = msglib_manager::get_effective_send_library(sender, dst_eid); + let packet = new_send_packet( + nonce, + universal_config::eid(), + bytes32::from_address(sender), + dst_eid, + receiver, + message, + ); + msglib_quote(send_lib, packet) + } + + // =================================================== Receiving ================================================== + + /// Enable verification (committing) of messages from a given src_eid and sender to the receiver OApp + public(friend) fun register_receive_pathway(receiver: address, src_eid: u32, sender: Bytes32) { + store::register_receive_pathway(receiver, src_eid, sender); + emit(ReceivePathwayRegistered { receiver, src_eid, sender: from_bytes32(sender) }); + } + + /// Check if the receive pathway is registered + public(friend) fun receive_pathway_registered(receiver: address, src_eid: u32, sender: Bytes32): bool { + store::receive_pathway_registered(receiver, src_eid, sender) + } + + /// Sets the payload hash for the given nonce + public(friend) fun inbound(receiver: address, src_eid: u32, sender: Bytes32, nonce: u64, payload_hash: Bytes32) { + assert!(!bytes32::is_zero(&payload_hash), EEMPTY_PAYLOAD_HASH); + store::set_payload_hash(receiver, src_eid, sender, nonce, payload_hash); + } + + // Get the lazy inbound nonce for the given sender + public(friend) fun lazy_inbound_nonce(receiver: address, src_eid: u32, sender: Bytes32): u64 { + store::lazy_inbound_nonce(receiver, src_eid, sender) + } + + /// Returns the max index of the gapless sequence of verfied msg nonces starting at the lazy_inbound_nonce. This is + /// initially 0, and the first nonce is always 1 + public(friend) fun inbound_nonce(receiver: address, src_eid: u32, sender: Bytes32): u64 { + let i = store::lazy_inbound_nonce(receiver, src_eid, sender); + loop { + i = i + 1; + if (!store::has_payload_hash(receiver, src_eid, sender, i)) { + return i - 1 + } + } + } + + /// Skip the nonce. The nonce must be the next nonce in the sequence + public(friend) fun skip(receiver: address, src_eid: u32, sender: Bytes32, nonce_to_skip: u64) { + assert!(nonce_to_skip == inbound_nonce(receiver, src_eid, sender) + 1, EINVALID_NONCE); + store::set_lazy_inbound_nonce(receiver, src_eid, sender, nonce_to_skip); + emit(InboundNonceSkipped { src_eid, sender: from_bytes32(sender), receiver, nonce: nonce_to_skip }) + } + + /// Marks a packet as verified but disables execution until it is re-verified + /// A non-verified nonce can be nilified by passing EMPTY_PAYLOAD_HASH for payload_hash + public(friend) fun nilify(receiver: address, src_eid: u32, sender: Bytes32, nonce: u64, payload_hash: Bytes32) { + let has_payload_hash = store::has_payload_hash(receiver, src_eid, sender, nonce); + assert!(nonce > store::lazy_inbound_nonce(receiver, src_eid, sender) || has_payload_hash, EINVALID_NONCE); + + let stored_payload_hash = if (has_payload_hash) { + store::get_payload_hash(receiver, src_eid, sender, nonce) + } else { + to_bytes32(EMPTY_PAYLOAD_HASH) + }; + + assert!(payload_hash == stored_payload_hash, EPAYLOAD_HASH_DOES_NOT_MATCH); + + // Set the hash to 0xff*32 to indicate that the packet is nilified + store::set_payload_hash(receiver, src_eid, sender, nonce, bytes32::to_bytes32(NIL_PAYLOAD_HASH)); + emit( + PacketNilified { + src_eid, sender: from_bytes32(sender), receiver, nonce, payload_hash: from_bytes32(payload_hash), + } + ) + } + + /// Marks a nonce as unexecutable and unverifiable. The nonce can never be re-verified or executed. + /// Only packets less than or equal to the current lazy inbound nonce can be burnt. + public(friend) fun burn(receiver: address, src_eid: u32, sender: Bytes32, nonce: u64, payload_hash: Bytes32) { + assert!(nonce <= store::lazy_inbound_nonce(receiver, src_eid, sender), EINVALID_NONCE); + + // check that the hash provided matches the stored hash and clear + let inbound_payload_hash = store::remove_payload_hash(receiver, src_eid, sender, nonce); + assert!(inbound_payload_hash == payload_hash, EPAYLOAD_HASH_DOES_NOT_MATCH); + + emit( + PacketBurnt { + src_eid, sender: from_bytes32(sender), receiver, nonce, payload_hash: from_bytes32(payload_hash), + } + ) + } + + /// Clear the stored message and increment the lazy_inbound_nonce to the provided nonce + /// This is used in the receive pathway as the final step because it validates the hash as well + /// If a lot of messages are queued, the messages can be cleared with a smaller step size to prevent OOG + public(friend) fun clear_payload( + receiver: address, + src_eid: u32, + sender: Bytes32, + nonce: u64, + payload: vector, + ) { + let current_nonce = store::lazy_inbound_nonce(receiver, src_eid, sender); + + // Make sure that all hashes are present up to the clear-to point, clear them, and update the lazy nonce + if (nonce > current_nonce) { + current_nonce = current_nonce + 1; + while (current_nonce <= nonce) { + assert!(store::has_payload_hash(receiver, src_eid, sender, current_nonce), ENO_PAYLOAD_HASH); + current_nonce = current_nonce + 1 + }; + store::set_lazy_inbound_nonce(receiver, src_eid, sender, nonce); + }; + + // Check if the payload hash matches the provided payload + let actual_hash = bytes32::keccak256(payload); + assert!(store::has_payload_hash(receiver, src_eid, sender, nonce), ENO_PAYLOAD_HASH); + + // Clear and check the payload hash + let expected_hash = store::remove_payload_hash(receiver, src_eid, sender, nonce); + assert!(actual_hash == expected_hash, EPAYLOAD_HASH_DOES_NOT_MATCH); + + emit(PacketDelivered { src_eid, sender: from_bytes32(sender), nonce, receiver }); + } + + /// Checks if the payload hash exists for the given nonce + public(friend) fun has_payload_hash(receiver: address, src_eid: u32, sender: Bytes32, nonce: u64): bool { + store::has_payload_hash(receiver, src_eid, sender, nonce) + } + + /// Get the payload hash for a given nonce + public(friend) fun get_payload_hash(receiver: address, src_eid: u32, sender: Bytes32, nonce: u64): Bytes32 { + store::get_payload_hash(receiver, src_eid, sender, nonce) + } + + /// Check if a message is verifiable (committable) + public(friend) fun verifiable(receiver: address, src_eid: u32, sender: Bytes32, nonce: u64): bool { + if (!store::receive_pathway_registered(receiver, src_eid, sender)) { + return false + }; + let lazy_inbound_nonce = store::lazy_inbound_nonce(receiver, src_eid, sender); + nonce > lazy_inbound_nonce || has_payload_hash(receiver, src_eid, sender, nonce) + } + + /// Verify (commit) a message + public(friend) fun verify( + receive_lib: address, + packet_header: RawPacket, + payload_hash: Bytes32, + extra_data: vector, + ) { + let call_ref = &store::make_dynamic_call_ref(receive_lib, b"commit_verification"); + let (receiver, src_eid, sender, nonce) = msglib_router::commit_verification( + receive_lib, + call_ref, + packet_header, + payload_hash, + extra_data, + ); + + verify_internal( + receive_lib, + payload_hash, + receiver, + src_eid, + sender, + nonce, + ); + } + + /// This function is the internal portion of verifying a message. This receives the decoded packet header + /// information (that verify() gets from the message library) as an input + /// + /// @param receive_lib: The address of the receive library + /// @param packet_header: The packet header + /// @param payload_hash: The hash of the payload + /// @params receiver: The address of the receiver + /// @params src_eid: The source EID + /// @params sender: The sender's address + /// @params nonce: The nonce of the message + public(friend) inline fun verify_internal( + receive_lib: address, + payload_hash: Bytes32, + receiver: address, + src_eid: u32, + sender: Bytes32, + nonce: u64, + ) { + // This is the same assertion as initializable() in EVM + store::assert_receive_pathway_registered( + receiver, + src_eid, + sender, + ); + assert!( + msglib_manager::is_valid_receive_library_for_oapp(receiver, src_eid, receive_lib), + err_EINVALID_MSGLIB() + ); + assert!(verifiable(receiver, src_eid, sender, nonce), err_ENOT_VERIFIABLE()); + + // insert the message into the messaging channel + inbound(receiver, src_eid, sender, nonce, payload_hash); + emit_packet_verified_event(src_eid, from_bytes32(sender), nonce, receiver, from_bytes32(payload_hash)); + } + + + /// Send an alert if lz_receive reverts + public(friend) fun lz_receive_alert( + receiver: address, + executor: address, + src_eid: u32, + sender: Bytes32, + nonce: u64, + guid: Bytes32, + gas: u64, + value: u64, + message: vector, + extra_data: vector, + reason: String, + ) { + emit(LzReceiveAlert { + receiver, + executor, + src_eid, + sender: from_bytes32(sender), + nonce, + guid: from_bytes32(guid), + gas, + value, + message, + extra_data, + reason, + }); + } + + // ==================================================== Events ==================================================== + + #[event] + struct ReceivePathwayRegistered has store, drop { + receiver: address, + src_eid: u32, + sender: vector, + } + + #[event] + struct PacketBurnt has store, drop { + src_eid: u32, + sender: vector, + receiver: address, + nonce: u64, + payload_hash: vector, + } + + #[event] + struct PacketNilified has store, drop { + src_eid: u32, + sender: vector, + receiver: address, + nonce: u64, + payload_hash: vector, + } + + #[event] + struct InboundNonceSkipped has store, drop { + src_eid: u32, + sender: vector, + receiver: address, + nonce: u64, + } + + #[event] + struct PacketDelivered has drop, store { + src_eid: u32, + sender: vector, + nonce: u64, + receiver: address, + } + + #[event] + struct PacketSent has drop, store { + encoded_packet: vector, + options: vector, + send_library: address, + } + + #[event] + struct PacketVerified has drop, store { + src_eid: u32, + sender: vector, + nonce: u64, + receiver: address, + payload_hash: vector, + } + + + #[event] + struct LzReceiveAlert has drop, store { + receiver: address, + executor: address, + src_eid: u32, + sender: vector, + nonce: u64, + guid: vector, + gas: u64, + value: u64, + message: vector, + extra_data: vector, + reason: String, + } + + // These 2 emit_ functions are needed so that inline functions can still emit when called from the test module + + public(friend) fun emit_packet_sent_event(encoded_packet: RawPacket, options: vector, send_library: address) { + emit(PacketSent { encoded_packet: get_packet_bytes(encoded_packet), options, send_library }); + } + + public(friend) fun emit_packet_verified_event( + src_eid: u32, + sender: vector, + nonce: u64, + receiver: address, + payload_hash: vector, + ) { + emit(PacketVerified { src_eid, sender, nonce, receiver, payload_hash }); + } + + #[test_only] + public fun receive_pathway_registered_event( + receiver: address, + src_eid: u32, + sender: vector, + ): ReceivePathwayRegistered { + ReceivePathwayRegistered { receiver, src_eid, sender } + } + + #[test_only] + public fun packet_burnt_event( + src_eid: u32, + sender: vector, + receiver: address, + nonce: u64, + payload_hash: vector, + ): PacketBurnt { + PacketBurnt { src_eid, sender, receiver, nonce, payload_hash } + } + + #[test_only] + public fun packet_nilified_event( + src_eid: u32, + sender: vector, + receiver: address, + nonce: u64, + payload_hash: vector, + ): PacketNilified { + PacketNilified { src_eid, sender, receiver, nonce, payload_hash } + } + + #[test_only] + public fun inbound_nonce_skipped_event( + src_eid: u32, + sender: vector, + receiver: address, + nonce: u64, + ): InboundNonceSkipped { + InboundNonceSkipped { src_eid, sender, receiver, nonce } + } + + #[test_only] + public fun packet_delivered_event( + src_eid: u32, + sender: vector, + nonce: u64, + receiver: address, + ): PacketDelivered { + PacketDelivered { src_eid, sender, nonce, receiver } + } + + #[test_only] + public fun packet_sent_event(encoded_packet: RawPacket, options: vector, send_library: address): PacketSent { + PacketSent { encoded_packet: get_packet_bytes(encoded_packet), options, send_library } + } + + #[test_only] + public fun packet_verified_event( + src_eid: u32, + sender: vector, + nonce: u64, + receiver: address, + payload_hash: vector, + ): PacketVerified { + PacketVerified { src_eid, sender, nonce, receiver, payload_hash } + } + + #[test_only] + public fun lz_receive_alert_event( + receiver: address, + executor: address, + src_eid: u32, + sender: vector, + nonce: u64, + guid: vector, + gas: u64, + value: u64, + message: vector, + extra_data: vector, + reason: String, + ): LzReceiveAlert { + LzReceiveAlert { + receiver, + executor, + src_eid, + sender, + nonce, + guid, + gas, + value, + message, + extra_data, + reason, + } + } + + // ================================================== Error Codes ================================================= + + const EEMPTY_PAYLOAD_HASH: u64 = 1; + const EINVALID_MSGLIB: u64 = 2; + const EINVALID_NONCE: u64 = 3; + const ENOT_VERIFIABLE: u64 = 4; + const ENO_PAYLOAD_HASH: u64 = 5; + const EPAYLOAD_HASH_DOES_NOT_MATCH: u64 = 6; + const EUNSUPPORTED_PAYMENT: u64 = 7; + + // These wrapper error functions are needed to support testing inline functions in a different testing module + public(friend) fun err_EUNSUPPORTED_PAYMENT(): u64 { EUNSUPPORTED_PAYMENT } + + public(friend) fun err_EINVALID_MSGLIB(): u64 { EINVALID_MSGLIB } + + public(friend) fun err_ENOT_VERIFIABLE(): u64 { ENOT_VERIFIABLE } +} \ No newline at end of file diff --git a/packages/layerzero-v2/aptos/contracts/endpoint_v2/sources/internal/messaging_composer.move b/packages/layerzero-v2/aptos/contracts/endpoint_v2/sources/internal/messaging_composer.move new file mode 100644 index 00000000..826294fa --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/endpoint_v2/sources/internal/messaging_composer.move @@ -0,0 +1,140 @@ +/// The messaging composer is responsible for managing composed messages +module endpoint_v2::messaging_composer { + use std::event::emit; + use std::string::String; + + use endpoint_v2::store; + use endpoint_v2_common::bytes32::{Self, Bytes32, from_bytes32}; + + friend endpoint_v2::endpoint; + + #[test_only] + friend endpoint_v2::messaging_composer_tests; + + // ================================================ Core Functions ================================================ + + /// Called by the OApp when receiving a message to trigger the sending of a composed message + /// This preps the message for delivery by lz_compose + public(friend) fun send_compose(oapp: address, to: address, index: u16, guid: Bytes32, message: vector) { + assert!(!store::has_compose_message_hash(oapp, to, guid, index), ECOMPOSE_ALREADY_SENT); + let message_hash = bytes32::keccak256(message); + store::set_compose_message_hash(oapp, to, guid, index, message_hash); + emit(ComposeSent { from: oapp, to, guid: from_bytes32(guid), index, message }); + } + + /// This clears the composed message from the store + /// This should be triggered by lz_compose on the OApp, and it confirms that the message was delivered + public(friend) fun clear_compose(from: address, to: address, guid: Bytes32, index: u16, message: vector) { + // Make sure the message hash matches the expected hash + assert!(store::has_compose_message_hash(from, to, guid, index), ECOMPOSE_NOT_FOUND); + let expected_hash = store::get_compose_message_hash(from, to, guid, index); + let actual_hash = bytes32::keccak256(message); + assert!(expected_hash == actual_hash, ECOMPOSE_ALREADY_CLEARED); + + // Set the message 0xff(*32) to prevent sending the message again + store::set_compose_message_hash(from, to, guid, index, bytes32::ff_bytes32()); + emit(ComposeDelivered { from, to, guid: from_bytes32(guid), index }); + } + + /// Get the hash of the composed message with the given parameters + /// NOT SENT: 0x00(*32) + /// SENT: hash value + /// RECEIVED / CLEARED: 0xff(*32) + public(friend) fun get_compose_message_hash(from: address, to: address, guid: Bytes32, index: u16): Bytes32 { + if (store::has_compose_message_hash(from, to, guid, index)) { + store::get_compose_message_hash(from, to, guid, index) + } else { + bytes32::zero_bytes32() + } + } + + /// Emit an event that indicates that the off-chain executor failed to deliver the compose message + public(friend) fun lz_compose_alert( + executor: address, + from: address, + to: address, + guid: Bytes32, + index: u16, + gas: u64, + value: u64, + message: vector, + extra_data: vector, + reason: String, + ) { + emit(LzComposeAlert { + from, to, executor, guid: from_bytes32(guid), index, gas, value, message, extra_data, reason, + }); + } + + // ==================================================== Events ==================================================== + + #[event] + struct ComposeSent has copy, drop, store { + from: address, + to: address, + guid: vector, + index: u16, + message: vector, + } + + #[event] + struct ComposeDelivered has copy, drop, store { + from: address, + to: address, + guid: vector, + index: u16, + } + + #[event] + struct LzComposeAlert has copy, drop, store { + from: address, + to: address, + executor: address, + guid: vector, + index: u16, + gas: u64, + value: u64, + message: vector, + extra_data: vector, + reason: String, + } + + #[test_only] + public fun compose_sent_event( + from: address, + to: address, + guid: vector, + index: u16, + message: vector, + ): ComposeSent { + ComposeSent { from, to, guid, index, message } + } + + #[test_only] + public fun compose_delivered_event(from: address, to: address, guid: vector, index: u16): ComposeDelivered { + ComposeDelivered { from, to, guid, index } + } + + #[test_only] + public fun lz_compose_alert_event( + from: address, + to: address, + executor: address, + guid: vector, + index: u16, + gas: u64, + value: u64, + message: vector, + extra_data: vector, + reason: String, + ): LzComposeAlert { + LzComposeAlert { from, to, executor, guid, index, gas, value, message, extra_data, reason } + } + + // ================================================== Error Codes ================================================= + + const ECOMPOSE_ALREADY_CLEARED: u64 = 1; + const ECOMPOSE_ALREADY_SENT: u64 = 2; + const ECOMPOSE_NOT_FOUND: u64 = 3; +} + diff --git a/packages/layerzero-v2/aptos/contracts/endpoint_v2/sources/internal/messaging_receipt.move b/packages/layerzero-v2/aptos/contracts/endpoint_v2/sources/internal/messaging_receipt.move new file mode 100644 index 00000000..a99eb82d --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/endpoint_v2/sources/internal/messaging_receipt.move @@ -0,0 +1,74 @@ +/// Messaging Receipt is returned upon endpoint::send and provides proof of the message being sent and fees paid +module endpoint_v2::messaging_receipt { + use endpoint_v2_common::bytes32::Bytes32; + + friend endpoint_v2::channels; + + #[test_only] + friend endpoint_v2::messaging_receipt_tests; + + // For indirect dependency: `channels_tests` calls friend functions via inline functions in `endpoint_v2::channels` + #[test_only] + friend endpoint_v2::channels_tests; + + /// Messaging receipt is returned from endpoint::send and provides proof of the message being sent and fees paid + struct MessagingReceipt has store, drop { + guid: Bytes32, + nonce: u64, + native_fee: u64, + zro_fee: u64, + } + + /// Constructs a new messaging receipt + /// This is a friend-only function so that the Messaging Receipt cannot be forged by a 3rd party + public(friend) fun new_messaging_receipt( + guid: Bytes32, + nonce: u64, + native_fee: u64, + zro_fee: u64, + ): MessagingReceipt { + MessagingReceipt { guid, nonce, native_fee, zro_fee } + } + + #[test_only] + public fun new_messaging_receipt_for_test( + guid: Bytes32, + nonce: u64, + native_fee: u64, + zro_fee: u64, + ): MessagingReceipt { + MessagingReceipt { guid, nonce, native_fee, zro_fee } + } + + /// Get the guid of a MessagingReceipt in the format of a bytes array + public fun get_guid(self: &MessagingReceipt): Bytes32 { + self.guid + } + + /// Gets the nonce of a MessagingReceipt + public fun get_nonce(self: &MessagingReceipt): u64 { + self.nonce + } + + /// Gets the native fee of a MessagingReceipt + public fun get_native_fee(self: &MessagingReceipt): u64 { + self.native_fee + } + + /// Gets the zro fee of a MessagingReceipt + public fun get_zro_fee(self: &MessagingReceipt): u64 { + self.zro_fee + } + + /// Unpacks the fields of a MessagingReceipt + /// @return (guid, nonce, native_fee, zro_fee) + public fun unpack_messaging_receipt(receipt: MessagingReceipt): (Bytes32, u64, u64, u64) { + let MessagingReceipt { + guid, + nonce, + native_fee, + zro_fee, + } = receipt; + (guid, nonce, native_fee, zro_fee) + } +} \ No newline at end of file diff --git a/packages/layerzero-v2/aptos/contracts/endpoint_v2/sources/internal/msglib_manager.move b/packages/layerzero-v2/aptos/contracts/endpoint_v2/sources/internal/msglib_manager.move new file mode 100644 index 00000000..4ec7a2d5 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/endpoint_v2/sources/internal/msglib_manager.move @@ -0,0 +1,469 @@ +/// The message library manager is responsible for managing the message libraries and providing default message library +/// selections for the endpoint. +/// The registration of a message library happens in several steps: +/// 1. The message library is deployed and made available through the message library router. +/// 2. The message library address is registered with the message library manager: this irrevocably enables the use of +/// the message library through this endpoint (but not on any pathways). +/// 3. A default message library is registered for each individual pathway: this irreversibly enables the use of the +/// message library for the given pathway. +/// +/// Once a message library's default is set, the OApp can override the default message library for any pathway through +/// the separate OApp configuration interface. +module endpoint_v2::msglib_manager { + use std::event::emit; + + use endpoint_v2::store; + use endpoint_v2::timeout; + use endpoint_v2::timeout::{new_timeout_from_expiry, Timeout}; + use router_node_0::router_node; + + friend endpoint_v2::admin; + friend endpoint_v2::endpoint; + friend endpoint_v2::channels; + + #[test_only] + friend endpoint_v2::msglib_manager_tests; + #[test_only] + friend endpoint_v2::test_helpers; + #[test_only] + friend endpoint_v2::channels_tests; + + // ==================================================== Helpers =================================================== + + inline fun assert_library_registered(lib: address) { + assert!(store::is_registered_msglib(lib), EUNREGISTERED_MSGLIB); + } + + inline fun is_in_grace_period(src_eid: u32): bool { + store::has_default_receive_library_timeout(src_eid) && + timeout::is_active(&store::get_default_receive_library_timeout(src_eid)) + } + + inline fun get_default_receive_library_timeout_prior_lib(src_eid: u32): address { + timeout::get_library(&store::get_default_receive_library_timeout(src_eid)) + } + + // ================================================ Core Functions ================================================ + + /// Register a new message library + public(friend) fun register_library(lib: address) { + // Make sure it is available to be registered + assert_connected_to_router(lib); + // Make sure it is not already registered + assert!(!store::is_registered_msglib(lib), EALREADY_REGISTERED); + // Add the message library and emit + store::add_msglib(lib); + emit(LibraryRegistered { new_lib: lib }); + } + + /// Asserts that a library is connected to the router and not a placeholder. + inline fun assert_connected_to_router(lib: address) { + // The version call will fail if a message library is not in the router or if it is a placeholder. + router_node::version(lib); + } + + /// Gets a list of all message libraries that are registered + public(friend) fun get_registered_libraries(start_index: u64, max_entries: u64): vector
{ + store::registered_msglibs(start_index, max_entries) + } + + /// Checks if a particular library address is registered + public(friend) fun is_registered_library(lib: address): bool { + store::is_registered_msglib(lib) + } + + public(friend) fun set_config( + oapp: address, + lib: address, + eid: u32, + config_type: u32, + config: vector, + ) { + assert_library_registered(lib); + let to_msglib_call_ref = &store::make_dynamic_call_ref(lib, b"set_config"); + router_node::set_config(lib, to_msglib_call_ref, oapp, eid, config_type, config); + } + + public(friend) fun get_config( + oapp: address, + lib: address, + eid: u32, + config_type: u32, + ): vector { + assert_library_registered(lib); + router_node::get_config(lib, oapp, eid, config_type) + } + + // ============================================ Default Send Libraries ============================================ + + fun assert_msglib_supports_send_eid(lib: address, dst_eid: u32) { + assert!(router_node::is_supported_send_eid(lib, dst_eid), EUNSUPPORTED_DST_EID); + } + + /// Gets the default send library + public(friend) fun get_default_send_library(dst_eid: u32): address { + store::get_default_send_library(dst_eid) + } + + /// Set the default Endpoint default send library for a dest_eid. + /// If there is no default send library set, the dest_eid is deemed to be unsupported. + public(friend) fun set_default_send_library(dst_eid: u32, new_lib: address) { + assert_library_registered(new_lib); + assert_msglib_supports_send_eid(new_lib, dst_eid); + + if (store::has_default_send_library(dst_eid)) { + let old_lib = store::get_default_send_library(dst_eid); + assert!(old_lib != new_lib, EATTEMPTED_TO_SET_CURRENT_LIBRARY); + }; + + store::set_default_send_library(dst_eid, new_lib); + emit(DefaultSendLibrarySet { eid: dst_eid, new_lib }); + } + + // ============================================== Oapp Send Libraries ============================================= + + public(friend) fun set_send_library( + sender: address, + dst_eid: u32, + msglib: address, + ) { + if (msglib == @0x0) { + assert!(store::has_send_library(sender, dst_eid), EOAPP_SEND_LIB_NOT_SET); + store::unset_send_library(sender, dst_eid); + } else { + assert_library_registered(msglib); + assert_msglib_supports_send_eid(msglib, dst_eid); + + if (store::has_send_library(sender, dst_eid)) { + let old_lib = store::get_send_library(sender, dst_eid); + assert!(old_lib != msglib, EATTEMPTED_TO_SET_CURRENT_LIBRARY); + }; + store::set_send_library(sender, dst_eid, msglib); + }; + emit(SendLibrarySet { sender, eid: dst_eid, new_lib: msglib }); + } + + /// Gets the effective send library for the given destination EID, returns both the library and a flag indicating if + /// this is a fallback to the default (meaning the library is not configured for the oapp) + public(friend) fun get_effective_send_library(sender: address, dst_eid: u32): (address, bool) { + if (store::has_send_library(sender, dst_eid)) { + (store::get_send_library(sender, dst_eid), false) + } else { + (store::get_default_send_library(dst_eid), true) + } + } + + + // =========================================== Default Receive Libraries ========================================== + + fun assert_msglib_supports_receive_eid(lib: address, src_eid: u32) { + assert!(router_node::is_supported_receive_eid(lib, src_eid), EUNSUPPORTED_SRC_EID); + } + + /// Set the default receive library. + /// If the grace_period is non-zero, also set the grace message library and expiry + public(friend) fun set_default_receive_library(src_eid: u32, new_lib: address, grace_period: u64) { + assert_library_registered(new_lib); + assert_msglib_supports_receive_eid(new_lib, src_eid); + + if (store::has_default_receive_library(src_eid)) { + let old_lib = store::get_default_receive_library(src_eid); + assert!(old_lib != new_lib, EATTEMPTED_TO_SET_CURRENT_LIBRARY); + }; + + let old_lib = if (store::has_default_receive_library(src_eid)) { + store::get_default_receive_library(src_eid) + } else @0x0; + + // Set the grace period if it is greater than 0 + if (grace_period > 0) { + assert!(old_lib != @0x0, ENO_PRIOR_LIBRARY_FOR_FALLBACK); + let timeout = timeout::new_timeout_from_grace_period(grace_period, old_lib); + store::set_default_receive_library_timeout(src_eid, timeout); + + let expiry = timeout::get_expiry(&timeout); + emit(DefaultReceiveLibraryTimeoutSet { eid: src_eid, old_lib, expiry }); + } else { + if (store::has_default_receive_library_timeout(src_eid)) { + store::unset_default_receive_library_timeout(src_eid); + }; + emit(DefaultReceiveLibraryTimeoutSet { eid: src_eid, old_lib, expiry: 0 }); + }; + store::set_default_receive_library(src_eid, new_lib); + emit(DefaultReceiveLibrarySet { eid: src_eid, new_lib }); + } + + public(friend) fun set_default_receive_library_timeout( + src_eid: u32, + lib: address, + expiry: u64, + ) { + if (expiry == 0) { + store::unset_default_receive_library_timeout(src_eid); + emit(DefaultReceiveLibraryTimeoutSet { eid: src_eid, expiry: 0, old_lib: lib }); + } else { + assert_library_registered(lib); + assert_msglib_supports_receive_eid(lib, src_eid); + let timeout = timeout::new_timeout_from_expiry(expiry, lib); + assert!(timeout::is_active(&timeout), EEXPIRY_IS_IN_PAST); + store::set_default_receive_library_timeout(src_eid, timeout); + emit(DefaultReceiveLibraryTimeoutSet { eid: src_eid, expiry, old_lib: lib }); + }; + } + + /// Asserts that a given receive library is the default receive library for a given source EID. + /// This will also check the grace period (prior library) if active. + public(friend) fun matches_default_receive_library(src_eid: u32, actual_receive_lib: address): bool { + if (actual_receive_lib == store::get_default_receive_library(src_eid)) { return true }; + + // If no match, check the grace period + if (!is_in_grace_period(src_eid)) { return false }; + + get_default_receive_library_timeout_prior_lib(src_eid) == actual_receive_lib + } + + public(friend) fun get_default_receive_library(src_eid: u32): address { + store::get_default_receive_library(src_eid) + } + + /// Get the default receive library timeout or return an empty Timeout if one is not set + public(friend) fun get_default_receive_library_timeout(src_eid: u32): Timeout { + if (store::has_default_receive_library_timeout(src_eid)) { + store::get_default_receive_library_timeout(src_eid) + } else { + new_timeout_from_expiry(0, @0x0) + } + } + + // ================================================= Receive Oapp ================================================= + + + public(friend) fun set_receive_library( + receiver: address, + src_eid: u32, + msglib: address, + grace_period: u64, + ) { + // If MsgLib is @0x0, unset the library + if (msglib == @0x0) { + // Setting a grace period is not supported when unsetting the library to match EVM spec + // This can still be achieved by explicitly setting the OApp to the default library with a grace period + // And then after the grace period, unsetting the library by setting it to @0x0 + assert!(grace_period == 0, ECANNOT_SET_GRACE_PERIOD_ON_RECEIVE_LIBRARY_UNSET); + + assert!(store::has_receive_library(receiver, src_eid), ERECEIVE_LIB_NOT_SET); + let old_lib = store::get_receive_library(receiver, src_eid); + store::unset_receive_library(receiver, src_eid); + emit(ReceiveLibrarySet { receiver, eid: src_eid, new_lib: @0x0 }); + if (store::has_receive_library_timeout(receiver, src_eid)) { + store::unset_receive_library_timeout(receiver, src_eid); + }; + emit(ReceiveLibraryTimeoutSet { receiver, eid: src_eid, old_lib, timeout: 0 }); + return + }; + + // Check if the library is registered and supports the receive EID + assert_library_registered(msglib); + assert_msglib_supports_receive_eid(msglib, src_eid); + + // Make sure we are not setting the same library + if (store::has_receive_library(receiver, src_eid)) { + let old_lib = store::get_receive_library(receiver, src_eid); + assert!(old_lib != msglib, EATTEMPTED_TO_SET_CURRENT_LIBRARY); + }; + + let (old_lib, is_default) = get_effective_receive_library(receiver, src_eid); + + store::set_receive_library(receiver, src_eid, msglib); + emit(ReceiveLibrarySet { receiver, eid: src_eid, new_lib: msglib }); + + if (grace_period == 0) { + // If there is a timeout and grace_period is 0, remove the current timeout + if (store::has_receive_library_timeout(receiver, src_eid)) { + store::unset_receive_library_timeout(receiver, src_eid); + }; + let non_default_old_lib = if (is_default) { @0x0 } else { old_lib }; + emit(ReceiveLibraryTimeoutSet { receiver, eid: src_eid, old_lib: non_default_old_lib, timeout: 0 }); + } else { + // Setting a grace period is not supported when setting the library from the default library + // This can still be achieved by explicitly setting the OApp to the default library, then setting it to the + // desired library with a default + assert!(!is_default, ETIMEOUT_SET_FOR_DEFAULT_RECEIVE_LIBRARY); + + let grace_period = timeout::new_timeout_from_grace_period(grace_period, old_lib); + store::set_receive_library_timeout(receiver, src_eid, grace_period); + let expiry = timeout::get_expiry(&grace_period); + emit(ReceiveLibraryTimeoutSet { receiver, eid: src_eid, old_lib, timeout: expiry }); + } + } + + + public(friend) fun set_receive_library_timeout( + receiver: address, + src_eid: u32, + lib: address, + expiry: u64, + ) { + if (expiry == 0) { + // Abort if there is no timeout to delete + assert!(store::has_receive_library_timeout(receiver, src_eid), ENO_TIMEOUT_TO_DELETE); + let timeout = store::get_receive_library_timeout(receiver, src_eid); + assert!(timeout::is_active(&timeout), ENO_TIMEOUT_TO_DELETE); + store::unset_receive_library_timeout(receiver, src_eid); + emit(ReceiveLibraryTimeoutSet { receiver, eid: src_eid, old_lib: lib, timeout: 0 }); + } else { + assert!(store::has_receive_library(receiver, src_eid), ERECEIVE_LIB_NOT_SET); + assert_msglib_supports_receive_eid(lib, src_eid); + assert_library_registered(lib); + let timeout = timeout::new_timeout_from_expiry(expiry, lib); + assert!(timeout::is_active(&timeout), EEXPIRY_IS_IN_PAST); + store::set_receive_library_timeout(receiver, src_eid, timeout); + emit(ReceiveLibraryTimeoutSet { receiver, eid: src_eid, old_lib: lib, timeout: expiry }); + }; + } + + /// Get the receive library timeout or return an empty timeout if it is not set + public(friend) fun get_receive_library_timeout(receiver: address, src_eid: u32): Timeout { + if (store::has_receive_library_timeout(receiver, src_eid)) { + store::get_receive_library_timeout(receiver, src_eid) + } else { + new_timeout_from_expiry(0, @0x0) + } + } + + /// Gets the effective receive library for the given source EID, returns both the library and a flag indicating if + /// this is a fallback to the default (meaning the library is not configured) + public(friend) fun get_effective_receive_library(receiver: address, src_eid: u32): (address, bool) { + if (store::has_receive_library(receiver, src_eid)) { + (store::get_receive_library(receiver, src_eid), false) + } else { + (store::get_default_receive_library(src_eid), true) + } + } + + /// Check if the provided message library is valid for the given OApp and source EID. + public(friend) fun is_valid_receive_library_for_oapp(receiver: address, src_eid: u32, msglib: address): bool { + if (!is_registered_library(msglib)) { return false }; + + // 1. Check if it is the default library, if it is not already set + if (!store::has_receive_library(receiver, src_eid)) { + return matches_default_receive_library(src_eid, msglib) + }; + + // 2. Check if it is the configured library + if (store::get_receive_library(receiver, src_eid) == msglib) { return true }; + + // 3. If it is in the grace period, check the prior library + if (store::has_receive_library_timeout(receiver, src_eid)) { + let timeout = store::get_receive_library_timeout(receiver, src_eid); + let is_active = timeout::is_active(&timeout); + if (is_active && timeout::get_library(&timeout) == msglib) { return true }; + }; + + // 4. Otherwise, it is invalid + false + } + + // ==================================================== Events ==================================================== + + #[event] + struct LibraryRegistered has drop, copy, store { new_lib: address } + + #[event] + struct DefaultSendLibrarySet has drop, copy, store { eid: u32, new_lib: address } + + #[event] + struct DefaultReceiveLibrarySet has drop, copy, store { + eid: u32, + new_lib: address, + } + + #[event] + struct DefaultReceiveLibraryTimeoutSet has drop, copy, store { + eid: u32, + old_lib: address, + expiry: u64, + } + + #[event] + struct SendLibrarySet has drop, copy, store { + sender: address, + eid: u32, + new_lib: address, + } + + #[event] + struct ReceiveLibrarySet has drop, copy, store { + receiver: address, + eid: u32, + new_lib: address, + } + + #[event] + struct ReceiveLibraryTimeoutSet has drop, copy, store { + receiver: address, + eid: u32, + old_lib: address, + timeout: u64, + } + + #[test_only] + public fun library_registered_event(lib: address): LibraryRegistered { LibraryRegistered { new_lib: lib } } + + #[test_only] + public fun default_send_library_set_event( + eid: u32, + new_lib: address, + ): DefaultSendLibrarySet { DefaultSendLibrarySet { eid, new_lib } } + + #[test_only] + public fun default_receive_library_set_event( + eid: u32, + new_lib: address, + ): DefaultReceiveLibrarySet { DefaultReceiveLibrarySet { eid, new_lib } } + + #[test_only] + public fun default_receive_library_timeout_set_event( + eid: u32, + old_lib: address, + expiry: u64, + ): DefaultReceiveLibraryTimeoutSet { DefaultReceiveLibraryTimeoutSet { eid, old_lib, expiry } } + + #[test_only] + public fun send_library_set_event( + sender: address, + eid: u32, + new_lib: address, + ): SendLibrarySet { SendLibrarySet { sender, eid, new_lib } } + + #[test_only] + public fun receive_library_set_event( + receiver: address, + eid: u32, + new_lib: address, + ): ReceiveLibrarySet { ReceiveLibrarySet { receiver, eid, new_lib } } + + #[test_only] + public fun receive_library_timeout_set_event( + receiver: address, + eid: u32, + old_lib: address, + timeout: u64, + ): ReceiveLibraryTimeoutSet { ReceiveLibraryTimeoutSet { receiver, eid, old_lib, timeout } } + + // ================================================== Error Codes ================================================= + + const EALREADY_REGISTERED: u64 = 1; + const EATTEMPTED_TO_SET_CURRENT_LIBRARY: u64 = 2; + const ECANNOT_SET_GRACE_PERIOD_ON_RECEIVE_LIBRARY_UNSET: u64 = 3; + const EEXPIRY_IS_IN_PAST: u64 = 4; + const ENO_PRIOR_LIBRARY_FOR_FALLBACK: u64 = 5; + const ENO_TIMEOUT_TO_DELETE: u64 = 6; + const EOAPP_SEND_LIB_NOT_SET: u64 = 7; + const ERECEIVE_LIB_NOT_SET: u64 = 8; + const ETIMEOUT_SET_FOR_DEFAULT_RECEIVE_LIBRARY: u64 = 9; + const EUNREGISTERED_MSGLIB: u64 = 10; + const EUNSUPPORTED_DST_EID: u64 = 11; + const EUNSUPPORTED_SRC_EID: u64 = 12; +} + diff --git a/packages/layerzero-v2/aptos/contracts/endpoint_v2/sources/internal/registration.move b/packages/layerzero-v2/aptos/contracts/endpoint_v2/sources/internal/registration.move new file mode 100644 index 00000000..726ecf36 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/endpoint_v2/sources/internal/registration.move @@ -0,0 +1,85 @@ +module endpoint_v2::registration { + use std::event::emit; + use std::string::{Self, String}; + + use endpoint_v2::store; + + friend endpoint_v2::endpoint; + + #[test_only] + friend endpoint_v2::registration_tests; + #[test_only] + friend endpoint_v2::channels_tests; + + /// Initialize the OApp with registering the lz_receive function + /// register_oapp() must also be called before the OApp can receive messages + public(friend) fun register_oapp(oapp: address, lz_receive_module: String) { + assert!(string::length(&lz_receive_module) <= 100, EEXCESSIVE_STRING_LENGTH); + assert!(!store::is_registered_oapp(oapp), EALREADY_REGISTERED); + store::register_oapp(oapp, lz_receive_module); + emit(OAppRegistered { + oapp, + lz_receive_module, + }); + } + + /// Registers the Composer with the lz_compose function + public(friend) fun register_composer(composer: address, lz_compose_module: String) { + assert!(string::length(&lz_compose_module) <= 100, EEXCESSIVE_STRING_LENGTH); + assert!(!store::is_registered_composer(composer), EALREADY_REGISTERED); + store::register_composer(composer, lz_compose_module); + emit(ComposerRegistered { + composer, + lz_compose_module, + }); + } + + /// Checks if the OApp is registered + public(friend) fun is_registered_oapp(oapp: address): bool { + store::is_registered_oapp(oapp) + } + + /// Checks if the Composer is registered + public(friend) fun is_registered_composer(composer: address): bool { + store::is_registered_composer(composer) + } + + /// Returns the lz_receive_module of the OApp + public(friend) fun lz_receive_module(oapp: address): String { + store::lz_receive_module(oapp) + } + + /// Returns the lz_compose_module of the Composer + public(friend) fun lz_compose_module(composer: address): String { + store::lz_compose_module(composer) + } + + // ==================================================== Events ==================================================== + + #[event] + struct OAppRegistered has drop, store { + oapp: address, + lz_receive_module: String, + } + + #[event] + struct ComposerRegistered has drop, store { + composer: address, + lz_compose_module: String, + } + + #[test_only] + public fun oapp_registered_event(oapp: address, lz_receive_module: String): OAppRegistered { + OAppRegistered { oapp, lz_receive_module } + } + + #[test_only] + public fun composer_registered_event(composer: address, lz_compose_module: String): ComposerRegistered { + ComposerRegistered { composer, lz_compose_module } + } + + // ================================================== Error Codes ================================================= + + const EALREADY_REGISTERED: u64 = 1; + const EEXCESSIVE_STRING_LENGTH: u64 = 2; +} diff --git a/packages/layerzero-v2/aptos/contracts/endpoint_v2/sources/internal/store.move b/packages/layerzero-v2/aptos/contracts/endpoint_v2/sources/internal/store.move new file mode 100644 index 00000000..2a01d346 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/endpoint_v2/sources/internal/store.move @@ -0,0 +1,493 @@ +module endpoint_v2::store { + use std::math64::min; + use std::string::{Self, String}; + use std::table::{Self, Table}; + use std::vector; + + use endpoint_v2::timeout::Timeout; + use endpoint_v2_common::bytes32::Bytes32; + use endpoint_v2_common::contract_identity::{Self, ContractSigner, DynamicCallRef}; + + friend endpoint_v2::channels; + friend endpoint_v2::messaging_composer; + friend endpoint_v2::msglib_manager; + friend endpoint_v2::registration; + + #[test_only] + friend endpoint_v2::admin; + #[test_only] + friend endpoint_v2::endpoint_tests; + #[test_only] + friend endpoint_v2::channels_tests; + #[test_only] + friend endpoint_v2::msglib_manager_tests; + #[test_only] + friend endpoint_v2::messaging_composer_tests; + #[test_only] + friend endpoint_v2::registration_tests; + + struct EndpointStore has key { + contract_signer: ContractSigner, + oapps: Table, + composers: Table, + msglibs: Table, + msglib_count: u64, + msglibs_registered: Table, + msglibs_default_send_libs: Table, + msglibs_default_receive_libs: Table, + msglibs_default_receive_lib_timeout_configs: Table, + } + + struct OAppStore has store { + lz_receive_module: String, + channels: Table, + msglibs_send_libs: Table, + msglibs_receive_libs: Table, + msglibs_receive_lib_timeout_configs: Table, + } + + struct ComposerStore has store { + lz_compose_module: String, + compose_message_hashes: Table, + } + + struct Channel has store { + outbound_nonce: u64, + inbound_pathway_registered: bool, + inbound_lazy_nonce: u64, + inbound_hashes: Table, + } + + struct ChannelKey has store, drop, copy { remote_eid: u32, remote_address: Bytes32 } + + struct ComposeKey has store, drop, copy { from: address, guid: Bytes32, index: u16 } + + fun init_module(account: &signer) { + move_to(account, EndpointStore { + contract_signer: contract_identity::create_contract_signer(account), + oapps: table::new(), + composers: table::new(), + msglibs: table::new(), + msglib_count: 0, + msglibs_registered: table::new(), + msglibs_default_send_libs: table::new(), + msglibs_default_receive_libs: table::new(), + msglibs_default_receive_lib_timeout_configs: table::new(), + }) + } + + #[test_only] + public fun init_module_for_test() { + let account = &std::account::create_signer_for_test(@endpoint_v2); + init_module(account); + } + + // ==================================================== General =================================================== + + public(friend) fun make_dynamic_call_ref( + target_contract: address, + authorization: vector, + ): DynamicCallRef acquires EndpointStore { + contract_identity::make_dynamic_call_ref(&store().contract_signer, target_contract, authorization) + } + + // OApp - Helpers + inline fun store(): &EndpointStore { borrow_global(@endpoint_v2) } + + inline fun store_mut(): &mut EndpointStore { borrow_global_mut(@endpoint_v2) } + + inline fun oapps(): &Table { &store().oapps } + + inline fun oapps_mut(): &mut Table { &mut store_mut().oapps } + + inline fun composers(): &Table { &store().composers } + + inline fun composers_mut(): &mut Table { &mut store_mut().composers } + + inline fun oapp_store(oapp: address): &OAppStore { + assert!(table::contains(oapps(), oapp), EUNREGISTERED_OAPP); + table::borrow(oapps(), oapp) + } + + inline fun oapp_store_mut(oapp: address): &mut OAppStore { + assert!(table::contains(oapps(), oapp), EUNREGISTERED_OAPP); + table::borrow_mut(oapps_mut(), oapp) + } + + inline fun composer_store(composer: address): &ComposerStore { + assert!(table::contains(composers(), composer), EUNREGISTERED_COMPOSER); + table::borrow(composers(), composer) + } + + inline fun composer_store_mut(composer: address): &mut ComposerStore { + assert!(table::contains(composers(), composer), EUNREGISTERED_COMPOSER); + table::borrow_mut(composers_mut(), composer) + } + + inline fun has_channel(oapp: address, remote_eid: u32, remote_address: Bytes32): bool { + table::contains(&oapp_store(oapp).channels, ChannelKey { remote_eid, remote_address }) + } + + inline fun create_channel(oapp: address, remote_eid: u32, remote_address: Bytes32) acquires EndpointStore { + table::add(&mut oapp_store_mut(oapp).channels, ChannelKey { remote_eid, remote_address }, Channel { + outbound_nonce: 0, + inbound_pathway_registered: false, + inbound_lazy_nonce: 0, + inbound_hashes: table::new(), + }); + } + + /// Get a reference to an OApp channel. The initialization of a channel should be implicit and transparent to the + /// user. + /// Therefore caller should generally check that the channel exists before calling this function; if it doesn't + /// exist, it should return the value that is consistent with the initial state of a channel + inline fun channel(oapp: address, remote_eid: u32, remote_address: Bytes32): &Channel { + let channel_key = ChannelKey { remote_eid, remote_address }; + table::borrow(&oapp_store(oapp).channels, channel_key) + } + + /// Get a mutable reference to an OApp channel. If the channel doesn't exist, create it + inline fun channel_mut(oapp: address, remote_eid: u32, remote_address: Bytes32): &mut Channel { + let channel_key = ChannelKey { remote_eid, remote_address }; + if (!table::contains(&oapp_store(oapp).channels, channel_key)) { + create_channel(oapp, remote_eid, remote_address); + }; + table::borrow_mut(&mut oapp_store_mut(oapp).channels, channel_key) + } + + // ================================================= OApp General ================================================= + + public(friend) fun register_oapp(oapp: address, lz_receive_module: String) acquires EndpointStore { + assert!(!string::is_empty(&lz_receive_module), EEMPTY_MODULE_NAME); + table::add(oapps_mut(), oapp, OAppStore { + channels: table::new(), + lz_receive_module, + msglibs_send_libs: table::new(), + msglibs_receive_libs: table::new(), + msglibs_receive_lib_timeout_configs: table::new(), + }); + } + + public(friend) fun is_registered_oapp(oapp: address): bool acquires EndpointStore { + table::contains(oapps(), oapp) + } + + public(friend) fun lz_receive_module(receiver: address): String acquires EndpointStore { + oapp_store(receiver).lz_receive_module + } + + // ================================================= OApp Outbound ================================================ + + public(friend) fun outbound_nonce(sender: address, dst_eid: u32, receiver: Bytes32): u64 acquires EndpointStore { + if (has_channel(sender, dst_eid, receiver)) { + channel(sender, dst_eid, receiver).outbound_nonce + } else { + 0 + } + } + + public(friend) fun increment_outbound_nonce( + sender: address, + dst_eid: u32, + receiver: Bytes32, + ): u64 acquires EndpointStore { + let outbound_nonce = &mut channel_mut(sender, dst_eid, receiver).outbound_nonce; + *outbound_nonce = *outbound_nonce + 1; + *outbound_nonce + } + + // ================================================= OApp Inbound ================================================= + + public(friend) fun receive_pathway_registered( + receiver: address, + src_eid: u32, + sender: Bytes32, + ): bool acquires EndpointStore { + if (is_registered_oapp(receiver) && has_channel(receiver, src_eid, sender)) { + channel(receiver, src_eid, sender).inbound_pathway_registered + } else { + false + } + } + + public(friend) fun register_receive_pathway( + receiver: address, + src_eid: u32, + sender: Bytes32, + ) acquires EndpointStore { + if (!has_channel(receiver, src_eid, sender)) { create_channel(receiver, src_eid, sender) }; + let channel = channel_mut(receiver, src_eid, sender); + channel.inbound_pathway_registered = true; + } + + public(friend) fun assert_receive_pathway_registered( + receiver: address, + src_eid: u32, + sender: Bytes32, + ) acquires EndpointStore { + assert!(receive_pathway_registered(receiver, src_eid, sender), EUNREGISTERED_PATHWAY); + } + + public(friend) fun lazy_inbound_nonce( + receiver: address, + src_eid: u32, + sender: Bytes32, + ): u64 acquires EndpointStore { + if (has_channel(receiver, src_eid, sender)) { + channel(receiver, src_eid, sender).inbound_lazy_nonce + } else { + 0 + } + } + + public(friend) fun set_lazy_inbound_nonce( + receiver: address, + src_eid: u32, + sender: Bytes32, + nonce: u64, + ) acquires EndpointStore { + channel_mut(receiver, src_eid, sender).inbound_lazy_nonce = nonce; + } + + public(friend) fun has_payload_hash( + receiver: address, + src_eid: u32, + sender: Bytes32, + nonce: u64, + ): bool acquires EndpointStore { + if (has_channel(receiver, src_eid, sender)) { + table::contains(&channel(receiver, src_eid, sender).inbound_hashes, nonce) + } else { + false + } + } + + public(friend) fun get_payload_hash( + receiver: address, + src_eid: u32, + sender: Bytes32, + nonce: u64, + ): Bytes32 acquires EndpointStore { + assert!(has_channel(receiver, src_eid, sender), EUNREGISTERED_PATHWAY); + let hashes = &channel(receiver, src_eid, sender).inbound_hashes; + assert!(table::contains(hashes, nonce), ENO_PAYLOAD_HASH); + *table::borrow(hashes, nonce) + } + + public(friend) fun set_payload_hash( + receiver: address, + src_eid: u32, + sender: Bytes32, + nonce: u64, + hash: Bytes32, + ) acquires EndpointStore { + table::upsert(&mut channel_mut(receiver, src_eid, sender).inbound_hashes, nonce, hash); + } + + public(friend) fun remove_payload_hash( + receiver: address, + src_eid: u32, + sender: Bytes32, + nonce: u64, + ): Bytes32 acquires EndpointStore { + let hashes = &mut channel_mut(receiver, src_eid, sender).inbound_hashes; + assert!(table::contains(hashes, nonce), ENO_PAYLOAD_HASH); + table::remove(hashes, nonce) + } + + // ==================================================== Compose =================================================== + + public(friend) fun register_composer(composer: address, lz_compose_module: String) acquires EndpointStore { + assert!(!string::is_empty(&lz_compose_module), EEMPTY_MODULE_NAME); + table::add(composers_mut(), composer, ComposerStore { + lz_compose_module, + compose_message_hashes: table::new(), + }); + } + + public(friend) fun is_registered_composer(composer: address): bool acquires EndpointStore { + table::contains(composers(), composer) + } + + public(friend) fun lz_compose_module(composer: address): String acquires EndpointStore { + composer_store(composer).lz_compose_module + } + + public(friend) fun has_compose_message_hash( + from: address, + to: address, + guid: Bytes32, + index: u16, + ): bool acquires EndpointStore { + let compose_key = ComposeKey { from, guid, index }; + table::contains(&composer_store(to).compose_message_hashes, compose_key) + } + + public(friend) fun get_compose_message_hash( + from: address, + to: address, + guid: Bytes32, + index: u16, + ): Bytes32 acquires EndpointStore { + let compose_key = ComposeKey { from, guid, index }; + *table::borrow(&composer_store(to).compose_message_hashes, compose_key) + } + + public(friend) fun set_compose_message_hash( + from: address, + to: address, + guid: Bytes32, + index: u16, + hash: Bytes32, + ) acquires EndpointStore { + let compose_key = ComposeKey { from, guid, index }; + table::upsert(&mut composer_store_mut(to).compose_message_hashes, compose_key, hash); + } + + // =============================================== Message Libraries ============================================== + + public(friend) fun registered_msglibs(start: u64, max_count: u64): vector
acquires EndpointStore { + let msglibs = &store().msglibs; + let end = min(start + max_count, store().msglib_count); + if (start >= end) { return vector[] }; + + let result = vector[]; + for (i in start..end) { + let lib = *table::borrow(msglibs, i); + vector::push_back(&mut result, lib); + }; + result + } + + public(friend) fun add_msglib(lib: address) acquires EndpointStore { + let count = store().msglib_count; + table::add(&mut store_mut().msglibs, count, lib); + store_mut().msglib_count = count + 1; + + table::add(&mut store_mut().msglibs_registered, lib, true); + } + + public(friend) fun is_registered_msglib(lib: address): bool acquires EndpointStore { + table::contains(&store().msglibs_registered, lib) + } + + public(friend) fun has_default_send_library(dst_eid: u32): bool acquires EndpointStore { + table::contains(&store().msglibs_default_send_libs, dst_eid) + } + + public(friend) fun get_default_send_library(dst_eid: u32): address acquires EndpointStore { + let default_send_libs = &store().msglibs_default_send_libs; + assert!(table::contains(default_send_libs, dst_eid), EUNSUPPORTED_DST_EID); + *table::borrow(default_send_libs, dst_eid) + } + + public(friend) fun set_default_send_library(dst_eid: u32, lib: address) acquires EndpointStore { + table::upsert(&mut store_mut().msglibs_default_send_libs, dst_eid, lib); + } + + public(friend) fun has_default_receive_library(src_eid: u32): bool acquires EndpointStore { + table::contains(&store().msglibs_default_receive_libs, src_eid) + } + + public(friend) fun get_default_receive_library(src_eid: u32): address acquires EndpointStore { + let default_receive_libs = &store().msglibs_default_receive_libs; + assert!(table::contains(default_receive_libs, src_eid), EUNSUPPORTED_SRC_EID); + *table::borrow(default_receive_libs, src_eid) + } + + public(friend) fun set_default_receive_library(src_eid: u32, lib: address) acquires EndpointStore { + table::upsert(&mut store_mut().msglibs_default_receive_libs, src_eid, lib); + } + + public(friend) fun has_default_receive_library_timeout(src_eid: u32): bool acquires EndpointStore { + table::contains(&store().msglibs_default_receive_lib_timeout_configs, src_eid) + } + + public(friend) fun get_default_receive_library_timeout( + src_eid: u32, + ): Timeout acquires EndpointStore { + *table::borrow(&store().msglibs_default_receive_lib_timeout_configs, src_eid) + } + + public(friend) fun set_default_receive_library_timeout( + src_eid: u32, + config: Timeout, + ) acquires EndpointStore { + table::upsert(&mut store_mut().msglibs_default_receive_lib_timeout_configs, src_eid, config); + } + + public(friend) fun unset_default_receive_library_timeout( + src_eid: u32, + ): Timeout acquires EndpointStore { + table::remove(&mut store_mut().msglibs_default_receive_lib_timeout_configs, src_eid) + } + + public(friend) fun has_send_library(sender: address, dst_eid: u32): bool acquires EndpointStore { + table::contains(&oapp_store(sender).msglibs_send_libs, dst_eid) + } + + public(friend) fun get_send_library(sender: address, dst_eid: u32): address acquires EndpointStore { + *table::borrow(&oapp_store(sender).msglibs_send_libs, dst_eid) + } + + public(friend) fun set_send_library(sender: address, dst_eid: u32, lib: address) acquires EndpointStore { + table::upsert(&mut oapp_store_mut(sender).msglibs_send_libs, dst_eid, lib); + } + + public(friend) fun unset_send_library(sender: address, dst_eid: u32): address acquires EndpointStore { + table::remove(&mut oapp_store_mut(sender).msglibs_send_libs, dst_eid) + } + + public(friend) fun has_receive_library(receiver: address, src_eid: u32): bool acquires EndpointStore { + table::contains(&oapp_store(receiver).msglibs_receive_libs, src_eid) + } + + public(friend) fun get_receive_library(receiver: address, src_eid: u32): address acquires EndpointStore { + *table::borrow(&oapp_store(receiver).msglibs_receive_libs, src_eid) + } + + public(friend) fun set_receive_library( + receiver: address, + src_eid: u32, + lib: address, + ) acquires EndpointStore { + table::upsert(&mut oapp_store_mut(receiver).msglibs_receive_libs, src_eid, lib); + } + + public(friend) fun unset_receive_library(receiver: address, src_eid: u32): address acquires EndpointStore { + table::remove(&mut oapp_store_mut(receiver).msglibs_receive_libs, src_eid) + } + + public(friend) fun has_receive_library_timeout(receiver: address, src_eid: u32): bool acquires EndpointStore { + table::contains(&oapp_store(receiver).msglibs_receive_lib_timeout_configs, src_eid) + } + + public(friend) fun get_receive_library_timeout(receiver: address, src_eid: u32): Timeout acquires EndpointStore { + *table::borrow(&oapp_store(receiver).msglibs_receive_lib_timeout_configs, src_eid) + } + + public(friend) fun set_receive_library_timeout( + receiver: address, + src_eid: u32, + config: Timeout, + ) acquires EndpointStore { + table::upsert(&mut oapp_store_mut(receiver).msglibs_receive_lib_timeout_configs, src_eid, config); + } + + public(friend) fun unset_receive_library_timeout( + receiver: address, + src_eid: u32, + ): Timeout acquires EndpointStore { + table::remove(&mut oapp_store_mut(receiver).msglibs_receive_lib_timeout_configs, src_eid) + } + + // ================================================== Error Codes ================================================= + + const EEMPTY_MODULE_NAME: u64 = 1; + const ENO_PAYLOAD_HASH: u64 = 2; + const EUNREGISTERED_OAPP: u64 = 3; + const EUNREGISTERED_COMPOSER: u64 = 4; + const EUNREGISTERED_PATHWAY: u64 = 5; + const EUNSUPPORTED_DST_EID: u64 = 6; + const EUNSUPPORTED_SRC_EID: u64 = 7; +} diff --git a/packages/layerzero-v2/aptos/contracts/endpoint_v2/sources/internal/timeout.move b/packages/layerzero-v2/aptos/contracts/endpoint_v2/sources/internal/timeout.move new file mode 100644 index 00000000..c4001784 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/endpoint_v2/sources/internal/timeout.move @@ -0,0 +1,51 @@ +/// The Timeout configuration is used when migrating the Receive message library, and it defines the window of blocks +/// that the prior library will be allowed to receive messages. When the grace period expires, messages from the prior +/// library will be rejected. +module endpoint_v2::timeout { + use std::block::get_current_block_height; + + friend endpoint_v2::msglib_manager; + + friend endpoint_v2::endpoint; + + #[test_only] + friend endpoint_v2::msglib_manager_tests; + + struct Timeout has store, drop, copy { + // The block number at which the grace period expires + expiry: u64, + // The address of the fallback library + lib: address, + } + + /// Create a new timeout configuration + public(friend) fun new_timeout_from_grace_period(grace_period_in_blocks: u64, lib: address): Timeout { + let expiry = get_current_block_height() + grace_period_in_blocks; + Timeout { expiry, lib } + } + + /// Create a new timeout configuration using an expiry block number + public(friend) fun new_timeout_from_expiry(expiry: u64, lib: address): Timeout { + Timeout { expiry, lib } + } + + public(friend) fun unpack_timeout(timeout: Timeout): (u64, address) { + (timeout.expiry, timeout.lib) + } + + /// Check if the timeout is active + /// A timeout is active so long as the current block height is less than the expiry block number + public(friend) fun is_active(self: &Timeout): bool { + self.expiry > get_current_block_height() + } + + /// Get the address of the fallback library + public(friend) fun get_library(self: &Timeout): address { + self.lib + } + + /// Get the expiry block number + public(friend) fun get_expiry(self: &Timeout): u64 { + self.expiry + } +} \ No newline at end of file diff --git a/packages/layerzero-v2/aptos/contracts/endpoint_v2/tests/endpoint_tests.move b/packages/layerzero-v2/aptos/contracts/endpoint_v2/tests/endpoint_tests.move new file mode 100644 index 00000000..74e2bc77 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/endpoint_v2/tests/endpoint_tests.move @@ -0,0 +1,137 @@ +#[test_only] +module endpoint_v2::endpoint_tests { + use std::account::create_signer_for_test; + use std::fungible_asset::FungibleAsset; + use std::option; + use std::signer::address_of; + use std::string; + + use endpoint_v2::admin; + use endpoint_v2::channels; + use endpoint_v2::endpoint::{ + Self, + EndpointOAppConfigTarget, + quote, + register_composer, + register_oapp, send, + }; + use endpoint_v2::test_helpers; + use endpoint_v2_common::bytes32; + use endpoint_v2_common::contract_identity::make_call_ref_for_test; + use endpoint_v2_common::guid::compute_guid; + use endpoint_v2_common::native_token_test_helpers::{burn_token_for_test, mint_native_token_for_test}; + use endpoint_v2_common::packet_v1_codec::new_packet_v1; + use endpoint_v2_common::universal_config; + + #[test] + fun test_quote() { + let oapp = &create_signer_for_test(@1234); + let local_eid = 1u32; + endpoint_v2::test_helpers::setup_layerzero_for_test(@simple_msglib, local_eid, local_eid); + register_oapp(oapp, string::utf8(b"test")); + + let sender = std::signer::address_of(oapp); + let receiver = bytes32::from_address(sender); + let message = vector[1, 2, 3, 4]; + let options = vector[]; + let (native_fee, zro_fee) = quote(sender, local_eid, receiver, message, options, false); + assert!(native_fee == 0, 0); + assert!(zro_fee == 0, 1); + } + + #[test] + fun test_send() { + let oapp = &create_signer_for_test(@1234); + // account setup + let sender = std::signer::address_of(oapp); + + let local_eid = 1u32; + test_helpers::setup_layerzero_for_test(@simple_msglib, local_eid, local_eid); + register_oapp(oapp, string::utf8(b"test")); + + let receiver = bytes32::from_address(sender); + let message = vector[1, 2, 3, 4]; + let options = vector[]; + + let (native_fee, _) = quote(sender, local_eid, receiver, message, options, false); + + let payment_native = mint_native_token_for_test(native_fee); + let payment_zro = option::none(); + send( + &make_call_ref_for_test(sender), + local_eid, + receiver, + message, + options, + &mut payment_native, + &mut payment_zro, + ); + burn_token_for_test(payment_native); + + // check that the packet was emitted + let guid = compute_guid(1, local_eid, bytes32::from_address(sender), local_eid, receiver); + let packet = new_packet_v1( + universal_config::eid(), + bytes32::from_address(sender), + local_eid, + receiver, + 1, + guid, + message, + ); + let expected_event = channels::packet_sent_event(packet, options, @simple_msglib); + assert!(std::event::was_event_emitted(&expected_event), 0); + + option::destroy_none(payment_zro); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::endpoint::EUNREGISTERED)] + fun test_get_oapp_caller_fails_if_not_registered() { + test_helpers::setup_layerzero_for_test(@simple_msglib, 100, 100); + let oapp = &create_signer_for_test(@1234); + // registering composer should not count as registering oapp + register_composer(oapp, string::utf8(b"test")); + // Must provide type since get_oapp_caller is generic + let call_ref = &make_call_ref_for_test(address_of(oapp)); + endpoint_v2::endpoint::get_oapp_caller(call_ref); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::endpoint::EUNREGISTERED)] + fun test_get_compose_caller_fails_if_not_registered() { + test_helpers::setup_layerzero_for_test(@simple_msglib, 100, 100); + let composer = &create_signer_for_test(@1234); + // registering oapp should not count as registering composer + register_oapp(composer, string::utf8(b"test")); + let call_ref = &make_call_ref_for_test(address_of(composer)); + // Must provide type since get_oapp_caller is generic + endpoint_v2::endpoint::get_compose_caller(call_ref); + } + + #[test] + fun get_default_receive_library_timeout_should_return_none_if_not_set() { + test_helpers::setup_layerzero_for_test(@simple_msglib, 100, 100); + let (timeout, library) = endpoint::get_default_receive_library_timeout(1); + assert!(library == @0x0, 0); + assert!(timeout == 0, 1); + } + + #[test] + fun get_default_receive_library_timeout_should_return_the_timeout_if_set() { + let std = &std::account::create_account_for_test(@std); + std::block::initialize_for_test(std, 1_000_000); + std::reconfiguration::initialize_for_test(std); + + test_helpers::setup_layerzero_for_test(@simple_msglib, 100, 100); + admin::set_default_receive_library_timeout( + &create_signer_for_test(@layerzero_admin), + 12, + @simple_msglib, + 10000 + ); + let (timeout, library) = endpoint::get_default_receive_library_timeout(12); + assert!(library == @simple_msglib, 0); + assert!(timeout == 10000, 1); + } +} diff --git a/packages/layerzero-v2/aptos/contracts/endpoint_v2/tests/internal/channels_tests.move b/packages/layerzero-v2/aptos/contracts/endpoint_v2/tests/internal/channels_tests.move new file mode 100644 index 00000000..7ebceb12 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/endpoint_v2/tests/internal/channels_tests.move @@ -0,0 +1,992 @@ +#[test_only] +module endpoint_v2::channels_tests { + use std::account::create_signer_for_test; + use std::event::{emitted_events, was_event_emitted}; + use std::fungible_asset::FungibleAsset; + use std::option::{Self, destroy_none}; + use std::string; + use std::vector; + + use endpoint_v2::channels; + use endpoint_v2::channels::{ + burn, + clear_payload, + get_payload_hash, + has_payload_hash, + inbound, + inbound_nonce, + inbound_nonce_skipped_event, + nilify, + packet_burnt_event, + packet_nilified_event, + packet_verified_event, + PacketVerified, + send_internal, + skip, + verify, + verify_internal, + }; + use endpoint_v2::endpoint::register_oapp; + use endpoint_v2::messaging_receipt; + use endpoint_v2::msglib_manager; + use endpoint_v2::registration; + use endpoint_v2::store; + use endpoint_v2_common::bytes32; + use endpoint_v2_common::bytes32::{from_bytes32, to_bytes32}; + use endpoint_v2_common::guid; + use endpoint_v2_common::guid::compute_guid; + use endpoint_v2_common::native_token_test_helpers::{burn_token_for_test, mint_native_token_for_test}; + use endpoint_v2_common::packet_raw; + use endpoint_v2_common::packet_v1_codec; + use endpoint_v2_common::send_packet::unpack_send_packet; + use endpoint_v2_common::universal_config; + + #[test] + fun test_send_internal() { + let dst_eid = 102; + let oapp = @123; + universal_config::init_module_for_test(104); + store::init_module_for_test(); + let oapp_signer = &create_signer_for_test(oapp); + register_oapp(oapp_signer, string::utf8(b"receiver")); + + // register the default send library + store::set_default_send_library(dst_eid, @11114444); + + let packet = packet_raw::bytes_to_raw_packet(b"123456"); + let native_token = mint_native_token_for_test(100); + let zro_token = option::none(); + + let called = false; + assert!(!called, 0); // needed to avoid unused variable warning + + let receiver = bytes32::from_address(@0x1234); + let expected_guid = guid::compute_guid( + 1, + 104, + bytes32::from_address(oapp), + 102, + receiver, + ); + + let receipt = send_internal( + oapp, + dst_eid, + receiver, + b"payload", + b"options", + &mut native_token, + &mut zro_token, + |msglib, send_packet| { + called = true; + + // called correct message library + assert!(msglib == @11114444, 1); + + // called with correct packet + let (nonce, src_eid, sender, dst_eid, receiver, guid, message) = unpack_send_packet(send_packet); + assert!(nonce == 1, 2); + assert!(src_eid == 104, 3); + assert!(sender == bytes32::from_address(oapp), 4); + assert!(dst_eid == 102, 5); + assert!(receiver == receiver, 6); + assert!(guid == expected_guid, 7); + assert!(message == b"payload", 8); + + (200, 300, packet) + } + ); + + let (guid, nonce, native_fee, zro_fee) = messaging_receipt::unpack_messaging_receipt(receipt); + assert!(guid == expected_guid, 9); + assert!(nonce == 1, 10); + assert!(native_fee == 200, 11); + assert!(zro_fee == 300, 12); + + assert!(called, 1); + called = false; + assert!(!called, 1); + + // Try again (nonce = 2) with a OApp configured send library (instead of the default) + store::set_send_library(oapp, dst_eid, @22223333); + let receipt = send_internal( + oapp, + dst_eid, + receiver, + b"payload", + b"options", + &mut native_token, + &mut zro_token, + |msglib, _send_packet| { + called = true; + // called the msglib matches what is configured for the oapp + assert!(msglib == @22223333, 1); + (2000, 3000, packet) + } + ); + assert!(called, 1); + + let (guid, nonce, native_fee, zro_fee) = messaging_receipt::unpack_messaging_receipt(receipt); + + // nonce should increase to 2 + assert!(nonce == 2, 10); + assert!(native_fee == 2000, 11); + assert!(zro_fee == 3000, 12); + + let expected_guid = guid::compute_guid( + 2, + 104, + bytes32::from_address(oapp), + 102, + receiver, + ); + assert!(guid == expected_guid, 13); + + burn_token_for_test(native_token); + destroy_none(zro_token); + } + + #[test] + fun test_quote_internal() { + let dst_eid = 102; + let oapp = @123; + + universal_config::init_module_for_test(104); + store::init_module_for_test(); + let oapp_signer = &create_signer_for_test(oapp); + + register_oapp(oapp_signer, string::utf8(b"receiver")); + + // register the default send library + store::set_default_send_library(dst_eid, @11114444); + + let called = false; + assert!(!called, 0); // needed to avoid unused variable warning + + let (native_fee, zro_fee) = channels::quote_internal( + oapp, + dst_eid, + bytes32::from_address(@0x1234), + b"payload", + |msglib, send_packet| { + called = true; + assert!(msglib == @11114444, 1); + let (nonce, src_eid, sender, dst_eid, receiver, guid, message) = unpack_send_packet(send_packet); + assert!(nonce == 1, 2); + assert!(src_eid == 104, 3); + assert!(sender == bytes32::from_address(@123), 4); + assert!(dst_eid == 102, 5); + assert!(receiver == bytes32::from_address(@0x1234), 6); + assert!(message == b"payload", 7); + + let expected_guid = compute_guid( + 1, + 104, + bytes32::from_address(oapp), + dst_eid, + bytes32::from_address(@0x1234), + ); + + assert!(guid == expected_guid, 8); + (200, 300) + } + ); + assert!(called, 1); + assert!(native_fee == 200, 2); + assert!(zro_fee == 300, 3); + + // call send() to increment nonce + let native_token = mint_native_token_for_test(100); + let zro_token = option::none(); + send_internal( + oapp, + dst_eid, + bytes32::from_address(@0x1234), + b"payload", + b"options", + &mut native_token, + &mut zro_token, + |msglib, _send_packet| { + called = true; + // called the msglib matches what is configured for the oapp + assert!(msglib == @11114444, 1); + let packet = packet_raw::bytes_to_raw_packet(b"123456"); + (2000, 3000, packet) + } + ); + + burn_token_for_test(native_token); + destroy_none(zro_token); + + assert!(called, 0); + called = false; + assert!(!called, 0); + + // call again with higher nonce state and a OApp configured send library + store::set_send_library(oapp, dst_eid, @22223333); + let (native_fee, zro_fee) = channels::quote_internal( + oapp, + dst_eid, + bytes32::from_address(@0x1234), + b"payload", + |msglib, send_packet| { + called = true; + assert!(msglib == @22223333, 1); + let (nonce, src_eid, sender, dst_eid, receiver, guid, message) = unpack_send_packet(send_packet); + assert!(nonce == 2, 2); + assert!(src_eid == 104, 3); + assert!(sender == bytes32::from_address(@123), 4); + assert!(dst_eid == 102, 5); + assert!(receiver == bytes32::from_address(@0x1234), 6); + assert!(message == b"payload", 7); + + let expected_guid = compute_guid( + 2, + 104, + bytes32::from_address(oapp), + dst_eid, + bytes32::from_address(@0x1234), + ); + + assert!(guid == expected_guid, 8); + (2000, 3000) + } + ); + + assert!(called, 0); + assert!(native_fee == 2000, 2); + assert!(zro_fee == 3000, 3); + } + + #[test] + fun test_register_receive_pathway() { + let oapp = @123; + let src_eid = 0x2; + let sender = bytes32::from_address(@0x1234); + store::init_module_for_test(); + + let oapp_signer = &create_signer_for_test(oapp); + register_oapp(oapp_signer, string::utf8(b"receiver")); + channels::register_receive_pathway(oapp, src_eid, sender); + assert!(was_event_emitted(&channels::receive_pathway_registered_event(oapp, src_eid, from_bytes32(sender))), 0); + assert!(channels::receive_pathway_registered(oapp, src_eid, sender), 0); + } + + #[test] + fun test_inbound() { + let oapp = @123; + let src_eid = 0x2; + let sender = bytes32::from_address(@0x1234); + let payload = b"payload"; + let payload_hash = bytes32::keccak256(payload); + store::init_module_for_test(); + let oapp_signer = &create_signer_for_test(oapp); + register_oapp(oapp_signer, string::utf8(b"receiver")); + channels::register_receive_pathway(oapp, src_eid, sender); + assert!(store::lazy_inbound_nonce(oapp, src_eid, sender) == 0, 0); + + // Inbound #1 + inbound(oapp, src_eid, sender, 1, payload_hash); + assert!(has_payload_hash(oapp, src_eid, sender, 1), 1); + assert!(get_payload_hash(oapp, src_eid, sender, 1) == payload_hash, 2); + + // Inbound #2 + inbound(oapp, src_eid, sender, 2, payload_hash); + assert!(has_payload_hash(oapp, src_eid, sender, 2), 3); + assert!(get_payload_hash(oapp, src_eid, sender, 2) == payload_hash, 4); + + // Inbound nonce should increment, but not lazy without clearing + assert!( + store::lazy_inbound_nonce(oapp, src_eid, sender) == 0, + 5, + ); // inbound does not increment lazy inbound + assert!(inbound_nonce(oapp, src_eid, sender) == 2, 6); // inbound_nonce is incremented + + // Only if cleared does the lazy increment to the inbound + clear_payload(oapp, src_eid, sender, 2, payload); + assert!( + store::lazy_inbound_nonce(oapp, src_eid, sender) == 2, + 5, + ); // inbound does not increment lazy inbound + assert!(inbound_nonce(oapp, src_eid, sender) == 2, 6); // inbound_nonce is incremented + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::channels::EEMPTY_PAYLOAD_HASH)] + fun test_inbound_should_not_accept_empty_payload_hash() { + let oapp = @123; + let src_eid = 0x2; + let sender = bytes32::from_address(@0x1234); + let nonce = 0x1; + let payload_hash = bytes32::zero_bytes32(); + store::init_module_for_test(); + let oapp_signer = &create_signer_for_test(oapp); + register_oapp(oapp_signer, string::utf8(b"receiver")); + channels::register_receive_pathway(oapp, src_eid, sender); + inbound(oapp, src_eid, sender, nonce, payload_hash); + } + + #[test] + fun test_inbound_nonce() { + let oapp = @123; + let src_eid = 0x2; + let sender = bytes32::from_address(@0x1234); + let payload = b"payload"; + let payload_hash = bytes32::keccak256(payload); + + store::init_module_for_test(); + let oapp_signer = &create_signer_for_test(oapp); + register_oapp(oapp_signer, string::utf8(b"receiver")); + channels::register_receive_pathway(oapp, src_eid, sender); + assert!(inbound_nonce(oapp, src_eid, sender) == 0, 0); + inbound(oapp, src_eid, sender, 1, payload_hash); + assert!(inbound_nonce(oapp, src_eid, sender) == 1, 1); + inbound(oapp, src_eid, sender, 2, payload_hash); + assert!(inbound_nonce(oapp, src_eid, sender) == 2, 2); + } + + #[test] + fun test_skip() { + let oapp = @123; + let src_eid = 0x2; + let sender = bytes32::from_address(@0x1234); + store::init_module_for_test(); + let oapp_signer = &create_signer_for_test(oapp); + register_oapp(oapp_signer, string::utf8(b"receiver")); + channels::register_receive_pathway(oapp, src_eid, sender); + assert!(inbound_nonce(oapp, src_eid, sender) == 0, 0); + skip(oapp, src_eid, sender, 1); + assert!(inbound_nonce(oapp, src_eid, sender) == 1, 1); + assert!(was_event_emitted(&inbound_nonce_skipped_event(src_eid, from_bytes32(sender), oapp, 1)), 2); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::channels::EINVALID_NONCE)] + fun test_skip_invalid_nonce() { + let oapp = @123; + let src_eid = 0x2; + let sender = bytes32::from_address(@0x1234); + store::init_module_for_test(); + let oapp_signer = &create_signer_for_test(oapp); + register_oapp(oapp_signer, string::utf8(b"receiver")); + channels::register_receive_pathway(oapp, src_eid, sender); + assert!(inbound_nonce(oapp, src_eid, sender) == 0, 0); + skip(oapp, src_eid, sender, 2); // skip_to_nonce must be 1 to succeed + } + + #[test] + fun test_nilify() { + let oapp = @123; + let src_eid = 0x2; + let sender = bytes32::from_address(@0x1234); + let payload = b"payload"; + let payload_hash = bytes32::keccak256(payload); + store::init_module_for_test(); + let oapp_signer = &create_signer_for_test(oapp); + register_oapp(oapp_signer, string::utf8(b"receiver")); + channels::register_receive_pathway(oapp, src_eid, sender); + + // Inbound #1 + inbound(oapp, src_eid, sender, 1, payload_hash); + assert!(has_payload_hash(oapp, src_eid, sender, 1), 0); + assert!(get_payload_hash(oapp, src_eid, sender, 1) == payload_hash, 1); + + // Inbound #2 + inbound(oapp, src_eid, sender, 2, payload_hash); + assert!(has_payload_hash(oapp, src_eid, sender, 2), 2); + assert!(get_payload_hash(oapp, src_eid, sender, 2) == payload_hash, 3); + + assert!(inbound_nonce(oapp, src_eid, sender) == 2, 3); + + // Nilify + nilify(oapp, src_eid, sender, 1, payload_hash); + assert!(was_event_emitted( + &packet_nilified_event(src_eid, from_bytes32(sender), oapp, 1, from_bytes32(payload_hash)) + ), 4); + + assert!(inbound_nonce(oapp, src_eid, sender) == 2, 3); + assert!( + get_payload_hash(oapp, src_eid, sender, 1) == bytes32::to_bytes32( + x"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + ), + 3, + ); // 1 is nilified + assert!(has_payload_hash(oapp, src_eid, sender, 2), 2); // 2 is not nilified + } + + #[test] + fun test_nilify_for_non_verified() { + let oapp = @123; + let src_eid = 0x2; + let sender = bytes32::from_address(@0x1234); + let payload = b"payload"; + let payload_hash = bytes32::keccak256(payload); + store::init_module_for_test(); + let oapp_signer = &create_signer_for_test(oapp); + register_oapp(oapp_signer, string::utf8(b"receiver")); + channels::register_receive_pathway(oapp, src_eid, sender); + + // Inbound #1 + inbound(oapp, src_eid, sender, 1, payload_hash); + assert!(has_payload_hash(oapp, src_eid, sender, 1), 0); + assert!(get_payload_hash(oapp, src_eid, sender, 1) == payload_hash, 1); + + // Inbound #2 + inbound(oapp, src_eid, sender, 2, payload_hash); + assert!(has_payload_hash(oapp, src_eid, sender, 2), 2); + assert!(get_payload_hash(oapp, src_eid, sender, 2) == payload_hash, 3); + + assert!(inbound_nonce(oapp, src_eid, sender) == 2, 3); + + let empty_payload_hash = x"0000000000000000000000000000000000000000000000000000000000000000"; + // Nilify + nilify( + oapp, + src_eid, + sender, + 3, + to_bytes32(empty_payload_hash), + ); + assert!(was_event_emitted( + &packet_nilified_event(src_eid, from_bytes32(sender), oapp, 3, empty_payload_hash) + ), 4); + + // inbound nonce increments to be beyond the nilified nonce + assert!(inbound_nonce(oapp, src_eid, sender) == 3, 3); + assert!( + get_payload_hash(oapp, src_eid, sender, 3) == bytes32::to_bytes32( + x"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + ), + 3, + ); // 3 is nilified + } + + #[test] + fun test_nilify_for_non_verified_2() { + let oapp = @123; + let src_eid = 0x2; + let sender = bytes32::from_address(@0x1234); + let payload = b"payload"; + let payload_hash = bytes32::keccak256(payload); + store::init_module_for_test(); + let oapp_signer = &create_signer_for_test(oapp); + register_oapp(oapp_signer, string::utf8(b"receiver")); + channels::register_receive_pathway(oapp, src_eid, sender); + + // Inbound #1 (don't do second inbound so there is a gap) + inbound(oapp, src_eid, sender, 1, payload_hash); + assert!(has_payload_hash(oapp, src_eid, sender, 1), 0); + assert!(get_payload_hash(oapp, src_eid, sender, 1) == payload_hash, 1); + + assert!(inbound_nonce(oapp, src_eid, sender) == 1, 3); + + let empty_payload_hash = x"0000000000000000000000000000000000000000000000000000000000000000"; + // Nilify + nilify( + oapp, + src_eid, + sender, + 3, + to_bytes32(empty_payload_hash), + ); + assert!(was_event_emitted( + &packet_nilified_event(src_eid, from_bytes32(sender), oapp, 3, empty_payload_hash) + ), 4); + + // inbound nonce doesn't increment + assert!(inbound_nonce(oapp, src_eid, sender) == 1, 3); + assert!( + get_payload_hash(oapp, src_eid, sender, 3) == bytes32::to_bytes32( + x"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + ), + 3, + ); // 3 is nilified + } + + #[test] + fun test_can_skip_after_nillify() { + let oapp = @123; + let src_eid = 0x2; + let sender = bytes32::from_address(@0x1234); + let payload = b"payload"; + let payload_hash = bytes32::keccak256(payload); + store::init_module_for_test(); + let oapp_signer = &create_signer_for_test(oapp); + register_oapp(oapp_signer, string::utf8(b"receiver")); + channels::register_receive_pathway(oapp, src_eid, sender); + + inbound(oapp, src_eid, sender, 1, payload_hash); + inbound(oapp, src_eid, sender, 2, payload_hash); + + nilify(oapp, src_eid, sender, 2, payload_hash); + nilify(oapp, src_eid, sender, 1, payload_hash); + + skip(oapp, src_eid, sender, 3); + + assert!(inbound_nonce(oapp, src_eid, sender) == 3, 0); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::channels::EINVALID_NONCE)] + fun test_cannot_skip_to_non_next_nonce() { + let oapp = @123; + let src_eid = 0x2; + let sender = bytes32::from_address(@0x1234); + let payload = b"payload"; + let payload_hash = bytes32::keccak256(payload); + store::init_module_for_test(); + let oapp_signer = &create_signer_for_test(oapp); + register_oapp(oapp_signer, string::utf8(b"receiver")); + channels::register_receive_pathway(oapp, src_eid, sender); + + inbound(oapp, src_eid, sender, 1, payload_hash); + inbound(oapp, src_eid, sender, 2, payload_hash); + + assert!(inbound_nonce(oapp, src_eid, sender) == 2, 0); + + skip(oapp, src_eid, sender, 2); // skip_to_nonce must be 3 to succeed + } + + + #[test] + fun test_burn() { + let oapp = @123; + let src_eid = 0x2; + let sender = bytes32::from_address(@0x1234); + let payload = b"payload"; + let payload_hash = bytes32::keccak256(payload); + store::init_module_for_test(); + let oapp_signer = &create_signer_for_test(oapp); + register_oapp(oapp_signer, string::utf8(b"receiver")); + channels::register_receive_pathway(oapp, src_eid, sender); + + inbound(oapp, src_eid, sender, 1, payload_hash); + inbound(oapp, src_eid, sender, 2, payload_hash); + assert!(inbound_nonce(oapp, src_eid, sender) == 2, 0); + + skip(oapp, src_eid, sender, 3); + + burn(oapp, src_eid, sender, 1, payload_hash); + assert!( + was_event_emitted(&packet_burnt_event(src_eid, from_bytes32(sender), oapp, 1, from_bytes32(payload_hash))), + 1, + ); + + assert!(store::lazy_inbound_nonce(oapp, src_eid, sender) == 3, 2); + assert!(!has_payload_hash(oapp, src_eid, sender, 1), 3); // 1 is burnt + assert!(has_payload_hash(oapp, src_eid, sender, 2), 4); // 2 is not burnt + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::channels::EINVALID_NONCE)] + fun test_cannot_burn_a_nonce_after_lazy_inbound_nonce() { + let oapp = @123; + let src_eid = 0x2; + let sender = bytes32::from_address(@0x1234); + let payload = b"payload"; + let payload_hash = bytes32::keccak256(payload); + store::init_module_for_test(); + let oapp_signer = &create_signer_for_test(oapp); + register_oapp(oapp_signer, string::utf8(b"receiver")); + channels::register_receive_pathway(oapp, src_eid, sender); + + inbound(oapp, src_eid, sender, 1, payload_hash); + inbound(oapp, src_eid, sender, 2, payload_hash); + assert!(inbound_nonce(oapp, src_eid, sender) == 2, 0); + + assert!(store::lazy_inbound_nonce(oapp, src_eid, sender) == 0, 1); + burn(oapp, src_eid, sender, 1, payload_hash); // lazy inbound is still 0 + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::channels::ENO_PAYLOAD_HASH)] + fun test_cannot_clear_a_burnt_nonce() { + let oapp = @123; + let src_eid = 0x2; + let sender = bytes32::from_address(@0x1234); + let payload = b"payload"; + let payload_hash = bytes32::keccak256(payload); + store::init_module_for_test(); + let oapp_signer = &create_signer_for_test(oapp); + register_oapp(oapp_signer, string::utf8(b"receiver")); + channels::register_receive_pathway(oapp, src_eid, sender); + + inbound(oapp, src_eid, sender, 1, payload_hash); + inbound(oapp, src_eid, sender, 2, payload_hash); + assert!(inbound_nonce(oapp, src_eid, sender) == 2, 0); + + clear_payload(oapp, src_eid, sender, 2, payload); // clear 2 + assert!(!has_payload_hash(oapp, src_eid, sender, 2), 1); // 2 is cleared + assert!(has_payload_hash(oapp, src_eid, sender, 1), 2); // still has hash #1 + + burn(oapp, src_eid, sender, 1, payload_hash); // burn 1 + assert!(!has_payload_hash(oapp, src_eid, sender, 1), 3); // 1 is burnt + + clear_payload(oapp, src_eid, sender, 1, payload); // cannot clear 1 + } + + #[test] + fun test_clear_payload() { + let oapp = @123; + let src_eid = 0x2; + let sender = bytes32::from_address(@0x1234); + let payload = b"payload"; + let payload_hash = bytes32::keccak256(payload); + store::init_module_for_test(); + let oapp_signer = &create_signer_for_test(oapp); + register_oapp(oapp_signer, string::utf8(b"receiver")); + channels::register_receive_pathway(oapp, src_eid, sender); + + inbound(oapp, src_eid, sender, 1, payload_hash); + inbound(oapp, src_eid, sender, 2, payload_hash); + assert!(store::lazy_inbound_nonce(oapp, src_eid, sender) == 0, 1); + assert!(inbound_nonce(oapp, src_eid, sender) == 2, 0); + + // clear 2 + clear_payload(oapp, src_eid, sender, 2, payload); + + assert!(store::lazy_inbound_nonce(oapp, src_eid, sender) == 2, 1); + assert!(inbound_nonce(oapp, src_eid, sender) == 2, 0); + assert!(!has_payload_hash(oapp, src_eid, sender, 2), 2); + + inbound(oapp, src_eid, sender, 3, payload_hash); + inbound(oapp, src_eid, sender, 4, payload_hash); + + // clear 1, 4, 3 + clear_payload(oapp, src_eid, sender, 1, payload); + clear_payload(oapp, src_eid, sender, 4, payload); + clear_payload(oapp, src_eid, sender, 3, payload); + } + + #[test] + fun test_can_clear_out_of_order() { + let oapp = @123; + let src_eid = 0x2; + let sender = bytes32::from_address(@0x1234); + let payload = b"payload"; + let payload_hash = bytes32::keccak256(payload); + store::init_module_for_test(); + let oapp_signer = &create_signer_for_test(oapp); + register_oapp(oapp_signer, string::utf8(b"receiver")); + channels::register_receive_pathway(oapp, src_eid, sender); + + inbound(oapp, src_eid, sender, 1, payload_hash); + inbound(oapp, src_eid, sender, 2, payload_hash); + assert!(store::lazy_inbound_nonce(oapp, src_eid, sender) == 0, 1); + assert!(inbound_nonce(oapp, src_eid, sender) == 2, 0); + + clear_payload(oapp, src_eid, sender, 2, payload); + assert!(has_payload_hash(oapp, src_eid, sender, 1), 2); // still has hash #1 + assert!(!has_payload_hash(oapp, src_eid, sender, 2), 2); // hash #2 removed + + clear_payload(oapp, src_eid, sender, 1, payload); + assert!(!has_payload_hash(oapp, src_eid, sender, 1), 2); // now hash #1 removed + assert!(store::lazy_inbound_nonce(oapp, src_eid, sender) == 2, 1); + assert!(inbound_nonce(oapp, src_eid, sender) == 2, 0); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::channels::EPAYLOAD_HASH_DOES_NOT_MATCH)] + fun test_clear_payload_should_fail_with_invalid_payload() { + let oapp = @123; + let src_eid = 0x2; + let sender = bytes32::from_address(@0x1234); + let payload = b"payload"; + let payload_hash = bytes32::keccak256(payload); + let invalid_payload = b"invalid_payload"; + store::init_module_for_test(); + let oapp_signer = &create_signer_for_test(oapp); + register_oapp(oapp_signer, string::utf8(b"receiver")); + channels::register_receive_pathway(oapp, src_eid, sender); + + inbound(oapp, src_eid, sender, 1, payload_hash); + inbound(oapp, src_eid, sender, 2, payload_hash); + assert!(store::lazy_inbound_nonce(oapp, src_eid, sender) == 0, 1); + assert!(inbound_nonce(oapp, src_eid, sender) == 2, 0); + + clear_payload(oapp, src_eid, sender, 2, invalid_payload); + clear_payload(oapp, src_eid, sender, 1, invalid_payload); + } + + #[test] + fun test_verify() { + // packet details + let receiver_oapp = @123; + let src_eid = 0x2; + let dst_eid = 0x3; + let sender = bytes32::from_address(@0x1234); + let receiver = bytes32::from_address(receiver_oapp); + let payload = b"payload"; + let payload_hash = bytes32::keccak256(payload); + let packet_header = packet_v1_codec::new_packet_v1_header_only( + src_eid, + sender, + dst_eid, + receiver, + 1, + ); + universal_config::init_module_for_test(dst_eid); + store::init_module_for_test(); + + // register destination defaults + msglib_manager::register_library(@simple_msglib); + msglib_manager::set_default_receive_library(src_eid, @simple_msglib, 0); + + // register destination + registration::register_oapp(receiver_oapp, string::utf8(b"receiver")); + channels::register_receive_pathway(receiver_oapp, src_eid, sender); // register pathway to receive from sender + + // initialize destination on simple_msglib + simple_msglib::msglib::initialize_for_test(); + + verify( + @simple_msglib, + packet_header, + payload_hash, + b"", + ); + + assert!(was_event_emitted(&packet_verified_event( + src_eid, + from_bytes32(sender), + 1, + bytes32::to_address(receiver), + from_bytes32(payload_hash), + )), 0); + } + + #[test] + fun test_verify_internal() { + // packet details + let receiver_oapp = @0x123; + let src_eid = 102; + let sender = bytes32::from_address(@0x1234); + let payload = b"payload"; + let payload_hash = bytes32::keccak256(payload); + let nonce = 11; + // use blocked message lib to ensure that it's not actually called when using the internal verify function, + // except the version() function, which is called upon library registration, but not in verify_internal() + let msglib = @blocked_msglib; + store::init_module_for_test(); + + // register destination defaults + msglib_manager::register_library(msglib); + msglib_manager::set_default_receive_library(src_eid, msglib, 0); + + // register destination + registration::register_oapp(receiver_oapp, string::utf8(b"receiver")); + channels::register_receive_pathway(receiver_oapp, src_eid, sender); // register pathway to receive from sender + + verify_internal( + msglib, + payload_hash, + receiver_oapp, + src_eid, + sender, + nonce, + ); + + assert!(was_event_emitted(&packet_verified_event( + src_eid, + from_bytes32(sender), + nonce, + receiver_oapp, + from_bytes32(payload_hash), + )), 0); + } + + + #[test] + fun test_verifiable() { + store::init_module_for_test(); + // returns false if pathway not registered + assert!(!channels::verifiable(@222, 1, bytes32::from_address(@123), 1), 0); + + // still returns false if registered but no pathway + registration::register_oapp(@123, string::utf8(b"receiver")); + assert!(!channels::verifiable(@222, 1, bytes32::from_address(@123), 1), 1); + + // return true if nonce (1) > lazy inbound(0) + channels::register_receive_pathway(@123, 1, bytes32::from_address(@222)); + assert!(channels::verifiable(@123, 1, bytes32::from_address(@222), 1), 1); + + // return false if nonce (0) <= lazy inbound(0) + assert!(!channels::verifiable(@123, 1, bytes32::from_address(@222), 0), 2); + + // return true if nonce (0) > lazy inbound(0) but has payload hash + channels::inbound(@123, 1, bytes32::from_address(@222), 0, bytes32::from_address(@999)); + assert!(channels::verifiable(@123, 1, bytes32::from_address(@222), 0), 2); + } + + #[test] + fun test_verify_allows_reverifying_if_not_executed() { + // packet details + let receiver_oapp = @123; + let src_eid = 0x2; + let dst_eid = 0x3; + let sender = bytes32::from_address(@0x1234); + let receiver = bytes32::from_address(receiver_oapp); + let payload = b"payload"; + let payload_hash = bytes32::keccak256(payload); + let packet_header = packet_v1_codec::new_packet_v1_header_only( + src_eid, + sender, + dst_eid, + receiver, + 1, + ); + store::init_module_for_test(); + universal_config::init_module_for_test(dst_eid); + + // register destination defaults + msglib_manager::register_library(@simple_msglib); + msglib_manager::set_default_receive_library(src_eid, @simple_msglib, 0); + + // register destination + registration::register_oapp(receiver_oapp, string::utf8(b"receiver")); + channels::register_receive_pathway(receiver_oapp, src_eid, sender); // register pathway to receive from sender + + // initialize destination on simple_msglib + simple_msglib::msglib::initialize_for_test(); + + verify( + @simple_msglib, + packet_header, + payload_hash, + b"", + ); + assert!(vector::length(&emitted_events()) == 1, 0); + verify( + @simple_msglib, + packet_header, + payload_hash, + b"", + ); + assert!(vector::length(&emitted_events()) == 2, 0); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::store::EUNREGISTERED_PATHWAY)] + fun test_verify_fails_if_unregistered_pathway() { + // packet details + let receiver_oapp = @123; + let src_eid = 0x2; + let dst_eid = 0x3; + let sender = bytes32::from_address(@0x1234); + let receiver = bytes32::from_address(receiver_oapp); + let payload = b"payload"; + let payload_hash = bytes32::keccak256(payload); + let packet_header = packet_v1_codec::new_packet_v1_header_only( + src_eid, + sender, + dst_eid, + receiver, + 1, + ); + store::init_module_for_test(); + universal_config::init_module_for_test(dst_eid); + + // register destination defaults + msglib_manager::register_library(@simple_msglib); + msglib_manager::set_default_receive_library(src_eid, @simple_msglib, 0); + + // register destination + registration::register_oapp(receiver_oapp, string::utf8(b"receiver")); + + // initialize destination on simple_msglib + simple_msglib::msglib::initialize_for_test(); + + verify( + @simple_msglib, + packet_header, + payload_hash, + b"", + ); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::channels::EINVALID_MSGLIB)] + fun test_verify_fails_receive_library_doesnt_match() { + // packet details + let receiver_oapp = @123; + let src_eid = 0x2; + let dst_eid = 0x3; + let sender = bytes32::from_address(@0x1234); + let receiver = bytes32::from_address(receiver_oapp); + let payload = b"payload"; + let payload_hash = bytes32::keccak256(payload); + let packet_header = packet_v1_codec::new_packet_v1_header_only( + src_eid, + sender, + dst_eid, + receiver, + 1, + ); + store::init_module_for_test(); + universal_config::init_module_for_test(dst_eid); + + // register destination + registration::register_oapp(receiver_oapp, string::utf8(b"receiver")); + channels::register_receive_pathway(receiver_oapp, src_eid, sender); // register pathway to receive from sender + + // register destination defaults + msglib_manager::register_library(@simple_msglib); + msglib_manager::register_library(@blocked_msglib); + msglib_manager::set_default_receive_library(src_eid, @simple_msglib, 0); + // oapp registers a different library + msglib_manager::set_receive_library(receiver_oapp, src_eid, @blocked_msglib, 0); + + + // initialize destination on simple_msglib + simple_msglib::msglib::initialize_for_test(); + + verify( + @simple_msglib, + packet_header, + payload_hash, + b"", + ); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::channels::ENOT_VERIFIABLE)] + fun test_verify_fails_if_nonce_already_processed() { + // packet details + let receiver_oapp = @123; + let src_eid = 0x2; + let dst_eid = 0x3; + let sender = bytes32::from_address(@0x1234); + let receiver = bytes32::from_address(receiver_oapp); + let payload = b"payload"; + let payload_hash = bytes32::keccak256(payload); + let packet_header = packet_v1_codec::new_packet_v1_header_only( + src_eid, + sender, + dst_eid, + receiver, + 1, + ); + store::init_module_for_test(); + universal_config::init_module_for_test(dst_eid); + + // register destination defaults + msglib_manager::register_library(@simple_msglib); + msglib_manager::set_default_receive_library(src_eid, @simple_msglib, 0); + + // register destination + registration::register_oapp(receiver_oapp, string::utf8(b"receiver")); + channels::register_receive_pathway(receiver_oapp, src_eid, sender); // register pathway to receive from sender + + // initialize destination on simple_msglib + simple_msglib::msglib::initialize_for_test(); + + skip(receiver_oapp, src_eid, sender, 1); + + verify( + @simple_msglib, + packet_header, + payload_hash, + b"", + ); + } +} diff --git a/packages/layerzero-v2/aptos/contracts/endpoint_v2/tests/internal/messaging_composer_tests.move b/packages/layerzero-v2/aptos/contracts/endpoint_v2/tests/internal/messaging_composer_tests.move new file mode 100644 index 00000000..45c8748f --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/endpoint_v2/tests/internal/messaging_composer_tests.move @@ -0,0 +1,52 @@ +#[test_only] +module endpoint_v2::messaging_composer_tests { + use std::string; + + use endpoint_v2::messaging_composer::{clear_compose, send_compose}; + use endpoint_v2::store; + use endpoint_v2_common::bytes32; + + #[test] + fun test_send_compose_and_clear() { + let oapp: address = @0x1; + let to = @0x2; + let index = 0; + let guid = bytes32::to_bytes32(b"................................"); + let message = b"message"; + store::init_module_for_test(); + store::register_composer(to, string::utf8(b"test")); + + send_compose(oapp, to, index, guid, message); + clear_compose(oapp, to, guid, index, message); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::messaging_composer::ECOMPOSE_NOT_FOUND)] + fun test_cannot_clear_before_send_compose() { + let oapp: address = @0x1; + let to = @0x2; + let index = 0; + let guid = bytes32::to_bytes32(b"................................"); + let message = b"message"; + store::init_module_for_test(); + store::register_composer(to, string::utf8(b"test")); + + clear_compose(oapp, to, guid, index, message); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::messaging_composer::ECOMPOSE_ALREADY_CLEARED)] + fun test_cannot_clear_same_message_twice() { + let oapp: address = @0x1; + let to = @0x2; + let index = 0; + let guid = bytes32::to_bytes32(b"................................"); + let message = b"message"; + store::init_module_for_test(); + store::register_composer(to, string::utf8(b"test")); + + send_compose(oapp, to, index, guid, message); + clear_compose(oapp, to, guid, index, message); + clear_compose(oapp, to, guid, index, message); + } +} diff --git a/packages/layerzero-v2/aptos/contracts/endpoint_v2/tests/internal/messaging_receipt_tests.move b/packages/layerzero-v2/aptos/contracts/endpoint_v2/tests/internal/messaging_receipt_tests.move new file mode 100644 index 00000000..8aaa72d9 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/endpoint_v2/tests/internal/messaging_receipt_tests.move @@ -0,0 +1,27 @@ +#[test_only] +module endpoint_v2::messaging_receipt_tests { + use endpoint_v2_common::bytes32; + + #[test] + fun test_messaging_receipt() { + let guid = bytes32::from_address(@0x12345678); + let nonce = 12; + let native_fee = 100; + let zro_fee = 10; + + let receipt = endpoint_v2::messaging_receipt::new_messaging_receipt(guid, nonce, native_fee, zro_fee); + + // check getters + assert!(endpoint_v2::messaging_receipt::get_guid(&receipt) == guid, 0); + assert!(endpoint_v2::messaging_receipt::get_nonce(&receipt) == nonce, 0); + assert!(endpoint_v2::messaging_receipt::get_native_fee(&receipt) == native_fee, 0); + assert!(endpoint_v2::messaging_receipt::get_zro_fee(&receipt) == zro_fee, 0); + + // check unpacked + let (guid_, nonce_, native_fee_, zro_fee_) = endpoint_v2::messaging_receipt::unpack_messaging_receipt(receipt); + assert!(guid_ == guid, 0); + assert!(nonce_ == nonce, 0); + assert!(native_fee_ == native_fee, 0); + assert!(zro_fee_ == zro_fee, 0); + } +} diff --git a/packages/layerzero-v2/aptos/contracts/endpoint_v2/tests/internal/msglib_manager_tests.move b/packages/layerzero-v2/aptos/contracts/endpoint_v2/tests/internal/msglib_manager_tests.move new file mode 100644 index 00000000..27aecf4f --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/endpoint_v2/tests/internal/msglib_manager_tests.move @@ -0,0 +1,806 @@ +#[test_only] +module endpoint_v2::msglib_manager_tests { + use std::account::create_signer_for_test; + use std::event::was_event_emitted; + use std::string; + use std::timestamp; + + use endpoint_v2::endpoint::{get_effective_send_library, get_registered_libraries, is_registered_library}; + use endpoint_v2::msglib_manager::{ + default_receive_library_set_event, + default_receive_library_timeout_set_event, + default_send_library_set_event, + get_default_receive_library, + get_default_send_library, + get_effective_receive_library, + is_valid_receive_library_for_oapp, + library_registered_event, + matches_default_receive_library, + receive_library_set_event, + receive_library_timeout_set_event, + register_library, + send_library_set_event, + set_default_receive_library, + set_default_receive_library_timeout, + set_default_send_library, + set_receive_library, + set_receive_library_timeout, + set_send_library, + }; + use endpoint_v2::store; + use endpoint_v2::timeout; + use endpoint_v2::timeout_test_helpers::{Self, set_block_height}; + use uln_302::configuration_tests::enable_receive_eid_for_test; + + const OAPP_ADDRESS: address = @0x1234; + + #[test_only] + /// Initialize the test environment + fun init_for_test() { + let native_framework = &create_signer_for_test(@std); + // start the clock + timestamp::set_time_has_started_for_testing(native_framework); + timeout_test_helpers::setup_for_timeouts(); + // initialize + endpoint_v2_common::universal_config::init_module_for_test(100); + store::init_module_for_test(); + // register some libraries + register_library(@blocked_msglib); + register_library(@simple_msglib); + register_library(@uln_302); + uln_302::msglib::initialize_for_test();// register an OApp + store::register_oapp(OAPP_ADDRESS, string::utf8(b"test")); + } + + // ================================================= Registration ================================================= + + #[test] + fun test_register_library() { + store::init_module_for_test(); + + // Register a new library + register_library(@blocked_msglib); + assert!(was_event_emitted(&library_registered_event(@blocked_msglib)), 2); + assert!(is_registered_library(@blocked_msglib), 0); + assert!(!is_registered_library(@simple_msglib), 1); + + // Register another library + register_library(@simple_msglib); + assert!(was_event_emitted(&library_registered_event(@simple_msglib)), 2); + assert!(is_registered_library(@blocked_msglib), 3); + assert!(is_registered_library(@simple_msglib), 4); + + let msglibs = get_registered_libraries(0, 10); + assert!(msglibs == vector[@blocked_msglib, @simple_msglib], 5); + + let msglibs = get_registered_libraries(0, 1); + assert!(msglibs == vector[@blocked_msglib], 6); + + let msglibs = get_registered_libraries(1, 5); + assert!(msglibs == vector[@simple_msglib], 7); + + let msglibs = get_registered_libraries(4, 5); + assert!(msglibs == vector[], 8); + + let msglibs = get_registered_libraries(1, 0); + assert!(msglibs == vector[], 8); + } + + #[test] + #[expected_failure(abort_code = router_node_1::router_node::ENOT_IMPLEMENTED)] + fun test_register_library_fails_if_unroutable() { + store::init_module_for_test(); + + // Register an unroutable library + register_library(@0x9874123); + } + + // ================================================ Send Libraries ================================================ + + #[test] + fun test_set_default_send_library() { + init_for_test(); + let dst_eid = 1; + + // Set the default send library + set_default_send_library(dst_eid, @blocked_msglib); + assert!(was_event_emitted(&default_send_library_set_event(dst_eid, @blocked_msglib)), 1); + assert!(store::get_default_send_library(dst_eid) == @blocked_msglib, 0); + + // Update the default send library + set_default_send_library(dst_eid, @simple_msglib); + assert!(was_event_emitted(&default_send_library_set_event(dst_eid, @simple_msglib)), 2); + assert!(store::get_default_send_library(dst_eid) == @simple_msglib, 1); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::msglib_manager::EATTEMPTED_TO_SET_CURRENT_LIBRARY)] + fun test_set_default_send_library_fails_if_setting_to_current_value() { + init_for_test(); + let dst_eid = 1; + + // Set the default send library twice to same value + set_default_send_library(dst_eid, @blocked_msglib); + set_default_send_library(dst_eid, @blocked_msglib); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::msglib_manager::EUNREGISTERED_MSGLIB)] + fun test_set_default_send_library_fails_if_library_not_registered() { + store::init_module_for_test(); + register_library(@blocked_msglib); + + // Set the default send library to an unregistered library + set_default_send_library(1, @simple_msglib); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::msglib_manager::EUNSUPPORTED_DST_EID)] + fun test_set_default_send_library_fails_if_library_does_not_support_eid() { + init_for_test(); + let dst_eid = 1; + // only enable receive side, not send side + uln_302::configuration_tests::enable_receive_eid_for_test(dst_eid); + + // Set the default send library to a library that does not support the EID + set_default_send_library(dst_eid, @uln_302); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::msglib_manager::EUNREGISTERED_MSGLIB)] + fun test_set_default_send_library_fails_if_attempting_to_unset() { + init_for_test(); + + // Attempt to unset + set_default_send_library(1, @0x0); + } + + #[test] + fun test_set_send_library() { + init_for_test(); + let dst_eid = 1; + uln_302::configuration_tests::enable_send_eid_for_test(dst_eid); + + // Set the default send library + set_default_send_library(dst_eid, @blocked_msglib); + assert!(was_event_emitted(&default_send_library_set_event(dst_eid, @blocked_msglib)), 3); + assert!(get_default_send_library(dst_eid) == @blocked_msglib, 0); + let (lib, is_default) = get_effective_send_library(OAPP_ADDRESS, dst_eid); + assert!(lib == @blocked_msglib, 1); + assert!(is_default, 2); + + // Set the OApp send library + set_send_library(OAPP_ADDRESS, dst_eid, @simple_msglib); + assert!(was_event_emitted(&send_library_set_event(OAPP_ADDRESS, dst_eid, @simple_msglib)), 4); + let (lib, is_default) = get_effective_send_library(OAPP_ADDRESS, dst_eid); + assert!(lib == @simple_msglib, 3); + assert!(!is_default, 4); + + // Update the OApp send library + set_send_library(OAPP_ADDRESS, dst_eid, @uln_302); + assert!(was_event_emitted(&send_library_set_event(OAPP_ADDRESS, dst_eid, @uln_302)), 7); + let (lib, is_default) = get_effective_send_library(OAPP_ADDRESS, dst_eid); + assert!(lib == @uln_302, 5); + assert!(!is_default, 6); + + // Unset the OApp send library + set_send_library(OAPP_ADDRESS, dst_eid, @0x0); + assert!(was_event_emitted(&send_library_set_event(OAPP_ADDRESS, dst_eid, @0x0)), 9); + let (lib, is_default) = get_effective_send_library(OAPP_ADDRESS, dst_eid); + assert!(lib == @blocked_msglib, 7); + assert!(is_default, 8); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::msglib_manager::EUNREGISTERED_MSGLIB)] + fun test_set_send_library_fails_if_library_not_registered() { + store::init_module_for_test(); + register_library(@blocked_msglib); + + // Set the OApp send library to an unregistered library + set_send_library(OAPP_ADDRESS, 1, @simple_msglib); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::msglib_manager::EATTEMPTED_TO_SET_CURRENT_LIBRARY)] + fun test_set_send_library_fails_if_setting_to_current_value() { + init_for_test(); + let dst_eid = 1; + set_default_send_library(dst_eid, @simple_msglib); + + // Set the OApp send library twice to same value + set_send_library(OAPP_ADDRESS, dst_eid, @simple_msglib); + set_send_library(OAPP_ADDRESS, dst_eid, @simple_msglib); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::msglib_manager::EUNSUPPORTED_DST_EID)] + fun test_set_send_library_fails_if_library_does_not_support_eid() { + init_for_test(); + let dst_eid = 1; + // only enable receive side, not send side + uln_302::configuration_tests::enable_receive_eid_for_test(dst_eid); + + // Set the OApp send library to a library that does not support the EID + set_send_library(OAPP_ADDRESS, dst_eid, @uln_302); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::msglib_manager::EOAPP_SEND_LIB_NOT_SET)] + fun test_set_send_library_fails_if_trying_to_unset_library_that_isnt_set() { + init_for_test(); + let dst_eid = 1; + + // Attempt to unset without having already set + set_send_library(OAPP_ADDRESS, dst_eid, @0x0); + } + + // Requires Manual Verification: + // * set_default_send_library_fails_if_attempting_to_set_to_receive_only_library + // * set_send_library_fails_if_attempting_to_set_to_receive_only_library + + // =============================================== Receive Libraries ============================================== + + #[test] + fun test_set_default_receive_library() { + init_for_test(); + let src_eid = 1; + + // Set the default receive library + set_default_receive_library(src_eid, @blocked_msglib, 0); + assert!(was_event_emitted(&default_receive_library_set_event(src_eid, @blocked_msglib)), 0); + assert!(get_default_receive_library(src_eid) == @blocked_msglib, 0); + assert!(matches_default_receive_library(src_eid, @blocked_msglib), 0); + + // Update the default receive library (no timeout) + set_default_receive_library(src_eid, @simple_msglib, 0); + assert!(was_event_emitted(&default_receive_library_set_event(src_eid, @simple_msglib)), 1); + assert!(get_default_receive_library(src_eid) == @simple_msglib, 1); + assert!(matches_default_receive_library(src_eid, @simple_msglib), 0); + assert!(!matches_default_receive_library(src_eid, @blocked_msglib), 0); + + // Update the default receive library (with timeout) + enable_receive_eid_for_test(src_eid); + set_default_receive_library(src_eid, @uln_302, 10); + assert!(was_event_emitted(&default_receive_library_set_event(src_eid, @uln_302)), 2); + assert!(was_event_emitted(&default_receive_library_timeout_set_event(src_eid, @simple_msglib, 10)), 3); + assert!(get_default_receive_library(src_eid) == @uln_302, 2); + // matches both + assert!(matches_default_receive_library(src_eid, @simple_msglib), 1); + assert!(matches_default_receive_library(src_eid, @uln_302), 1); + + // Update the default receive library (no timeout) + set_default_receive_library(src_eid, @blocked_msglib, 0); + assert!(was_event_emitted(&default_receive_library_set_event(src_eid, @blocked_msglib)), 4); + assert!(was_event_emitted(&default_receive_library_timeout_set_event(src_eid, @0x0, 0)), 5); + assert!(get_default_receive_library(src_eid) == @blocked_msglib, 3); + // does not preserve old timemout library or prior library + assert!(matches_default_receive_library(src_eid, @blocked_msglib), 2); + assert!(!matches_default_receive_library(src_eid, @simple_msglib), 2); + assert!(!matches_default_receive_library(src_eid, @uln_302), 2); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::msglib_manager::EUNREGISTERED_MSGLIB)] + fun test_set_default_receive_libraries_fails_if_not_registered() { + store::init_module_for_test(); + register_library(@blocked_msglib); + + // Set the default receive library to an unregistered library + set_default_receive_library(1, @simple_msglib, 0); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::msglib_manager::EATTEMPTED_TO_SET_CURRENT_LIBRARY)] + fun test_set_default_receive_libraries_fails_if_setting_to_current_value() { + init_for_test(); + let src_eid = 1; + + // Set the default receive library twice to same value + set_default_receive_library(src_eid, @simple_msglib, 0); + set_default_receive_library(src_eid, @simple_msglib, 0); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::msglib_manager::EUNSUPPORTED_SRC_EID)] + fun test_set_default_receive_libraries_fails_if_library_does_not_support_eid() { + init_for_test(); + let src_eid = 1; + // only enable send side, not receive side + uln_302::configuration_tests::enable_send_eid_for_test(src_eid); + + // Set the default receive library to a library that does not support the EID + set_default_receive_library(src_eid, @uln_302, 0); + } + + #[test] + fun test_set_receive_library() { + init_for_test(); + + // Set the OApp receive library + set_default_receive_library(1, @blocked_msglib, 0); + let (lib, is_default) = get_effective_receive_library(OAPP_ADDRESS, 1); + assert!(lib == @blocked_msglib, 0); + assert!(is_default, 0); + + set_receive_library(OAPP_ADDRESS, 1, @simple_msglib, 0); + assert!(was_event_emitted(&receive_library_set_event(OAPP_ADDRESS, 1, @simple_msglib)), 0); + assert!(was_event_emitted(&receive_library_timeout_set_event(OAPP_ADDRESS, 1, @0x0, 0)), 4); + let (lib, is_default) = get_effective_receive_library(OAPP_ADDRESS, 1); + assert!(lib == @simple_msglib, 0); + assert!(!is_default, 0); + + // Use a timeout + set_receive_library(OAPP_ADDRESS, 1, @blocked_msglib, 10); + assert!(was_event_emitted(&receive_library_set_event(OAPP_ADDRESS, 1, @blocked_msglib)), 1); + assert!(was_event_emitted(&receive_library_timeout_set_event(OAPP_ADDRESS, 1, @simple_msglib, 10)), 2); + let (lib, is_default) = get_effective_receive_library(OAPP_ADDRESS, 1); + assert!(lib == @blocked_msglib, 0); + assert!(!is_default, 0); + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @simple_msglib), 0); + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @blocked_msglib), 0); + + // Setting without a timeout removes the old timeout + uln_302::configuration_tests::enable_receive_eid_for_test(1); + set_receive_library(OAPP_ADDRESS, 1, @uln_302, 0); + assert!(was_event_emitted(&receive_library_set_event(OAPP_ADDRESS, 1, @uln_302)), 3); + assert!(was_event_emitted(&receive_library_timeout_set_event(OAPP_ADDRESS, 1, @blocked_msglib, 0)), 4); + let (lib, is_default) = get_effective_receive_library(OAPP_ADDRESS, 1); + assert!(lib == @uln_302, 0); + assert!(!is_default, 0); + assert!(!is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @simple_msglib), 0); + assert!(!is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @blocked_msglib), 0); + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @uln_302), 0); + + // Setting with timeout + set_receive_library(OAPP_ADDRESS, 1, @simple_msglib, 10); + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @uln_302), 0); + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @simple_msglib), 0); + + // Unsetting removes the timeout also + set_receive_library(OAPP_ADDRESS, 1, @0x0, 0); + assert!(was_event_emitted(&receive_library_set_event(OAPP_ADDRESS, 1, @0x0)), 5); + assert!(was_event_emitted(&receive_library_timeout_set_event(OAPP_ADDRESS, 1, @simple_msglib, 0)), 6); + assert!(!is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @uln_302), 0); + assert!(!is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @simple_msglib), 0); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::msglib_manager::EUNREGISTERED_MSGLIB)] + fun test_set_receive_library_fails_if_not_registered() { + store::init_module_for_test(); + register_library(@blocked_msglib); + + // Set the OApp receive library to an unregistered library + set_receive_library(OAPP_ADDRESS, 1, @simple_msglib, 0); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::msglib_manager::EATTEMPTED_TO_SET_CURRENT_LIBRARY)] + fun test_set_receive_library_fails_if_setting_to_current_value() { + init_for_test(); + let src_eid = 1; + set_default_receive_library(src_eid, @simple_msglib, 0); + + // Set the OApp receive library twice to same value + set_receive_library(OAPP_ADDRESS, src_eid, @simple_msglib, 0); + set_receive_library(OAPP_ADDRESS, src_eid, @simple_msglib, 0); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::msglib_manager::EUNSUPPORTED_SRC_EID)] + fun test_set_receive_library_fails_if_library_does_not_support_eid() { + init_for_test(); + let src_eid = 1; + // only enable send side, not receive side + uln_302::configuration_tests::enable_send_eid_for_test(src_eid); + + // Set the OApp receive library to a library that does not support the EID + // Note this would fail in either case, because the default is not set (and could not be set, because it would + // fail for the same reason) + set_receive_library(OAPP_ADDRESS, src_eid, @uln_302, 0); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::msglib_manager::ERECEIVE_LIB_NOT_SET)] + fun test_set_receive_library_fails_if_trying_to_unset_library_that_is_not_set() { + init_for_test(); + let src_eid = 1; + set_default_receive_library(src_eid, @simple_msglib, 0); + + // Attempt to unset without having already set + set_receive_library(OAPP_ADDRESS, src_eid, @0x0, 0); + } + + #[test] + #[expected_failure( + abort_code = endpoint_v2::msglib_manager::ECANNOT_SET_GRACE_PERIOD_ON_RECEIVE_LIBRARY_UNSET + )] + fun test_set_receive_library_aborts_if_provided_a_grace_period_time_for_unset() { + init_for_test(); + let src_eid = 1; + set_default_receive_library(src_eid, @simple_msglib, 0); + + set_receive_library(OAPP_ADDRESS, src_eid, @simple_msglib, 0); + + // Attempt to unset with a grace period + set_receive_library(OAPP_ADDRESS, src_eid, @0x0, 10); + } + + #[test] + fun test_matches_default_receive_library() { + init_for_test(); + + let src_eid = 2; + uln_302::configuration_tests::enable_receive_eid_for_test(src_eid); + set_default_receive_library(src_eid, @simple_msglib, 0); + assert!(was_event_emitted(&default_receive_library_set_event(src_eid, @simple_msglib)), 0); + + // No OApp specific config set + assert!(matches_default_receive_library(src_eid, @simple_msglib), 0); + + // Update default without grace period + set_default_receive_library(src_eid, @uln_302, 0); + assert!(was_event_emitted(&default_receive_library_set_event(src_eid, @uln_302)), 1); + assert!(!matches_default_receive_library(src_eid, @simple_msglib), 1); + assert!(matches_default_receive_library(src_eid, @uln_302), 2); + + // Update default with grace period + set_default_receive_library(src_eid, @blocked_msglib, 10); + assert!(was_event_emitted(&default_receive_library_set_event(src_eid, @blocked_msglib)), 2); + assert!(was_event_emitted(&default_receive_library_timeout_set_event(src_eid, @uln_302, 10)), 3); + set_block_height(9); + assert!(matches_default_receive_library(src_eid, @uln_302), 3); + assert!(matches_default_receive_library(src_eid, @blocked_msglib), 4); + + // Grace period expired + set_block_height(11); + assert!(!matches_default_receive_library(src_eid, @uln_302), 5); + assert!(matches_default_receive_library(src_eid, @blocked_msglib), 6); + } + + #[test] + fun test_is_valid_receive_library() { + init_for_test(); + uln_302::configuration_tests::enable_receive_eid_for_test(1); + + set_default_receive_library(1, @blocked_msglib, 0); + assert!(was_event_emitted(&default_receive_library_set_event(1, @blocked_msglib)), 0); + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @blocked_msglib), 0); + + // Set a new library for the OApp - if there is no grace period, the other is immediately invalid + set_receive_library(OAPP_ADDRESS, 1, @simple_msglib, 0); + assert!(was_event_emitted(&receive_library_set_event(OAPP_ADDRESS, 1, @simple_msglib)), 1); + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @simple_msglib), 0); + assert!(!is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @blocked_msglib), 0); + + // Updating with a grace period, leaves the prior one valid up to the grace period end time + set_receive_library(OAPP_ADDRESS, 1, @uln_302, 10); + assert!(was_event_emitted(&receive_library_set_event(OAPP_ADDRESS, 1, @uln_302)), 2); + assert!(was_event_emitted(&receive_library_timeout_set_event(OAPP_ADDRESS, 1, @simple_msglib, 10)), 3); + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @uln_302), 0); + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @simple_msglib), 0); + set_block_height(9); + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @uln_302), 0); + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @simple_msglib), 0); + + // After the grace period, only the new library is valid + set_block_height(10); + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @uln_302), 0); + assert!(!is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @simple_msglib), 0); + + // Unsetting for oapp + set_receive_library(OAPP_ADDRESS, 1, @0x0, 0); + assert!(was_event_emitted(&receive_library_set_event(OAPP_ADDRESS, 1, @0x0)), 4); + assert!(!is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @uln_302), 0); // prior immediately invalid + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @blocked_msglib), 0); // default also valid + + // Set again + set_receive_library(OAPP_ADDRESS, 1, @uln_302, 0); + assert!(was_event_emitted(&receive_library_set_event(OAPP_ADDRESS, 1, @uln_302)), 5); + assert!(!is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @blocked_msglib), 0); + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @uln_302), 0); + + // Unset again + set_receive_library(OAPP_ADDRESS, 1, @0x0, 0); + assert!(was_event_emitted(&receive_library_set_event(OAPP_ADDRESS, 1, @0x0)), 6); + assert!(!is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @uln_302), 0); // prior not valid + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @blocked_msglib), 0); // default valid + } + + #[test] + fun test_set_default_receive_library_timeout() { + init_for_test(); + + let src_eid = 2; + uln_302::configuration_tests::enable_receive_eid_for_test(src_eid); + + // t = 0 blocks + set_default_receive_library(src_eid, @blocked_msglib, 0); + assert!(was_event_emitted(&default_receive_library_set_event(src_eid, @blocked_msglib)), 0); + assert!(was_event_emitted(&default_receive_library_timeout_set_event(src_eid, @0x0, 0)), 3); + + // Set the grace period to 10 blocks + set_default_receive_library(src_eid, @simple_msglib, 10); + assert!(was_event_emitted(&default_receive_library_set_event(src_eid, @simple_msglib)), 1); + + // Grace period should be active + assert!(matches_default_receive_library(src_eid, @blocked_msglib), 0); + assert!(matches_default_receive_library(src_eid, @simple_msglib), 1); + let grace_period_config = store::get_default_receive_library_timeout(src_eid); + assert!(timeout::is_active(&grace_period_config), 2); + + // Reset the expiry to 20 blocks + set_default_receive_library_timeout(src_eid, @blocked_msglib, 20); + assert!(was_event_emitted(&default_receive_library_timeout_set_event(src_eid, @blocked_msglib, 20)), 3); + assert!(matches_default_receive_library(src_eid, @blocked_msglib), 2); + assert!(matches_default_receive_library(src_eid, @simple_msglib), 3); + grace_period_config = store::get_default_receive_library_timeout(src_eid); + assert!(timeout::is_active(&grace_period_config), 2); + + // t = 19 blocks: grace period should still be active + set_block_height(19); + assert!(matches_default_receive_library(src_eid, @blocked_msglib), 4); + assert!(matches_default_receive_library(src_eid, @simple_msglib), 5); + let grace_period_config = store::get_default_receive_library_timeout(src_eid); + assert!(timeout::is_active(&grace_period_config), 2); + + // t = 20 blocks (at the expiry): grace period should be expired + set_block_height(21); + assert!(!matches_default_receive_library(src_eid, @blocked_msglib), 6); + assert!(matches_default_receive_library(src_eid, @simple_msglib), 7); + let grace_period_config = store::get_default_receive_library_timeout(src_eid); + assert!(!timeout::is_active(&grace_period_config), 8); + + // Adding timeout at any point can revive the grace period and the fallback can be updated to any library + set_block_height(100); + set_default_receive_library_timeout(src_eid, @uln_302, 101); + assert!(was_event_emitted(&default_receive_library_timeout_set_event(src_eid, @uln_302, 101)), 9); + assert!(matches_default_receive_library(src_eid, @uln_302), 9); + assert!(matches_default_receive_library(src_eid, @simple_msglib), 10); + let grace_period_config = store::get_default_receive_library_timeout(src_eid); + assert!(timeout::is_active(&grace_period_config), 11); + + // After the timeout ends, the fallback should be invlid + set_block_height(1002); + assert!(!matches_default_receive_library(src_eid, @uln_302), 12); + assert!(matches_default_receive_library(src_eid, @simple_msglib), 13); + let grace_period_config = store::get_default_receive_library_timeout(src_eid); + assert!(!timeout::is_active(&grace_period_config), 14); + + // Clear the grace period config + // This should not fail despite the lib being unknown + set_default_receive_library_timeout(src_eid, @0x9999, 0); + assert!(was_event_emitted(&default_receive_library_timeout_set_event(src_eid, @0x9999, 0)), 15); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::msglib_manager::EEXPIRY_IS_IN_PAST)] + fun test_set_default_receive_library_timeout_fails_if_in_the_past() { + init_for_test(); + let src_eid = 2; + uln_302::configuration_tests::enable_receive_eid_for_test(src_eid); + set_default_receive_library(src_eid, @simple_msglib, 0); + + set_block_height(20); + + // Attempt to set a grace period that is in the past + set_default_receive_library_timeout(src_eid, @simple_msglib, 10); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::msglib_manager::EUNSUPPORTED_SRC_EID)] + fun test_set_default_receive_library_timeout_fails_if_msglib_doesnt_support_eid() { + init_for_test(); + let src_eid = 2; + // only enable send side, not receive side + uln_302::configuration_tests::enable_send_eid_for_test(src_eid); + + // Set the grace period to 10 blocks + set_default_receive_library(src_eid, @uln_302, 10); + + // Reset the expiry to 20 blocks + set_default_receive_library_timeout(src_eid, @uln_302, 20); + } + + #[test] + fun test_set_receive_library_timeout() { + init_for_test(); + + let src_eid = 2; + uln_302::configuration_tests::enable_receive_eid_for_test(src_eid); + + set_default_receive_library(src_eid, @simple_msglib, 0); + + // Both default and receive should be valid after setting the oapp lib with a timeout + set_receive_library(OAPP_ADDRESS, src_eid, @uln_302, 0); + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, src_eid, @uln_302), 0); + assert!(!is_valid_receive_library_for_oapp(OAPP_ADDRESS, src_eid, @simple_msglib), 1); + // unrelated msglib + assert!(!is_valid_receive_library_for_oapp(OAPP_ADDRESS, src_eid, @blocked_msglib), 2); + + // Reset the grace period to 20 blocks from current block (0) and to a different lib + set_receive_library_timeout(OAPP_ADDRESS, src_eid, @blocked_msglib, 20); + assert!(was_event_emitted(&receive_library_timeout_set_event(OAPP_ADDRESS, src_eid, @blocked_msglib, 20)), 3); + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, src_eid, @blocked_msglib), 2); + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, src_eid, @uln_302), 3); + // old grace period msglib + assert!(!is_valid_receive_library_for_oapp(OAPP_ADDRESS, src_eid, @simple_msglib), 4); + + // t = 19 blocks (before the expiry): grace period should still be active + set_block_height(19); + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, src_eid, @blocked_msglib), 5); + + // t = 20 blocks (at the expiry): grace period should be expired + set_block_height(20); + assert!(!is_valid_receive_library_for_oapp(OAPP_ADDRESS, src_eid, @blocked_msglib), 6); + + // set a new timout + set_receive_library_timeout(OAPP_ADDRESS, src_eid, @simple_msglib, 30); + assert!(was_event_emitted(&receive_library_timeout_set_event(OAPP_ADDRESS, src_eid, @simple_msglib, 30)), 7); + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, src_eid, @simple_msglib), 8); + + // unset the timeout + set_receive_library_timeout(OAPP_ADDRESS, src_eid, @1234567, 0); + assert!(was_event_emitted(&receive_library_timeout_set_event(OAPP_ADDRESS, src_eid, @0x0, 0)), 8); + assert!(!is_valid_receive_library_for_oapp(OAPP_ADDRESS, src_eid, @simple_msglib), 8); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::msglib_manager::EEXPIRY_IS_IN_PAST)] + fun test_set_receive_library_timeout_fails_if_in_the_past() { + init_for_test(); + let src_eid = 2; + uln_302::configuration_tests::enable_receive_eid_for_test(src_eid); + + set_default_receive_library(src_eid, @simple_msglib, 0); + set_receive_library(OAPP_ADDRESS, src_eid, @simple_msglib, 0); + set_block_height(20); + + // Attempt to set a grace period that is in the past + set_receive_library_timeout(OAPP_ADDRESS, src_eid, @simple_msglib, 10); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::msglib_manager::ERECEIVE_LIB_NOT_SET)] + fun test_set_receive_library_timeout_fails_if_trying_to_unset_library_that_is_not_set() { + init_for_test(); + let src_eid = 2; + set_default_receive_library(src_eid, @simple_msglib, 0); + + // Attempt to unset without having already set + set_receive_library_timeout(OAPP_ADDRESS, src_eid, @0x0, 10); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::msglib_manager::ENO_TIMEOUT_TO_DELETE)] + fun test_set_receive_library_timeout_fails_if_no_grace_period_config_to_delete() { + init_for_test(); + let src_eid = 2; + + set_default_receive_library(src_eid, @simple_msglib, 0); + set_receive_library(OAPP_ADDRESS, src_eid, @blocked_msglib, 0); + // Attempt to unset without having already set + set_receive_library_timeout(OAPP_ADDRESS, src_eid, @0x0, 0); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::msglib_manager::ERECEIVE_LIB_NOT_SET)] + fun test_set_receive_library_timeout_fails_if_no_oapp_receive_library_already_set() { + init_for_test(); + let src_eid = 2; + set_default_receive_library(src_eid, @simple_msglib, 0); + + // Attempt to set a grace period without having already set + set_receive_library_timeout(OAPP_ADDRESS, src_eid, @simple_msglib, 10); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::msglib_manager::EUNSUPPORTED_SRC_EID)] + fun test_set_receive_library_timeout_fails_if_msglib_doesnt_support_eid() { + init_for_test(); + let src_eid = 2; + // only enable send side, not receive side + uln_302::configuration_tests::enable_send_eid_for_test(src_eid); + + set_default_receive_library(src_eid, @simple_msglib, 0); + set_receive_library(OAPP_ADDRESS, src_eid, @simple_msglib, 0); + + // Attempt to set a grace period to a library that does not support the EID + set_receive_library_timeout(OAPP_ADDRESS, src_eid, @uln_302, 10); + } + + #[test] + fun test_get_effective_receive_library() { + init_for_test(); + + let src_eid = 2; + uln_302::configuration_tests::enable_receive_eid_for_test(src_eid); + + set_default_receive_library(src_eid, @simple_msglib, 0); + let (lib, is_default) = get_effective_receive_library(OAPP_ADDRESS, src_eid); + assert!(lib == @simple_msglib, 0); + assert!(is_default, 1); + + // set for oapp + set_receive_library( + OAPP_ADDRESS, + src_eid, + @uln_302, + 0, + ); + + let (lib, is_default) = get_effective_receive_library(OAPP_ADDRESS, src_eid); + assert!(lib == @uln_302, 2); + assert!(!is_default, 3); + + // remove the oapp setting + set_receive_library( + OAPP_ADDRESS, + src_eid, + @0x0, + 0, + ); + + let (lib, is_default) = get_effective_receive_library(OAPP_ADDRESS, src_eid); + assert!(lib == @simple_msglib, 4); + assert!(is_default, 5); + } + + #[test] + fun test_is_valid_receive_library_for_oapp() { + init_for_test(); + + let src_eid = 2; + uln_302::configuration_tests::enable_receive_eid_for_test(src_eid); + + // An unregistered library is not ever valid + assert!(!is_valid_receive_library_for_oapp(OAPP_ADDRESS, src_eid, @0x983721498743), 0); + + // The default is valid if one is not set for OApp + set_default_receive_library(src_eid, @simple_msglib, 0); + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, src_eid, @simple_msglib), 0); + assert!(!is_valid_receive_library_for_oapp(OAPP_ADDRESS, src_eid, @uln_302), 1); + + // set for oapp + set_receive_library( + OAPP_ADDRESS, + src_eid, + @uln_302, + 0, + ); + + // Only the oapp setting is valid + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, src_eid, @uln_302), 3); + assert!(!is_valid_receive_library_for_oapp(OAPP_ADDRESS, src_eid, @simple_msglib), 4); + + // Switch with a grace period + set_receive_library( + OAPP_ADDRESS, + src_eid, + @simple_msglib, + 10, + ); + + // Both are valid during grace period + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, src_eid, @simple_msglib), 5); + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, src_eid, @uln_302), 6); + + // After the grace period, only the new one is valid + set_block_height(11); + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, src_eid, @simple_msglib), 7); + assert!(!is_valid_receive_library_for_oapp(OAPP_ADDRESS, src_eid, @uln_302), 8); + + // remove the oapp setting + set_receive_library( + OAPP_ADDRESS, + src_eid, + @0x0, + 0, + ); + + // After unsetting, only the default is valid + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, src_eid, @simple_msglib), 3); + assert!(!is_valid_receive_library_for_oapp(OAPP_ADDRESS, src_eid, @uln_302), 4); + } +} \ No newline at end of file diff --git a/packages/layerzero-v2/aptos/contracts/endpoint_v2/tests/internal/registration_tests.move b/packages/layerzero-v2/aptos/contracts/endpoint_v2/tests/internal/registration_tests.move new file mode 100644 index 00000000..d3add2a2 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/endpoint_v2/tests/internal/registration_tests.move @@ -0,0 +1,81 @@ +#[test_only] +module endpoint_v2::registration_tests { + use std::event::was_event_emitted; + use std::string::utf8; + + use endpoint_v2::registration::{ + composer_registered_event, + is_registered_composer, + is_registered_oapp, + oapp_registered_event, + register_composer, + register_oapp + }; + use endpoint_v2::store; + + #[test] + fun test_register() { + store::init_module_for_test(); + register_oapp(@1234, utf8(b"test_receive")); + assert!(was_event_emitted(&oapp_registered_event(@1234, utf8(b"test_receive"))), 0); + assert!(is_registered_oapp(@1234), 1); + assert!(!is_registered_composer(@1234), 2); + let receiver_module = store::lz_receive_module(@1234); + assert!(receiver_module == utf8(b"test_receive"), 0); + + register_composer(@2234, utf8(b"test_compose")); + assert!(was_event_emitted(&composer_registered_event(@2234, utf8(b"test_compose"))), 1); + assert!(!is_registered_oapp(@2234), 2); + assert!(is_registered_composer(@2234), 3); + let composer_module = store::lz_compose_module(@2234); + assert!(composer_module == utf8(b"test_compose"), 1); + + // register oapp in same address as composer + register_oapp(@2234, utf8(b"test_receive")); + assert!(was_event_emitted(&oapp_registered_event(@2234, utf8(b"test_receive"))), 2); + assert!(is_registered_oapp(@2234), 3); + assert!(is_registered_composer(@2234), 4); + let receiver_module = store::lz_receive_module(@2234); + assert!(receiver_module == utf8(b"test_receive"), 2); + + // register composer in same address as oapp + register_composer(@1234, utf8(b"test_compose")); + assert!(was_event_emitted(&composer_registered_event(@1234, utf8(b"test_compose"))), 3); + assert!(is_registered_oapp(@1234), 4); + assert!(is_registered_composer(@1234), 5); + let composer_module = store::lz_compose_module(@1234); + assert!(composer_module == utf8(b"test_compose"), 3); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::registration::EALREADY_REGISTERED)] + fun test_register_oapp_fails_if_already_registered() { + store::init_module_for_test(); + register_oapp(@1234, utf8(b"test")); + register_oapp(@1234, utf8(b"test")); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::registration::EALREADY_REGISTERED)] + fun test_register_composer_fails_if_already_registered() { + store::init_module_for_test(); + register_oapp(@1234, utf8(b"test")); + register_composer(@1234, utf8(b"test")); + register_composer(@1234, utf8(b"test")); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::store::EEMPTY_MODULE_NAME)] + fun test_register_fails_if_empty_lz_receive_module() { + endpoint_v2::store::init_module_for_test(); + register_oapp(@1234, utf8(b"")); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::store::EEMPTY_MODULE_NAME)] + fun test_register_fails_if_empty_lz_compose_module() { + endpoint_v2::store::init_module_for_test(); + register_oapp(@1234, utf8(b"receive")); + register_composer(@1234, utf8(b"")); + } +} diff --git a/packages/layerzero-v2/aptos/contracts/endpoint_v2/tests/internal/timeout_test_helpers.move b/packages/layerzero-v2/aptos/contracts/endpoint_v2/tests/internal/timeout_test_helpers.move new file mode 100644 index 00000000..5bbdf535 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/endpoint_v2/tests/internal/timeout_test_helpers.move @@ -0,0 +1,22 @@ +#[test_only] +module endpoint_v2::timeout_test_helpers { + #[test_only] + public fun setup_for_timeouts() { + let std = &std::account::create_account_for_test(@std); + std::block::initialize_for_test(std, 1_000_000); + std::reconfiguration::initialize_for_test(std); + let vm = &std::account::create_account_for_test(@0x0); + // genesis block + std::block::emit_writeset_block_event(vm, @1234); + } + + + #[test_only] + public fun set_block_height(target_height: u64) { + let vm = &std::account::create_signer_for_test(@0x0); + let start_height = std::block::get_current_block_height(); + for (i in start_height..target_height) { + std::block::emit_writeset_block_event(vm, @1234); + } + } +} diff --git a/packages/layerzero-v2/aptos/contracts/endpoint_v2/tests/test_helpers.move b/packages/layerzero-v2/aptos/contracts/endpoint_v2/tests/test_helpers.move new file mode 100644 index 00000000..7569265e --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/endpoint_v2/tests/test_helpers.move @@ -0,0 +1,143 @@ +#[test_only] +module endpoint_v2::test_helpers { + use std::account::create_signer_for_test; + use std::signer::address_of; + use std::timestamp; + + use endpoint_v2::admin; + use endpoint_v2_common::contract_identity::make_call_ref_for_test; + use endpoint_v2_common::native_token_test_helpers::initialize_native_token_for_test; + use msglib_types::worker_options::EXECUTOR_WORKER_ID; + use price_feed_module_0::price; + use price_feed_module_0::price::tag_price_with_eid; + use treasury::treasury; + use worker_common::worker_config; + + public fun setup_layerzero_for_test( + msglib_addr: address, // should be the address of whatever msglib we are trying to test + local_eid: u32, + remote_eid: u32, + ) { + let native_framework = &create_signer_for_test(@std); + let layerzero_admin = &create_signer_for_test(@layerzero_admin); + + // set global time + timestamp::set_time_has_started_for_testing(native_framework); + + // init + endpoint_v2_common::universal_config::init_module_for_test(local_eid); + admin::initialize_endpoint_for_test(); + simple_msglib::msglib::initialize_for_test(); + + // config/wire + endpoint_v2::msglib_manager::register_library(msglib_addr); + + // defaults + endpoint_v2::admin::set_default_send_library( + layerzero_admin, + remote_eid, + msglib_addr, + ); + endpoint_v2::admin::set_default_receive_library( + layerzero_admin, + remote_eid, + msglib_addr, + 0, + ); + } + + public fun setup_layerzero_for_test_uln(local_eid: u32, remote_eid: u32) { + initialize_native_token_for_test(); + let native_framework = &create_signer_for_test(@std); + let layerzero_admin = &create_signer_for_test(@layerzero_admin); + + // set global time + timestamp::set_time_has_started_for_testing(native_framework); + + // init + endpoint_v2_common::universal_config::init_module_for_test(local_eid); + admin::initialize_endpoint_for_test(); + uln_302::msglib::initialize_for_test(); + + // config/wire + endpoint_v2::msglib_manager::register_library(@uln_302); + + // uln + uln_302::configuration_tests::enable_send_eid_for_test(remote_eid); + uln_302::configuration_tests::enable_receive_eid_for_test(local_eid); + + // defaults + endpoint_v2::admin::set_default_send_library( + layerzero_admin, + remote_eid, + @uln_302, + ); + endpoint_v2::admin::set_default_receive_library( + layerzero_admin, + local_eid, + @uln_302, + 0, + ); + + let executor = @3002; + uln_302::admin::set_default_executor_config( + layerzero_admin, + remote_eid, + 100000, + executor, + ); + + // Executor config + worker_config::initialize_for_worker_test_only( + executor, + EXECUTOR_WORKER_ID(), + executor, + @13002, + vector[executor], + vector[@uln_302], + @executor_fee_lib_0, + ); + worker_config::set_executor_dst_config( + &make_call_ref_for_test(executor), + remote_eid, + 1000, + 1000, + 1000, + 1000, + 1000, + ); + // opt into using worker config for feelib routing + uln_302::msglib::set_worker_config_for_fee_lib_routing_opt_in(&create_signer_for_test(executor), true); + let feed_address = @1234; + worker_config::set_price_feed( + &make_call_ref_for_test(executor), + @price_feed_module_0, + feed_address, + ); + + // Treasury + treasury::init_module_for_test(); + + // Price feed + let feed_updater = &create_signer_for_test(@555); + price_feed_module_0::feeds::initialize(&create_signer_for_test(feed_address)); + price_feed_module_0::feeds::enable_feed_updater( + &create_signer_for_test(feed_address), + address_of(feed_updater) + ); + price_feed_module_0::feeds::set_price( + feed_updater, + feed_address, + price::serialize_eid_tagged_price_list(&vector[ + tag_price_with_eid( + remote_eid, + price::new_price(1, 1, 1), + ), + tag_price_with_eid( + local_eid, + price::new_price(1, 1, 1), + ), + ]), + ) + } +} diff --git a/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/Move.toml b/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/Move.toml new file mode 100644 index 00000000..2b406d98 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/Move.toml @@ -0,0 +1,21 @@ +[package] +name = "endpoint_v2_common" +version = "1.0.0" + +[addresses] +endpoint_v2_common = "_" +endpoint_v2 = "_" +layerzero_admin = "_" +layerzero_treasury_admin = "_" +native_token_metadata_address = "0xa" + +[dev-addresses] +endpoint_v2_common = "0x9098" +endpoint_v2 = "0x12345678" +layerzero_admin = "0x18943124" +layerzero_treasury_admin = "0x123456231423" + +[dependencies.AptosFramework] +git = "https://github.com/aptos-labs/aptos-framework.git" +rev = "mainnet" +subdir = "aptos-framework" diff --git a/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/sources/assert_no_duplicates.move b/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/sources/assert_no_duplicates.move new file mode 100644 index 00000000..81be6e71 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/sources/assert_no_duplicates.move @@ -0,0 +1,19 @@ +module endpoint_v2_common::assert_no_duplicates { + use std::vector; + + /// Assert that there are no duplicate addresses in the given vector. + public fun assert_no_duplicates(items: &vector) { + for (i in 0..vector::length(items)) { + for (j in 0..i) { + if (vector::borrow(items, i) == vector::borrow(items, j)) { + abort EDUPLICATE_ITEM + } + } + } + } + + + // ================================================== Error Codes ================================================= + + const EDUPLICATE_ITEM: u64 = 1; +} \ No newline at end of file diff --git a/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/sources/bytes32.move b/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/sources/bytes32.move new file mode 100644 index 00000000..fb328552 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/sources/bytes32.move @@ -0,0 +1,62 @@ +/// This is a wrapper for vector that enforces a length of 32 bytes +module endpoint_v2_common::bytes32 { + use std::bcs; + use std::from_bcs; + use std::vector; + + public inline fun ZEROS_32_BYTES(): vector { + x"0000000000000000000000000000000000000000000000000000000000000000" + } + + struct Bytes32 has store, drop, copy { + bytes: vector + } + + /// Returns a Bytes32 with all bytes set to zero + public fun zero_bytes32(): Bytes32 { + Bytes32 { bytes: ZEROS_32_BYTES() } + } + + /// Returns a Bytes32 with all bytes set to 0xff + public fun ff_bytes32(): Bytes32 { + Bytes32 { bytes: x"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" } + } + + /// Returns true if the given Bytes32 is all zeros + public fun is_zero(bytes32: &Bytes32): bool { + bytes32.bytes == ZEROS_32_BYTES() + } + + /// Converts a vector of bytes to a Bytes32 + /// The vector must be exactly 32 bytes long + public fun to_bytes32(bytes: vector): Bytes32 { + assert!(vector::length(&bytes) == 32, EINVALID_LENGTH); + Bytes32 { bytes } + } + + /// Converts a Bytes32 to a vector of bytes + public fun from_bytes32(bytes32: Bytes32): vector { + bytes32.bytes + } + + /// Converts an address to a Bytes32 + public fun from_address(addr: address): Bytes32 { + let bytes = bcs::to_bytes(&addr); + to_bytes32(bytes) + } + + /// Converts a Bytes32 to an address + public fun to_address(bytes32: Bytes32): address { + from_bcs::to_address(bytes32.bytes) + } + + /// Get the keccak256 hash of the given bytes + public fun keccak256(bytes: vector): Bytes32 { + let hash = std::aptos_hash::keccak256(bytes); + to_bytes32(hash) + } + + // ================================================== Error Codes ================================================= + + const EINVALID_LENGTH: u64 = 1; +} diff --git a/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/sources/contract_identity.move b/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/sources/contract_identity.move new file mode 100644 index 00000000..dbb0cdb9 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/sources/contract_identity.move @@ -0,0 +1,100 @@ +/// This module defines structs for contracts to authenticate themselves and prove authorization for specific actions +/// +/// The *ContractSigner* is stored by the contract after producing it in init_module(). It can only be generated one +/// time per address to defend against impersonation attacks. It is copiable however, so that it can be explicitly +/// shared if required. +/// +/// This can be used to generate a *CallRef* which is passed to the callee. There is a generic CallRef struct, +/// in which the `Target` should be substituted with a struct defined in the callee module that represents the +/// authorization type granted by the caller. +/// +/// The *DynamicCallRef* is used to pass the target contract address and an authorization byte-vector to the callee. +/// This is useful when the target contract address is not known at compile time. Upon receiving a DynamicCallRef, +/// the callee should use the `get_dynamic_call_ref_caller` function to identify the caller and verify the authorization +/// matches the expected authorization for the call. +/// +/// The Target and the (address, authorization) pair are useful to mitigate the risk of a call ref being used to perform +/// an action that was not intended by the caller. +module endpoint_v2_common::contract_identity { + use std::signer::address_of; + + struct SignerCreated has key {} + + /// Struct to persist the contract identity + /// Access to the contract signer provides universal access to the contract's authority and should be protected + struct ContractSigner has store, copy { contract_address: address } + + /// A Static call reference that can be used to identify the caller and statically validate the authorization + struct CallRef has drop { contract_address: address } + + /// A Dynamic call reference that can be used to identify the caller and validate the intended target contract + /// and authorization + struct DynamicCallRef has drop { contract_address: address, target_contract: address, authorization: vector } + + /// Creates a ContractSigner for the contract to store and use for generating ContractCallRefs + /// Make a record of the creation to prevent future contract signer creation + public fun create_contract_signer(account: &signer): ContractSigner { + assert!(!exists(address_of(account)), ECONTRACT_SIGNER_ALREADY_EXISTS); + move_to(account, SignerCreated {}); + ContractSigner { contract_address: address_of(account) } + } + + /// Destroys the contract signer - once destroyed the contract signer cannot be recreated using + /// `create_contract_signer`; however, any copies of the contract signer will continue to exist + public fun irrecoverably_destroy_contract_signer(contract_signer: ContractSigner) { + let ContractSigner { contract_address: _ } = contract_signer; + } + + /// Make a static call ref from a ContractSigner + /// Generally the target does not have to be specified as it can be inferred from the function signature it is + /// used with + public fun make_call_ref(contract: &ContractSigner): CallRef { + CallRef { contract_address: contract.contract_address } + } + + /// Get the calling contract address from a static CallRef + public fun get_call_ref_caller(call_ref: &CallRef): address { + call_ref.contract_address + } + + /// This function is used to create a ContractCallRef from a ContractSigner + public fun make_dynamic_call_ref( + contract: &ContractSigner, + target_contract: address, + authorization: vector, + ): DynamicCallRef { + DynamicCallRef { contract_address: contract.contract_address, target_contract, authorization } + } + + /// This function is used to get the calling contract address, while asserting that the recipient is the correct + /// receiver contract + public fun get_dynamic_call_ref_caller( + call_ref: &DynamicCallRef, + receiver_to_assert: address, + authorization_to_assert: vector, + ): address { + assert!(call_ref.target_contract == receiver_to_assert, ETARGET_CONTRACT_MISMATCH); + assert!(call_ref.authorization == authorization_to_assert, EAUTHORIZATION_MISMATCH); + call_ref.contract_address + } + + #[test_only] + public fun make_call_ref_for_test(contract_address: address): CallRef { + CallRef { contract_address } + } + + #[test_only] + public fun make_dynamic_call_ref_for_test( + contract_address: address, + target_contract: address, + authorization: vector, + ): DynamicCallRef { + DynamicCallRef { contract_address, target_contract, authorization } + } + + // ================================================== Error Codes ================================================= + + const EAUTHORIZATION_MISMATCH: u64 = 1; + const ECONTRACT_SIGNER_ALREADY_EXISTS: u64 = 2; + const ETARGET_CONTRACT_MISMATCH: u64 = 2; +} diff --git a/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/sources/guid.move b/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/sources/guid.move new file mode 100644 index 00000000..bffca9a3 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/sources/guid.move @@ -0,0 +1,15 @@ +/// This module provides the function to compute the GUID for a message +module endpoint_v2_common::guid { + use endpoint_v2_common::bytes32::{Self, Bytes32}; + use endpoint_v2_common::serde; + + public fun compute_guid(nonce: u64, src_eid: u32, sender: Bytes32, dst_eid: u32, receiver: Bytes32): Bytes32 { + let guid_bytes = vector[]; + serde::append_u64(&mut guid_bytes, nonce); + serde::append_u32(&mut guid_bytes, src_eid); + serde::append_bytes32(&mut guid_bytes, sender); + serde::append_u32(&mut guid_bytes, dst_eid); + serde::append_bytes32(&mut guid_bytes, receiver); + bytes32::keccak256(guid_bytes) + } +} diff --git a/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/sources/native_token.move b/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/sources/native_token.move new file mode 100644 index 00000000..a9cdcbb6 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/sources/native_token.move @@ -0,0 +1,21 @@ +/// This module provides a function to withdraw gas token using Coin methods. +/// +/// This is necessary for chains that have a Coin representation of the gas token. The coin::withdraw function is aware +/// of both Coin and FungibleAsset(@0xa) balances unlike the fungible_asset::withdraw function which is only +/// aware of FungibleAsset(@0xa) balances. +/// +/// This module should be swapped with native_token_fa.move for chains that have a fully FungibleAsset-based gas token. +module endpoint_v2_common::native_token { + use std::aptos_coin::AptosCoin; + use std::coin; + use std::fungible_asset::FungibleAsset; + + public fun balance(account: address): u64 { + coin::balance(account) + } + + public fun withdraw(account: &signer, amount: u64): FungibleAsset { + let coin = coin::withdraw(move account, amount); + coin::coin_to_fungible_asset(coin) + } +} diff --git a/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/sources/packet_raw.move b/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/sources/packet_raw.move new file mode 100644 index 00000000..4f5c00be --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/sources/packet_raw.move @@ -0,0 +1,36 @@ +/// This module defines a semantic wrapper for raw packet (and packet header) bytes +/// This format is agnostic to the codec used in the Message Library, but it is valuable to provide clarity and type +/// safety for Packets and headers in the codebase +module endpoint_v2_common::packet_raw { + use std::vector; + + struct RawPacket has drop, copy, store { + packet: vector, + } + + /// Create a vector from a RawPacket + public fun bytes_to_raw_packet(packet_bytes: vector): RawPacket { + RawPacket { packet: packet_bytes } + } + + /// Borrow the packet bytes from a RawPacket + public fun borrow_packet_bytes(raw_packet: &RawPacket): &vector { + &raw_packet.packet + } + + /// Borrow the packet bytes mutably from a RawPacket + public fun borrow_packet_bytes_mut(raw_packet: &mut RawPacket): &mut vector { + &mut raw_packet.packet + } + + /// Move the packet bytes from a RawPacket + public fun get_packet_bytes(raw_packet: RawPacket): vector { + let RawPacket { packet } = raw_packet; + packet + } + + /// Get the packet length of a RawPacket + public fun length(raw_packet: &RawPacket): u64 { + vector::length(&raw_packet.packet) + } +} diff --git a/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/sources/packet_v1_codec.move b/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/sources/packet_v1_codec.move new file mode 100644 index 00000000..24770ada --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/sources/packet_v1_codec.move @@ -0,0 +1,179 @@ +module endpoint_v2_common::packet_v1_codec { + use std::vector; + + use endpoint_v2_common::bytes32::{Self, Bytes32}; + use endpoint_v2_common::packet_raw::{Self, borrow_packet_bytes, bytes_to_raw_packet, RawPacket}; + use endpoint_v2_common::serde; + + const PACKET_VERSION: u8 = 1; + + // Header Offsets + const VERSION_OFFSET: u64 = 0; + const NONCE_OFFSET: u64 = 1; + const SRC_EID_OFFSET: u64 = 9; + const SENDER_OFFSET: u64 = 13; + const DST_EID_OFFSET: u64 = 45; + const RECEIVER_OFFSET: u64 = 49; + const HEADER_LENGTH: u64 = 81; + + // Message Offsets + const GUID_OFFSET: u64 = 81; + const MESSAGE_OFFSET: u64 = 113; + + /// Build a new packet with the given parameters + public fun new_packet_v1( + src_eid: u32, + sender: Bytes32, + dst_eid: u32, + receiver: Bytes32, + nonce: u64, + guid: Bytes32, + message: vector, + ): RawPacket { + let bytes = new_packet_v1_header_only_bytes(src_eid, sender, dst_eid, receiver, nonce); + serde::append_bytes32(&mut bytes, guid); + vector::append(&mut bytes, message); + bytes_to_raw_packet(bytes) + } + + /// Build a new packet (header only) with the given parameters + public fun new_packet_v1_header_only( + src_eid: u32, + sender: Bytes32, + dst_eid: u32, + receiver: Bytes32, + nonce: u64, + ): RawPacket { + bytes_to_raw_packet( + new_packet_v1_header_only_bytes(src_eid, sender, dst_eid, receiver, nonce) + ) + } + + /// Build a new packet (header only) with the given parameters and output byte form + public fun new_packet_v1_header_only_bytes( + src_eid: u32, + sender: Bytes32, + dst_eid: u32, + receiver: Bytes32, + nonce: u64, + ): vector { + let bytes = vector[]; + serde::append_u8(&mut bytes, PACKET_VERSION); + serde::append_u64(&mut bytes, nonce); + serde::append_u32(&mut bytes, src_eid); + serde::append_bytes32(&mut bytes, sender); + serde::append_u32(&mut bytes, dst_eid); + serde::append_bytes32(&mut bytes, receiver); + bytes + } + + /// Extract only the Packet header part from a RawPacket + public fun extract_header(raw_packet: &RawPacket): RawPacket { + if (packet_raw::length(raw_packet) > GUID_OFFSET) { + let packet_header_bytes = vector::slice(borrow_packet_bytes(raw_packet), 0, GUID_OFFSET); + bytes_to_raw_packet(packet_header_bytes) + } else { + *raw_packet + } + } + + /// Check that the packet is the expected version for this codec (version 1) + public fun is_valid_version(raw_packet: &RawPacket): bool { + get_version(raw_packet) == PACKET_VERSION + } + + /// Get the version of the packet + public fun get_version(raw_packet: &RawPacket): u8 { + let packet_bytes = packet_raw::borrow_packet_bytes(raw_packet); + serde::extract_u8(packet_bytes, &mut VERSION_OFFSET) + } + + /// Get the nonce of the packet + public fun get_nonce(raw_packet: &RawPacket): u64 { + let packet_bytes = packet_raw::borrow_packet_bytes(raw_packet); + serde::extract_u64(packet_bytes, &mut NONCE_OFFSET) + } + + /// Get the source EID of the packet + public fun get_src_eid(raw_packet: &RawPacket): u32 { + let packet_bytes = packet_raw::borrow_packet_bytes(raw_packet); + serde::extract_u32(packet_bytes, &mut SRC_EID_OFFSET) + } + + /// Get the sender of the packet + public fun get_sender(raw_packet: &RawPacket): Bytes32 { + let packet_bytes = packet_raw::borrow_packet_bytes(raw_packet); + serde::extract_bytes32(packet_bytes, &mut SENDER_OFFSET) + } + + /// Get the destination EID of the packet + public fun get_dst_eid(raw_packet: &RawPacket): u32 { + let packet_bytes = packet_raw::borrow_packet_bytes(raw_packet); + serde::extract_u32(packet_bytes, &mut DST_EID_OFFSET) + } + + /// Get the receiver of the packet + public fun get_receiver(raw_packet: &RawPacket): Bytes32 { + let packet_bytes = packet_raw::borrow_packet_bytes(raw_packet); + serde::extract_bytes32(packet_bytes, &mut RECEIVER_OFFSET) + } + + /// Get the GUID of the packet + public fun get_guid(raw_packet: &RawPacket): Bytes32 { + let packet_bytes = packet_raw::borrow_packet_bytes(raw_packet); + serde::extract_bytes32(packet_bytes, &mut GUID_OFFSET) + } + + /// Get the length of the message in the packet + public fun get_message_length(raw_packet: &RawPacket): u64 { + let packet_length = packet_raw::length(raw_packet); + packet_length - MESSAGE_OFFSET + } + + /// Get the message of the packet + public fun get_message(raw_packet: &RawPacket): vector { + let packet_bytes = packet_raw::borrow_packet_bytes(raw_packet); + serde::extract_bytes_until_end(packet_bytes, &mut MESSAGE_OFFSET) + } + + /// Get the payload of the packet + public fun get_payload_hash(packet: &RawPacket): Bytes32 { + let guid = get_guid(packet); + let message = get_message(packet); + compute_payload_hash(guid, message) + } + + /// Compute the payload of the packet + public fun compute_payload(guid: Bytes32, message: vector): vector { + let payload = vector[]; + vector::append(&mut payload, bytes32::from_bytes32(guid)); + vector::append(&mut payload, message); + payload + } + + /// Compute the payload hash of the packet + public fun compute_payload_hash(guid: Bytes32, message: vector): Bytes32 { + bytes32::keccak256(compute_payload(guid, message)) + } + + /// Assert that the packet is a valid packet for the local EID + public fun assert_receive_header(packet_header: &RawPacket, local_eid: u32) { + let packet_bytes = packet_raw::borrow_packet_bytes(packet_header); + assert!(vector::length(packet_bytes) == HEADER_LENGTH, EINVALID_PACKET_HEADER); + assert!(is_valid_version(packet_header), EINVALID_PACKET_VERSION); + assert!(get_dst_eid(packet_header) == local_eid, EINVALID_EID) + } + + public fun is_receive_header_valid(packet_header: &RawPacket, local_eid: u32): bool { + let packet_bytes = packet_raw::borrow_packet_bytes(packet_header); + vector::length(packet_bytes) == HEADER_LENGTH + && is_valid_version(packet_header) + && get_dst_eid(packet_header) == local_eid + } + + // ================================================== Error Codes ================================================= + + const EINVALID_EID: u64 = 1; + const EINVALID_PACKET_HEADER: u64 = 2; + const EINVALID_PACKET_VERSION: u64 = 3; +} \ No newline at end of file diff --git a/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/sources/send_packet.move b/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/sources/send_packet.move new file mode 100644 index 00000000..5756bc44 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/sources/send_packet.move @@ -0,0 +1,89 @@ +/// This module defines the internal send packet structure. This is the packet structure that is used internally by the +/// endpoint_v2 to communicate with the message libraries. Unlike the packet_v1_codec, which is may be upgraded for +/// use with a future message library, the internal packet is a fixed structure and is unchangable even with future +/// message libraries +module endpoint_v2_common::send_packet { + use std::vector; + + use endpoint_v2_common::bytes32::Bytes32; + use endpoint_v2_common::guid; + + struct SendPacket has copy, drop, store { + nonce: u64, + src_eid: u32, + sender: Bytes32, + dst_eid: u32, + receiver: Bytes32, + guid: Bytes32, + message: vector, + } + + /// Create a new send packet + public fun new_send_packet( + nonce: u64, + src_eid: u32, + sender: Bytes32, + dst_eid: u32, + receiver: Bytes32, + message: vector, + ): SendPacket { + let guid = guid::compute_guid(nonce, src_eid, sender, dst_eid, receiver); + SendPacket { + nonce, + src_eid, + sender, + dst_eid, + receiver, + guid, + message, + } + } + + /// Unpack the send packet into its components + public fun unpack_send_packet( + packet: SendPacket, + ): (u64, u32, Bytes32, u32, Bytes32, Bytes32, vector) { + let SendPacket { + nonce, + src_eid, + sender, + dst_eid, + receiver, + guid, + message, + } = packet; + (nonce, src_eid, sender, dst_eid, receiver, guid, message) + } + + public fun get_nonce(packet: &SendPacket): u64 { + packet.nonce + } + + public fun get_src_eid(packet: &SendPacket): u32 { + packet.src_eid + } + + public fun get_sender(packet: &SendPacket): Bytes32 { + packet.sender + } + + public fun get_dst_eid(packet: &SendPacket): u32 { + packet.dst_eid + } + + public fun get_receiver(packet: &SendPacket): Bytes32 { + packet.receiver + } + + public fun get_guid(packet: &SendPacket): Bytes32 { + packet.guid + } + + public fun borrow_message(packet: &SendPacket): &vector { + &packet.message + } + + public fun get_message_length(packet: &SendPacket): u64 { + vector::length(&packet.message) + } +} diff --git a/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/sources/serde.move b/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/sources/serde.move new file mode 100644 index 00000000..9aa73c4a --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/sources/serde.move @@ -0,0 +1,200 @@ +/// Serialization and deserialization utilities +module endpoint_v2_common::serde { + use std::bcs::to_bytes; + use std::from_bcs::to_address; + use std::vector; + + use endpoint_v2_common::bytes32::{Self, Bytes32}; + + /// Extract a uint from a vector of bytes starting at `position`, up to 8 bytes (u64). + /// Position will be incremented to the position after the end of the read + /// This decodes in big-endian format, with the most significant byte first + public fun extract_uint(input: &vector, position: &mut u64, bytes: u8): u64 { + let result: u64 = 0; + for (i in 0..bytes) { + let byte: u8 = *vector::borrow(input, *position); + result = result + ((byte as u64) << ((bytes - i - 1) * 8)); + *position = *position + 1; + }; + result + } + + /// Extract a u8 from a vector of bytes starting at `position` (position will be updated to the end of read) + public inline fun extract_u8(input: &vector, position: &mut u64): u8 { + (extract_uint(input, position, 1) as u8) + } + + /// Extract a u16 from a vector of bytes starting at `position` (position will be updated to the end of read) + public inline fun extract_u16(input: &vector, position: &mut u64): u16 { + (extract_uint(input, position, 2) as u16) + } + + /// Extract a u32 from a vector of bytes starting at `position` (position will be updated to the end of read) + public inline fun extract_u32(input: &vector, position: &mut u64): u32 { + (extract_uint(input, position, 4) as u32) + } + + /// Extract a u64 from a vector of bytes starting at `position` (position will be updated to the end of read) + public inline fun extract_u64(input: &vector, position: &mut u64): u64 { + extract_uint(input, position, 8) + } + + /// Extract a u128 from a vector of bytes starting at `position` (position will be updated to the end of read) + /// This function does not use extract_uint because it is more efficient to handle u128 as a special case + public fun extract_u128(input: &vector, position: &mut u64): u128 { + let result: u128 = 0; + for (i in 0..16) { + let byte: u8 = *vector::borrow(input, *position); + result = result + ((byte as u128) << ((16 - i - 1) * 8)); + *position = *position + 1; + }; + result + } + + /// Extract a u256 from a vector of bytes starting at `position` (position will be updated to the end of read) + /// This function does not use extract_uint because it is more efficient to handle u256 as a special case + public fun extract_u256(input: &vector, position: &mut u64): u256 { + let result: u256 = 0; + for (i in 0..32) { + let byte: u8 = *vector::borrow(input, *position); + result = result + ((byte as u256) << ((32 - i - 1) * 8)); + *position = *position + 1; + }; + result + } + + /// Extract a vector of bytes from a vector starting at `position` and ending at `position + len` + /// This will update the position to `position + len` + public fun extract_fixed_len_bytes(input: &vector, position: &mut u64, len: u64): vector { + let result = vector::slice(input, *position, *position + len); + *position = *position + len; + result + } + + /// Extract a vector of bytes from a vector starting at `position` and ending at the end of the vector + public fun extract_bytes_until_end(input: &vector, position: &mut u64): vector { + let len = vector::length(input); + let result = vector::slice(input, *position, len); + *position = len; + result + } + + /// Extract an address from a vector of bytes + public fun extract_address(input: &vector, position: &mut u64): address { + let bytes = vector::slice(input, *position, *position + 32); + *position = *position + 32; + to_address(bytes) + } + + /// Append a uint to a vector of bytes, up to 8 bytes (u64) + public fun append_uint(target: &mut vector, value: u64, bytes: u8) { + for (i in 0..bytes) { + let byte: u8 = (((value >> (8 * (bytes - i - 1))) & 0xFF) as u8); + vector::push_back(target, byte); + }; + } + + /// Append a u8 to a vector of bytes + public inline fun append_u8(target: &mut vector, value: u8) { append_uint(target, (value as u64), 1); } + + /// Append a u16 to a vector of bytes + public inline fun append_u16(target: &mut vector, value: u16) { append_uint(target, (value as u64), 2); } + + /// Append a u32 to a vector of bytes + public inline fun append_u32(target: &mut vector, value: u32) { append_uint(target, (value as u64), 4); } + + /// Append a u64 to a vector of bytes + public inline fun append_u64(target: &mut vector, value: u64) { append_uint(target, value, 8); } + + + /// Append a u128 to a vector of bytes + /// This function does not use append_uint because it is more efficient to handle u128 as a special case + public fun append_u128(target: &mut vector, value: u128) { + for (i in 0..16) { + let byte: u8 = (((value >> (8 * (16 - i - 1))) & 0xFF) as u8); + vector::push_back(target, byte); + } + } + + /// Append a u256 to a vector of bytes + /// This function does not use append_uint because it is more efficient to handle u256 as a special case + public fun append_u256(target: &mut vector, value: u256) { + for (i in 0..32) { + let byte: u8 = (((value >> (8 * (32 - i - 1))) & 0xFF) as u8); + vector::push_back(target, byte); + } + } + + /// Get the remaining length of the byte-vector starting at `position` + public fun get_remaining_length(input: &vector, position: u64): u64 { + vector::length(input) - position + } + + /// Pad the bytes provided with zeros to the left make it the target size + /// This will throw if the length of the provided vector surpasses the target size + public fun pad_zero_left(bytes: vector, target_size: u64): vector { + let bytes_size = vector::length(&bytes); + assert!(target_size >= bytes_size, EINVALID_LENGTH); + let output = vector[]; + let padding_needed = target_size - bytes_size; + for (i in 0..padding_needed) { + vector::push_back(&mut output, 0); + }; + vector::append(&mut output, bytes); + output + } + + /// Append a byte vector to the of a buffer + public inline fun append_bytes(buf: &mut vector, bytes: vector) { + vector::append(buf, bytes); + } + + /// Append an address to a vector of bytes + public fun append_address(buf: &mut vector, addr: address) { + let bytes = to_bytes(&addr); + vector::append(buf, bytes); + } + + /// Append a bytes32 to a vector of bytes + public fun append_bytes32(buf: &mut vector, bytes32: Bytes32) { + vector::append(buf, bytes32::from_bytes32(bytes32)); + } + + /// Extract a bytes32 from a vector of bytes + public fun extract_bytes32(input: &vector, position: &mut u64): Bytes32 { + let bytes = vector::slice(input, *position, *position + 32); + *position = *position + 32; + bytes32::to_bytes32(bytes) + } + + /// This function flattens a vector of byte-vectors into a single byte-vector + public inline fun flatten(input: vector>): vector { + let result = vector[]; + vector::for_each(input, |element| { + vector::append(&mut result, element); + }); + result + } + + /// This function creates a vector of `Element` by applying the function `f` to each element in the range [0, count) + public inline fun map_count(count: u64, f: |u64|Element): vector { + let vec = vector[]; + for (i in 0..count) { + vector::push_back(&mut vec, f(i)); + }; + vec + } + + /// This function create a bytes vector by applying the function `f` to a &mut buffer empty vector + /// this is useful for directly creating a bytes vector from an append_*() function in a single line + /// for example: let eighteen = bytes_of(|buf| append_u8(buf, 0x12)) + public inline fun bytes_of(f: |&mut vector|()): vector { + let buf = vector[]; + f(&mut buf); + buf + } + + // ================================================== Error Codes ================================================= + + const EINVALID_LENGTH: u64 = 1; +} \ No newline at end of file diff --git a/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/sources/universal_config.move b/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/sources/universal_config.move new file mode 100644 index 00000000..43438be4 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/sources/universal_config.move @@ -0,0 +1,167 @@ +/// This module provides a groud truth for EID and ZRO Metadata address +module endpoint_v2_common::universal_config { + use std::event::emit; + use std::fungible_asset::{Self, FungibleAsset, Metadata}; + use std::object::{Self, Object}; + use std::option::{Self, Option}; + use std::signer::address_of; + + #[test_only] + use std::account::create_signer_for_test; + + #[test_only] + friend endpoint_v2_common::universal_config_tests; + + struct UniversalStore has key { + // The EID for this endpoint + eid: u32, + // The ZRO metadata if it has been set + zro_data: Option>, + // Whether the ZRO address is locked. Once locked the zro metadata cannot be changed + zro_locked: bool, + } + + /// Initialize the UniversalStore must be called by endpoint_v2_common + public entry fun initialize(admin: &signer, eid: u32) acquires UniversalStore { + assert_admin(address_of(move admin)); + assert!(universal_store().eid == 0, EALREADY_INITIALIZED); + universal_store_mut().eid = eid; + } + + fun init_module(account: &signer) { + move_to(account, UniversalStore { + eid: 0, + zro_data: option::none(), + zro_locked: false, + }); + } + + #[test_only] + public fun init_module_for_test(eid: u32) acquires UniversalStore { + init_module(&create_signer_for_test(@endpoint_v2_common)); + initialize(&create_signer_for_test(@layerzero_admin), eid); + } + + #[view] + /// Get the EID for the V2 Endpoint + public fun eid(): u32 acquires UniversalStore { + universal_store().eid + } + + /// Set the ZRO address + /// @param account: The layerzero admin account signer + /// @param zro_address: The address of the ZRO metadata (@0x0 to unset) + public entry fun set_zro_address(account: &signer, zro_address: address) acquires UniversalStore { + assert_admin(address_of(move account)); + assert!(!universal_store().zro_locked, EZRO_ADDRESS_LOCKED); + + if (zro_address == @0x0) { + // Unset the ZRO address + assert!(option::is_some(&universal_store().zro_data), ENO_CHANGE); + let zro_data_store = &mut universal_store_mut().zro_data; + *zro_data_store = option::none(); + } else { + // Set the ZRO address + assert!(object::object_exists(zro_address), EINVALID_ZRO_ADDRESS); + if (has_zro_metadata()) { + assert!(get_zro_address() != zro_address, ENO_CHANGE); + }; + let zro_metadata = object::address_to_object(zro_address); + let zro_data_store = &mut universal_store_mut().zro_data; + *zro_data_store = option::some(zro_metadata); + }; + + emit(ZroMetadataSet { zro_address }); + } + + /// Lock the ZRO address so it can no longer be set or unset + public entry fun lock_zro_address(account: &signer) acquires UniversalStore { + assert_admin(address_of(move account)); + assert!(!universal_store().zro_locked, ENO_CHANGE); + assert!(option::is_some(&universal_store().zro_data), EZRO_ADDRESS_NOT_SET); + + let locked_store = &mut universal_store_mut().zro_locked; + *locked_store = true; + + emit(ZroMetadataLocked {}); + } + + #[view] + /// Check if ZRO address is set + public fun has_zro_metadata(): bool acquires UniversalStore { + option::is_some(&universal_store().zro_data) + } + + #[view] + /// Get the ZRO address + public fun get_zro_address(): address acquires UniversalStore { + object::object_address(&get_zro_metadata()) + } + + #[view] + /// Get the ZRO metadata + public fun get_zro_metadata(): Object acquires UniversalStore { + assert_zro_metadata_set(); + *option::borrow(&universal_store().zro_data) + } + + /// Assert that the ZRO metadata is set + public fun assert_zro_metadata_set() acquires UniversalStore { + assert!(has_zro_metadata(), EINVALID_ZRO_ADDRESS); + } + + /// Check if the given FungibleAsset is the ZRO asset + public fun is_zro(fa: &FungibleAsset): bool acquires UniversalStore { + if (!has_zro_metadata()) { return false }; + let metadata = fungible_asset::asset_metadata(fa); + metadata == get_zro_metadata() + } + + /// Check if the given Metadata is the ZRO metadata + public fun is_zro_metadata(metadata: Object): bool acquires UniversalStore { + if (!has_zro_metadata()) { return false }; + metadata == get_zro_metadata() + } + + // ==================================================== Helpers =================================================== + + inline fun assert_admin(admin: address) { + assert!(admin == @layerzero_admin, EUNAUTHORIZED); + } + + inline fun universal_store(): &UniversalStore { borrow_global(@endpoint_v2_common) } + + inline fun universal_store_mut(): &mut UniversalStore { borrow_global_mut(@endpoint_v2_common) } + + #[test_only] + public fun change_eid_for_test(eid: u32) acquires UniversalStore { universal_store_mut().eid = eid; } + + // ==================================================== Events ==================================================== + + #[event] + struct ZroMetadataSet has drop, store { + zro_address: address, + } + + #[event] + struct ZroMetadataLocked has drop, store {} + + #[test_only] + public fun zro_metadata_set(zro_address: address): ZroMetadataSet { + ZroMetadataSet { zro_address } + } + + #[test_only] + public fun zro_metadata_locked(): ZroMetadataLocked { + ZroMetadataLocked {} + } + + // ================================================== Error Codes ================================================= + + const EALREADY_INITIALIZED: u64 = 1; + const EUNAUTHORIZED: u64 = 2; + const EINVALID_ZRO_ADDRESS: u64 = 3; + const ENO_CHANGE: u64 = 4; + const EZRO_ADDRESS_NOT_SET: u64 = 5; + const EZRO_ADDRESS_LOCKED: u64 = 6; +} diff --git a/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/tests/assert_no_duplicates_tests.move b/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/tests/assert_no_duplicates_tests.move new file mode 100644 index 00000000..ecc9680d --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/tests/assert_no_duplicates_tests.move @@ -0,0 +1,17 @@ +#[test_only] +module endpoint_v2_common::assert_no_duplicates_tests { + use endpoint_v2_common::assert_no_duplicates::assert_no_duplicates; + + #[test] + fun does_not_abort_if_no_duplicate_addresses() { + let addresses = vector
[@1, @2, @3, @4]; + assert_no_duplicates(&addresses); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2_common::assert_no_duplicates::EDUPLICATE_ITEM)] + fun aborts_if_duplicate_addresses() { + let addresses = vector
[@1, @2, @3, @1, @5, @6]; + assert_no_duplicates(&addresses); + } +} diff --git a/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/tests/bytes_32_tests.move b/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/tests/bytes_32_tests.move new file mode 100644 index 00000000..b6e735a9 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/tests/bytes_32_tests.move @@ -0,0 +1,33 @@ +#[test_only] +module endpoint_v2_common::bytes_32_tests { + use endpoint_v2_common::bytes32::{from_address, from_bytes32, is_zero, to_address, to_bytes32, zero_bytes32}; + + #[test] + fun test_zero_bytes32() { + let zero_bytes32 = zero_bytes32(); + assert!(is_zero(&zero_bytes32), 0); + + let zero_manual = to_bytes32(x"0000000000000000000000000000000000000000000000000000000000000000"); + assert!(is_zero(&zero_manual), 0); + + let non_zero = to_bytes32(x"0000000000000000000000000000000000000000000000000000000000000001"); + assert!(!is_zero(&non_zero), 0); + } + + #[test] + fun test_from_to_address() { + let addr = @0x12345; + let bytes32 = from_address(addr); + assert!(from_bytes32(bytes32) == x"0000000000000000000000000000000000000000000000000000000000012345", 0); + + let addr2 = to_address(bytes32); + assert!(addr == addr2, 0); + } + + #[test] + fun test_to_from_bytes32() { + let bytes = x"1234560000000000000000000000000000000000000000000000000000123456"; + let bytes32 = to_bytes32(bytes); + assert!(from_bytes32(bytes32) == bytes, 0); + } +} diff --git a/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/tests/contract_identity_tests.move b/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/tests/contract_identity_tests.move new file mode 100644 index 00000000..04af0c10 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/tests/contract_identity_tests.move @@ -0,0 +1,61 @@ +#[test_only] +module endpoint_v2_common::contract_identity_tests { + use std::account::create_signer_for_test; + + use endpoint_v2_common::contract_identity; + use endpoint_v2_common::contract_identity::irrecoverably_destroy_contract_signer; + + #[test] + fun test_contract_identity() { + let account = create_signer_for_test(@1234); + let contract_signer = contract_identity::create_contract_signer(&account); + let call_ref = contract_identity::make_dynamic_call_ref(&contract_signer, @5555, b"general"); + let caller = contract_identity::get_dynamic_call_ref_caller(&call_ref, @5555, b"general"); + assert!(caller == @1234, 0); + irrecoverably_destroy_contract_signer(contract_signer); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2_common::contract_identity::ECONTRACT_SIGNER_ALREADY_EXISTS)] + fun test_contract_identity_fails_if_already_exists() { + let account = create_signer_for_test(@1234); + let cs1 = contract_identity::create_contract_signer(&account); + let cs2 = contract_identity::create_contract_signer(&account); + irrecoverably_destroy_contract_signer(cs1); + irrecoverably_destroy_contract_signer(cs2); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2_common::contract_identity::ETARGET_CONTRACT_MISMATCH)] + fun test_get_caller_fails_if_incorrect_receiver() { + let account = create_signer_for_test(@1234); + let contract_signer = contract_identity::create_contract_signer(&account); + let call_ref = contract_identity::make_dynamic_call_ref(&contract_signer, @5555, b"general"); + // shoud be @5555 + contract_identity::get_dynamic_call_ref_caller(&call_ref, @6666, b"general"); + irrecoverably_destroy_contract_signer(contract_signer); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2_common::contract_identity::EAUTHORIZATION_MISMATCH)] + fun test_get_caller_fails_if_incorrect_authorization() { + let account = create_signer_for_test(@1234); + let contract_signer = contract_identity::create_contract_signer(&account); + let call_ref = contract_identity::make_dynamic_call_ref(&contract_signer, @5555, b"general"); + let caller = contract_identity::get_dynamic_call_ref_caller(&call_ref, @5555, b"other"); + assert!(caller == @1234, 0); + irrecoverably_destroy_contract_signer(contract_signer); + } + + struct TestTarget {} + + #[test] + fun test_get_call_ref() { + let account = create_signer_for_test(@3333); + let contract_signer = contract_identity::create_contract_signer(&account); + let call_ref = contract_identity::make_call_ref(&contract_signer); + let caller = contract_identity::get_call_ref_caller(&call_ref); + assert!(caller == @3333, 0); + irrecoverably_destroy_contract_signer(contract_signer); + } +} diff --git a/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/tests/native_token_test_helpers.move b/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/tests/native_token_test_helpers.move new file mode 100644 index 00000000..27454078 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/tests/native_token_test_helpers.move @@ -0,0 +1,22 @@ +#[test_only] +module endpoint_v2_common::native_token_test_helpers { + use std::aptos_coin; + use std::fungible_asset; + use std::fungible_asset::FungibleAsset; + use std::primary_fungible_store::ensure_primary_store_exists; + + public fun initialize_native_token_for_test() { + let fa = aptos_coin::mint_apt_fa_for_test(0); + fungible_asset::destroy_zero(fa); + } + + public fun mint_native_token_for_test(amount: u64): FungibleAsset { + aptos_coin::mint_apt_fa_for_test(amount) + } + + public fun burn_token_for_test(token: FungibleAsset) { + let metadata = fungible_asset::asset_metadata(&token); + let burn_loc = ensure_primary_store_exists(@0x0, metadata); + fungible_asset::deposit(burn_loc, token); + } +} diff --git a/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/tests/packet_tests.move b/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/tests/packet_tests.move new file mode 100644 index 00000000..12cd5e75 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/tests/packet_tests.move @@ -0,0 +1,76 @@ +#[test_only] +module endpoint_v2_common::packet_tests { + use std::vector; + + use endpoint_v2_common::bytes32; + use endpoint_v2_common::guid::compute_guid; + use endpoint_v2_common::packet_raw::get_packet_bytes; + use endpoint_v2_common::packet_v1_codec; + use endpoint_v2_common::packet_v1_codec::{ + compute_payload, + extract_header, + get_dst_eid, + get_guid, + get_message, + get_nonce, + get_receiver, + get_sender, get_src_eid, new_packet_v1, + }; + + #[test] + fun test_encode_and_extract_packet() { + let src_eid = 1; + let sender = bytes32::from_address(@0x3); + let dst_eid = 2; + let receiver = bytes32::from_address(@0x4); + let nonce = 0x1234; + let message = vector[9, 8, 7, 6, 5, 4]; + let guid = compute_guid(nonce, src_eid, sender, dst_eid, receiver); + + let packet = new_packet_v1(src_eid, sender, dst_eid, receiver, nonce, guid, message); + + // test header extraction and assertion + let packet_header = extract_header(&packet); + packet_v1_codec::assert_receive_header(&packet_header, 2); + + // test textual decoders + assert!(get_src_eid(&packet) == 1, 2); + + assert!(get_sender(&packet) == sender, 3); + assert!(get_dst_eid(&packet) == 2, 4); + assert!(get_receiver(&packet) == receiver, 5); + assert!(get_nonce(&packet) == 0x1234, 6); + assert!(get_message(&packet) == message, 7); + assert!(get_guid(&packet) == guid, 8); + + // construct expected serialized packet + let expected = vector[ + 1, // version + 0, 0, 0, 0, 0, 0, 18, 52, // nonce + 0, 0, 0, 1, // src_eid + 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, 3, // sender + 0, 0, 0, 2, // dst_eid + 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, 4, // receiver + ]; + vector::append(&mut expected, bytes32::from_bytes32(guid)); + vector::append(&mut expected, vector[9, 8, 7, 6, 5, 4]); + + let serialized = get_packet_bytes(packet); + // test whole + assert!(serialized == expected, 1); + } + + #[test] + fun test_compute_payload() { + let guid = bytes32::to_bytes32(b"................................"); + let message = vector[18, 19, 20]; + let payload = compute_payload(guid, message); + + let expected = vector[ + 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, + 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, // 32 periods + 18, 19, 20, // message + ]; + assert!(payload == expected, 1); + } +} diff --git a/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/tests/packet_v1_codec_tests.move b/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/tests/packet_v1_codec_tests.move new file mode 100644 index 00000000..f7db0e96 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/tests/packet_v1_codec_tests.move @@ -0,0 +1,56 @@ +#[test_only] +module endpoint_v2_common::packet_v1_codec_tests { + use std::vector; + + use endpoint_v2_common::bytes32; + use endpoint_v2_common::packet_raw; + use endpoint_v2_common::packet_v1_codec::{assert_receive_header, new_packet_v1_header_only}; + + #[test] + fun test_assert_receive_header() { + let header = new_packet_v1_header_only( + 1, + bytes32::from_address(@0x3), + 2, + bytes32::from_address(@0x4), + 0x1234, + ); + assert_receive_header(&header, 2); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2_common::packet_v1_codec::EINVALID_PACKET_HEADER)] + fun test_assert_receive_header_fails_if_invalid_length() { + let header = packet_raw::bytes_to_raw_packet(b"1234"); + assert_receive_header(&header, 1); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2_common::packet_v1_codec::EINVALID_PACKET_VERSION)] + fun test_assert_receive_header_fails_if_invalid_version() { + let header = new_packet_v1_header_only( + 1, + bytes32::from_address(@0x3), + 2, + bytes32::from_address(@0x4), + 0x1234, + ); + let bytes = packet_raw::borrow_packet_bytes_mut(&mut header); + *vector::borrow_mut(bytes, 0) = 0x02; + assert_receive_header(&header, 2); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2_common::packet_v1_codec::EINVALID_EID)] + fun test_assert_receive_header_fails_if_invalid_dst_eid() { + let header = new_packet_v1_header_only( + 1, + bytes32::from_address(@0x3), + 2, + bytes32::from_address(@0x4), + 0x1234, + ); + // dst_eid (not src_eid) should match local_eid + assert_receive_header(&header, 1); + } +} diff --git a/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/tests/serde_tests.move b/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/tests/serde_tests.move new file mode 100644 index 00000000..355d1749 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/tests/serde_tests.move @@ -0,0 +1,146 @@ +#[test_only] +module endpoint_v2_common::serde_tests { + use endpoint_v2_common::bytes32; + use endpoint_v2_common::serde::{ + append_address, + append_bytes, + append_bytes32, + append_u128, + append_u16, + append_u256, + append_u32, + append_u64, + append_u8, + append_uint, + bytes_of, + extract_address, + extract_bytes32, + extract_bytes_until_end, + extract_u128, + extract_u16, + extract_u256, + extract_u32, + extract_u64, + extract_u8, + extract_uint, + map_count + }; + + #[test] + fun test_extract_uint() { + let data: vector = x"99999999123456789999999999999999999999"; + let position = 4; + let result: u32 = (extract_uint(&data, &mut position, 2) as u32); + assert!(result == 0x1234, (result as u64)); // little endian + assert!(position == 6, 0); + } + + #[test] + fun test_append_uint() { + let data: vector = x"99999999"; + append_uint(&mut data, 0x1234, 2); + assert!(data == x"999999991234", 0); + } + + #[test] + fun test_append_then_extract_uint() { + let data: vector = x"99999999"; + append_uint(&mut data, 0x1234, 2); + let position = 4; + let result: u32 = (extract_uint(&data, &mut position, 2) as u32); + assert!(result == 0x1234, 0); + assert!(position == 6, 0); + } + + #[test] + fun test_append_bytes() { + let data: vector = x"4444"; + append_bytes(&mut data, x"1234567890"); + assert!(data == x"44441234567890", 0); + } + + #[test] + fun test_extract_bytes_until_end() { + let data: vector = x"444400000000001234567890"; + let position = 2; + let result: vector = extract_bytes_until_end(&data, &mut position); + assert!(result == x"00000000001234567890", 0); + assert!(position == 12, 0); + } + + #[test] + fun test_append_address() { + let data: vector = x"4444"; + append_address(&mut data, @0x12345678); + assert!(data == x"44440000000000000000000000000000000000000000000000000000000012345678", 0); + } + + #[test] + fun test_extract_address() { + let data: vector = x"44440000000000000000000000000000000000000000000000000000000087654321"; + let position = 2; + let result: address = extract_address(&data, &mut position); + assert!(result == @0x87654321, 0); + assert!(position == 34, 0); + } + + #[test] + fun test_various() { + let buf: vector = x"4444"; + append_u8(&mut buf, 0x12); // 1 byte + append_u16(&mut buf, 0x1234); // 2 bytes + append_u32(&mut buf, 0x12345678); // 4 bytes + append_u64(&mut buf, 0x1234567890); // 8 bytes + append_u128(&mut buf, 0x12345678901234567890); // 16 bytes + append_u256(&mut buf, 0x1234567890123456789012345678901234567890123456789012345678901234); // 32 bytes + + let pos = 2; // start after the initial junk data + assert!(extract_u8(&buf, &mut pos) == 0x12, 0); + assert!(extract_u16(&buf, &mut pos) == 0x1234, 0); + assert!(extract_u32(&buf, &mut pos) == 0x12345678, 0); + assert!(extract_u64(&buf, &mut pos) == 0x1234567890, 0); + assert!(extract_u128(&buf, &mut pos) == 0x12345678901234567890, 0); + assert!(extract_u256(&buf, &mut pos) == 0x1234567890123456789012345678901234567890123456789012345678901234, 0); + // 2 initial bytes + 63 bytes in closure = 65 + assert!(pos == 65, 0); + } + + #[test] + fun test_append_bytes32() { + let data: vector = x"4444"; + let b32 = bytes32::to_bytes32(x"5555555555555555555555555555555555555555555555555555555555555555"); + append_bytes32(&mut data, b32); + assert!(data == x"44445555555555555555555555555555555555555555555555555555555555555555", 0); + } + + #[test] + fun test_extract_bytes32() { + let data = x"444455555555555555555555555555555555555555555555555555555555555555551234"; + let pos = 2; + let result = extract_bytes32(&data, &mut pos); + assert!(result == bytes32::to_bytes32(x"5555555555555555555555555555555555555555555555555555555555555555"), 0); + } + + #[test] + fun test_map_count() { + let data = x"00010002000300040005"; + let pos = 0; + let vec = map_count(4, |_i| extract_u16(&data, &mut pos)); + let expected = vector[1, 2, 3, 4]; + assert!(vec == expected, 0); + } + + #[test] + fun test_map_count_using_i() { + let vec = map_count(5, |i| (i as u8)); + let expected = vector[0, 1, 2, 3, 4]; + assert!(vec == expected, 0); + } + + #[test] + fun test_bytes_of() { + let data = bytes_of(|buf| append_address(buf, @0x12345678)); + let expected = x"0000000000000000000000000000000000000000000000000000000012345678"; + assert!(data == expected, 0); + } +} diff --git a/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/tests/universal_config_tests.move b/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/tests/universal_config_tests.move new file mode 100644 index 00000000..6634acd5 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/tests/universal_config_tests.move @@ -0,0 +1,136 @@ +#[test_only] +module endpoint_v2_common::universal_config_tests { + use std::account::create_signer_for_test; + use std::fungible_asset; + + use endpoint_v2_common::native_token_test_helpers::burn_token_for_test; + use endpoint_v2_common::universal_config::{ + assert_zro_metadata_set, eid, get_zro_address, get_zro_metadata, has_zro_metadata, init_module_for_test, + initialize, is_zro, is_zro_metadata, lock_zro_address, set_zro_address, + }; + use endpoint_v2_common::zro_test_helpers::create_fa; + + #[test] + fun test_eid() { + let ep_common = &create_signer_for_test(@layerzero_admin); + init_module_for_test(0); // Initializing to 0 = not initialized state + initialize(ep_common, 55); + assert!(eid() == 55, 1); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2_common::universal_config::EUNAUTHORIZED)] + fun test_eid_should_fail_if_not_layerzero_admin() { + let lz_admin = &create_signer_for_test(@endpoint_v2_common); + init_module_for_test(0); // Initializing to 0 = not initialized state + initialize(lz_admin, 55); + } + + #[test] + fun test_set_zro_address() { + let (other_addr, other_metadata, _) = create_fa(b"OTHER"); + let (zro_addr, zro_metadata, _) = create_fa(b"ZRO"); + init_module_for_test(55); + let lz_admin = &create_signer_for_test(@layerzero_admin); + assert!(!has_zro_metadata(), 0); + + set_zro_address(lz_admin, other_addr); + assert!(get_zro_address() == other_addr, 0); + assert!(has_zro_metadata(), 0); + assert!(get_zro_metadata() == other_metadata, 1); + assert!(has_zro_metadata(), 0); + // can change setting if not locked + set_zro_address(lz_admin, zro_addr); + assert!(has_zro_metadata(), 0); + // can unset if not locked + set_zro_address(lz_admin, @0x0); + assert!(!has_zro_metadata(), 0); + + set_zro_address(lz_admin, zro_addr); + assert!(get_zro_address() == zro_addr, 0); + assert!(get_zro_metadata() == zro_metadata, 0); + assert_zro_metadata_set(); + assert!(has_zro_metadata(), 0); + + let fa = fungible_asset::zero(zro_metadata); + let fa_other = fungible_asset::zero(other_metadata); + + assert!(is_zro_metadata(zro_metadata), 0); + assert!(!is_zro_metadata(other_metadata), 0); + + assert!(is_zro(&fa), 0); + assert!(!is_zro(&fa_other), 0); + + lock_zro_address(lz_admin); + + burn_token_for_test(fa); + burn_token_for_test(fa_other); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2_common::universal_config::EUNAUTHORIZED)] + fun test_set_zro_address_should_fail_if_not_admin() { + init_module_for_test(55); + let lz = &create_signer_for_test(@layerzero_treasury_admin); + + let (zro_addr, _, _) = create_fa(b"ZRO"); + set_zro_address(lz, zro_addr); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2_common::universal_config::EZRO_ADDRESS_LOCKED)] + fun test_set_zro_address_fails_if_locked_when_setting() { + let (zro_addr, _, _) = create_fa(b"ZRO"); + init_module_for_test(55); + + let lz_admin = &create_signer_for_test(@layerzero_admin); + set_zro_address(lz_admin, zro_addr); + lock_zro_address(lz_admin); + set_zro_address(lz_admin, zro_addr); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2_common::universal_config::ENO_CHANGE)] + fun test_lock_zro_address_fails_if_no_change() { + let (zro_addr, _, _) = create_fa(b"ZRO"); + init_module_for_test(55); + + let lz_admin = &create_signer_for_test(@layerzero_admin); + set_zro_address(lz_admin, zro_addr); + lock_zro_address(lz_admin); + lock_zro_address(lz_admin); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2_common::universal_config::EZRO_ADDRESS_LOCKED)] + fun test_set_zro_address_fails_if_locked_when_unsetting() { + let (zro_addr, _, _) = create_fa(b"ZRO"); + init_module_for_test(55); + + let lz_admin = &create_signer_for_test(@layerzero_admin); + set_zro_address(lz_admin, zro_addr); + lock_zro_address(lz_admin); + set_zro_address(lz_admin, @0x0); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2_common::universal_config::ENO_CHANGE)] + fun test_set_zro_address_fails_if_no_change() { + let (zro_addr, _, _) = create_fa(b"ZRO"); + init_module_for_test(55); + + let lz_admin = &create_signer_for_test(@layerzero_admin); + set_zro_address(lz_admin, zro_addr); + set_zro_address(lz_admin, zro_addr); + } + + + #[test] + #[expected_failure(abort_code = endpoint_v2_common::universal_config::ENO_CHANGE)] + fun test_set_zro_address_fails_if_no_change_unsetting() { + init_module_for_test(55); + + let lz_admin = &create_signer_for_test(@layerzero_admin); + set_zro_address(lz_admin, @0x0); + } +} diff --git a/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/tests/zro_test_helpers.move b/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/tests/zro_test_helpers.move new file mode 100644 index 00000000..36656bd0 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/endpoint_v2_common/tests/zro_test_helpers.move @@ -0,0 +1,29 @@ +#[test_only] +module endpoint_v2_common::zro_test_helpers { + use std::account::create_signer_for_test; + use std::fungible_asset; + use std::fungible_asset::{Metadata, MintRef}; + use std::object; + use std::object::{address_from_constructor_ref, Object, object_from_constructor_ref}; + use std::option; + use std::primary_fungible_store::create_primary_store_enabled_fungible_asset; + use std::string::utf8; + + public fun create_fa(name: vector): (address, Object, MintRef) { + let lz = &create_signer_for_test(@layerzero_treasury_admin); + let constructor = object::create_named_object(lz, name); + create_primary_store_enabled_fungible_asset( + &constructor, + option::none(), + utf8(b"ZRO"), + utf8(b"ZRO"), + 8, + utf8(b""), + utf8(b""), + ); + let mint_ref = fungible_asset::generate_mint_ref(&constructor); + let addr = address_from_constructor_ref(&constructor); + let metadata = object_from_constructor_ref(&constructor); + (addr, metadata, mint_ref) + } +} diff --git a/packages/layerzero-v2/aptos/contracts/layerzero_views/Move.toml b/packages/layerzero-v2/aptos/contracts/layerzero_views/Move.toml new file mode 100644 index 00000000..82c484cc --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/layerzero_views/Move.toml @@ -0,0 +1,69 @@ +[package] +name = "layerzero_views" +version = "1.0.0" + +[addresses] +layerzero_views = "_" +dvn = "_" +router_node_0 = "_" +zro = "_" +simple_msglib = "_" +blocked_msglib = "_" +uln_302 = "_" +router_node_1 = "_" +endpoint_v2_common = "_" +endpoint_v2 = "_" +layerzero_admin = "_" +layerzero_treasury_admin = "_" +treasury = "_" +msglib_types = "_" +worker_common = "_" +price_feed_router_0 = "_" +price_feed_router_1 = "_" +price_feed_module_0 = "_" +executor_fee_lib_router_0 = "_" +executor_fee_lib_router_1 = "_" +dvn_fee_lib_router_0 = "_" +dvn_fee_lib_router_1 = "_" +executor_fee_lib_0 = "_" +dvn_fee_lib_0 = "_" + +[dev-addresses] +layerzero_views = "0x1010123" +dvn = "0x3001" +router_node_0 = "0x9001" +zro = "0x1112" +simple_msglib = "0x9002" +blocked_msglib = "0x9003" +uln_302 = "0x9005" +router_node_1 = "0x9007" +endpoint_v2_common = "0x9098" +endpoint_v2 = "0x12345678" +layerzero_admin = "0x18943124" +layerzero_treasury_admin = "0x1894312499" +treasury = "0x123432432" +msglib_types = "0x97324123" +worker_common = "0x3999" +price_feed_router_0 = "0x65DD71A" +price_feed_router_1 = "0x65DD71AB" +price_feed_module_0 = "0x65DD71" +executor_fee_lib_router_0 = "0x30001" +executor_fee_lib_router_1 = "0x30002" +dvn_fee_lib_router_0 = "0x30001a" +dvn_fee_lib_router_1 = "0x30002a" +executor_fee_lib_0 = "0x3000" +dvn_fee_lib_0 = "0x3000a" + +[dependencies.AptosFramework] +git = "https://github.com/aptos-labs/aptos-framework.git" +rev = "mainnet" +subdir = "aptos-framework" + +[dependencies] +endpoint_v2_common = { local = "../endpoint_v2_common" } +endpoint_v2 = { local = "../endpoint_v2" } +uln_302 = { local = "../msglib/libs/uln_302" } + +[dev-dependencies] +dvn = { local = "../workers/dvn" } +treasury = { local = "../treasury" } \ No newline at end of file diff --git a/packages/layerzero-v2/aptos/contracts/layerzero_views/sources/endpoint_views.move b/packages/layerzero-v2/aptos/contracts/layerzero_views/sources/endpoint_views.move new file mode 100644 index 00000000..775fbf71 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/layerzero_views/sources/endpoint_views.move @@ -0,0 +1,79 @@ +module layerzero_views::endpoint_views { + use endpoint_v2::endpoint; + use endpoint_v2_common::bytes32; + + // EXECUTABLE STATES + const STATE_NOT_EXECUTABLE: u8 = 0; + const STATE_VERIFIED_BUT_NOT_EXECUTABLE: u8 = 1; + const STATE_EXECUTABLE: u8 = 2; + const STATE_EXECUTED: u8 = 3; + + #[view] + public fun initializable( + src_eid: u32, + sender: vector, + receiver: address, + ): bool { + endpoint::initializable(src_eid, sender, receiver) + } + + #[view] + /// View function to check if a message is verifiable (can commit_verification on the uln via endpoint.verify()) + public fun verifiable( + src_eid: u32, + sender: vector, + nonce: u64, + receiver: address, + receive_lib: address, + ): bool { + if ( + !endpoint::is_valid_receive_library_for_oapp(receiver, src_eid, receive_lib) || + !endpoint::verifiable_view(src_eid, sender, nonce, receiver) + ) { + return false + }; + + true + } + + #[view] + public fun executable( + src_eid: u32, + sender: vector, + nonce: u64, + receiver: address, + ): u8 { + let sender_bytes32 = bytes32::to_bytes32(sender); + let has_payload_hash = endpoint::has_payload_hash( + src_eid, + sender_bytes32, + nonce, + receiver, + ); + + if (!has_payload_hash && nonce <= endpoint::get_lazy_inbound_nonce(receiver, src_eid, sender)) { + return STATE_EXECUTED + }; + + if (has_payload_hash) { + let payload_hash = endpoint::payload_hash( + receiver, + src_eid, + sender_bytes32, + nonce, + ); + if ( + !bytes32::is_zero(&payload_hash) && nonce <= endpoint::get_inbound_nonce( + receiver, src_eid, sender, + )) { + return STATE_EXECUTABLE + }; + + if (payload_hash != bytes32::zero_bytes32()) { + return STATE_VERIFIED_BUT_NOT_EXECUTABLE + } + }; + + return STATE_NOT_EXECUTABLE + } +} diff --git a/packages/layerzero-v2/aptos/contracts/layerzero_views/sources/uln_302.move b/packages/layerzero-v2/aptos/contracts/layerzero_views/sources/uln_302.move new file mode 100644 index 00000000..3de4e685 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/layerzero_views/sources/uln_302.move @@ -0,0 +1,94 @@ +module layerzero_views::uln_302 { + use endpoint_v2::endpoint; + use endpoint_v2_common::bytes32::{Self, Bytes32}; + use endpoint_v2_common::packet_raw; + use endpoint_v2_common::packet_v1_codec; + use endpoint_v2_common::universal_config; + use layerzero_views::endpoint_views; + + // VERIFICATION STATES + const STATE_VERIFYING: u8 = 0; + const STATE_VERIFIABLE: u8 = 1; + const STATE_VERIFIED: u8 = 2; + const STATE_NOT_INITIALIZABLE: u8 = 3; + + #[view] + /// View function to check if a message is verifiable (can commit_verification on the uln via endpoint.verify()) + public fun verifiable( + packet_header_bytes: vector, + payload_hash: vector, + ): u8 { + // extract data from packet + let packet_header = packet_raw::bytes_to_raw_packet(packet_header_bytes); + let src_eid = packet_v1_codec::get_src_eid(&packet_header); + let sender = packet_v1_codec::get_sender(&packet_header); + let nonce = packet_v1_codec::get_nonce(&packet_header); + let receiver = bytes32::to_address( + packet_v1_codec::get_receiver(&packet_header) + ); + + packet_v1_codec::assert_receive_header( + &packet_header, + universal_config::eid() + ); + + // check endpoint initializable + if (!endpoint_views::initializable( + src_eid, + bytes32::from_bytes32(sender), + receiver, + )) { + return STATE_NOT_INITIALIZABLE + }; + + // check endpoint verifiable + if (!endpoint_verifiable( + src_eid, + sender, + nonce, + receiver, + payload_hash, + )) { + return STATE_VERIFIED + }; + + // check uln verifiable + if (uln_302::msglib::verifiable(packet_header_bytes, payload_hash)) { + return STATE_VERIFIABLE + }; + STATE_VERIFYING + } + + fun endpoint_verifiable( + src_eid: u32, + sender: Bytes32, + nonce: u64, + receiver: address, + payload_hash: vector, + ): bool { + let (receive_lib, _) = endpoint_v2::endpoint::get_effective_receive_library(receiver, src_eid); + let sender_vec = bytes32::from_bytes32(sender); + // check if commit_verification is possible via endpoint::verify() + if (!endpoint_views::verifiable( + src_eid, + sender_vec, + nonce, + receiver, + receive_lib, + )) { + return false + }; + + if ( + endpoint::has_payload_hash_view( + src_eid, + bytes32::from_bytes32(sender), + nonce, + receiver, + ) && endpoint_v2::endpoint::get_payload_hash(receiver, src_eid, sender_vec, nonce) == + payload_hash) { + return false + }; + true + } +} diff --git a/packages/layerzero-v2/aptos/contracts/layerzero_views/tests/layerzero_views_tests.move b/packages/layerzero-v2/aptos/contracts/layerzero_views/tests/layerzero_views_tests.move new file mode 100644 index 00000000..94aca712 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/layerzero_views/tests/layerzero_views_tests.move @@ -0,0 +1,289 @@ +#[test_only] +module layerzero_views::layerzero_views_tests { + use std::account::{create_account_for_test, create_signer_for_test}; + use std::event::was_event_emitted; + use std::fungible_asset::{Self, FungibleAsset, Metadata}; + use std::object::address_to_object; + use std::option; + use std::primary_fungible_store; + + use endpoint_v2::channels::{ + packet_delivered_event, + packet_sent_event, + packet_verified_event, + }; + use endpoint_v2::endpoint::{Self, register_oapp, wrap_guid}; + use endpoint_v2_common::bytes32::{Self, from_bytes32}; + use endpoint_v2_common::contract_identity::{Self, irrecoverably_destroy_contract_signer, make_call_ref_for_test}; + use endpoint_v2_common::guid::compute_guid; + use endpoint_v2_common::native_token_test_helpers::{ + burn_token_for_test, + mint_native_token_for_test + }; + use endpoint_v2_common::packet_raw::{Self, get_packet_bytes}; + use endpoint_v2_common::packet_v1_codec::{Self, new_packet_v1}; + use executor_fee_lib_0::executor_option::{append_executor_options, new_executor_options, new_lz_receive_option}; + use layerzero_views::endpoint_views; + use layerzero_views::uln_302; + use msglib_types::worker_options; + use worker_common::worker_config; + + #[test] + fun test_verifiable() { + // account setup + let src_eid = 1; + let dst_eid = 1; + let oapp_address = @9112; + let account = &create_signer_for_test(oapp_address); + let sender = bytes32::from_address(oapp_address); + let receiver = bytes32::from_address(oapp_address); + let nonce = 1u64; + let guid = compute_guid( + nonce, + src_eid, + sender, + dst_eid, + receiver, + ); + + // init test context + endpoint_v2::test_helpers::setup_layerzero_for_test_uln(dst_eid, src_eid); + + // DVN Config + let pubkey1 = x"618d6c6d3b7bd345563636f27db50a39a7fe80c712d22d92aa3485264c9d8446b68db4661092205507a2a39cf3008c23e416cc4f41d87f4431a7cbc0d516f1a4"; + dvn::dvn::init_module_for_test(); + dvn::dvn::initialize( + &create_account_for_test(@dvn), + @dvn, + vector[@111], + vector[pubkey1], + 1, + vector[@uln_302], + @dvn_fee_lib_0, + ); + worker_config::set_dvn_dst_config( + &make_call_ref_for_test(@dvn), + src_eid, + 1000, + 1000, + 1000, + ); + // opt into using worker config for dvn feelib routing + uln_302::msglib::set_worker_config_for_fee_lib_routing_opt_in(&create_signer_for_test(@dvn), true); + + let feed_address = @1234; + worker_config::set_price_feed( + &make_call_ref_for_test(@dvn), + @price_feed_module_0, + feed_address, + ); + + let contract_signer = contract_identity::create_contract_signer(account); + let call_ref = &contract_identity::make_call_ref(&contract_signer); + register_oapp( + account, + std::string::utf8(b"test_oapp") + ); + endpoint::register_receive_pathway(call_ref, dst_eid, receiver); + + let options = worker_options::new_empty_type_3_options(); + append_executor_options( + &mut options, + &new_executor_options( + vector[new_lz_receive_option(100, 0)], + vector[], + vector[], + false, + ) + ); + + let message = vector[1, 2, 3, 4]; + + let (fee_in_native, _) = endpoint::quote( + oapp_address, + dst_eid, + bytes32::from_address(oapp_address), + message, + options, + false // pay_in_zro, + ); + + let packet = new_packet_v1( + src_eid, + sender, + dst_eid, + receiver, + nonce, + guid, + message, + ); + let packet_header = packet_v1_codec::extract_header(&packet); + let payload_hash = packet_v1_codec::get_payload_hash(&packet); + + // send test + + // deposit into account + let fee = mint_native_token_for_test(fee_in_native); + primary_fungible_store::ensure_primary_store_exists( + oapp_address, + fungible_asset::asset_metadata(&fee) + ); + burn_token_for_test(fee); + let zro_fee = option::none(); + + let native_metadata = address_to_object(@native_token_metadata_address); + let native_fee = primary_fungible_store::withdraw( + account, + native_metadata, + fee_in_native, + ); + + let sending_call_ref = &contract_identity::make_call_ref(&contract_signer); + assert!( + uln_302::verifiable( + packet_raw::get_packet_bytes(packet_header), + bytes32::from_bytes32(payload_hash) + ) == 0, // VERIFYING + 1, + ); + assert!( + endpoint_views::executable( + src_eid, + bytes32::from_bytes32(sender), + nonce, + bytes32::to_address(receiver) + ) == 0, // NOT_EXECUTABLE + 1, + ); + // send(oapp, dst_eid, type, options, native_fee); + endpoint::send( + sending_call_ref, + dst_eid, + receiver, + message, + options, + &mut native_fee, + &mut zro_fee, + ); + burn_token_for_test(native_fee); + option::destroy_none(zro_fee); + + assert!( + was_event_emitted( + &packet_sent_event(packet, options, @uln_302) + ), + 0, + ); + assert!( + uln_302::verifiable( + packet_raw::get_packet_bytes(packet_header), + bytes32::from_bytes32(payload_hash) + ) == 0, // VERIFYING + 1, + ); + assert!( + endpoint_views::executable( + src_eid, + bytes32::from_bytes32(sender), + nonce, + bytes32::to_address(receiver) + ) == 0, // NOT_EXECUTABLE + 1, + ); + + let admin = &create_signer_for_test(@111); + dvn::dvn::verify( + admin, + packet_raw::get_packet_bytes(packet_header), + bytes32::from_bytes32(payload_hash), + 100, + @uln_302, + 123456789123, + x"a13c94e82fc009f71f152f137bed7fb799fa7d75a91a0e3a4ed2000fd408ba052743f3b91ee00cf6a5e98cd4d12b3b2e4984213c0c1c5a251b4e98eeec54f7a800", + ); + + assert!( + uln_302::verifiable( + packet_raw::get_packet_bytes(packet_header), + bytes32::from_bytes32(payload_hash) + ) == 1, // VERIFIABLE + 2, + ); + assert!( + endpoint_views::executable( + src_eid, + bytes32::from_bytes32(sender), + nonce, + bytes32::to_address(receiver) + ) == 0, // NOT_EXECUTABLE + 1, + ); + + // - verify packet + endpoint_v2::endpoint::verify( + @uln_302, + get_packet_bytes(packet_header), + bytes32::from_bytes32(payload_hash), + b"", + ); + + assert!( + was_event_emitted( + &packet_verified_event( + src_eid, + from_bytes32(sender), + nonce, + oapp_address, + from_bytes32(payload_hash), + ) + ), + 1, + ); + + assert!( + uln_302::verifiable( + packet_raw::get_packet_bytes(packet_header), + bytes32::from_bytes32(payload_hash) + ) == 2, // VERIFIED + 3, + ); + assert!( + endpoint_views::executable( + src_eid, + bytes32::from_bytes32(sender), + nonce, + bytes32::to_address(receiver) + ) == 2, // NOT_EXECUTABLE + 1, + ); + + let receiving_call_ref = &contract_identity::make_call_ref(&contract_signer); + // - execute packet + // - check that counter has incremented + // doesn't matter who the caller of lz_receive + endpoint::clear( + receiving_call_ref, + src_eid, + sender, + nonce, + wrap_guid(guid), + message, + ); + assert!( + was_event_emitted( + &packet_delivered_event(src_eid, from_bytes32(sender), nonce, oapp_address) + ), + 2, + ); + assert!( + endpoint_views::executable( + src_eid, + bytes32::from_bytes32(sender), + nonce, + bytes32::to_address(receiver) + ) == 3, // NOT_EXECUTABLE + 1, + ); + irrecoverably_destroy_contract_signer(contract_signer); + } +} diff --git a/packages/layerzero-v2/aptos/contracts/layerzero_views/tests/uln_302_tests.move b/packages/layerzero-v2/aptos/contracts/layerzero_views/tests/uln_302_tests.move new file mode 100644 index 00000000..ca33e849 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/layerzero_views/tests/uln_302_tests.move @@ -0,0 +1,360 @@ +#[test_only] +module uln_302::uln_302_tests { + use std::account::{create_account_for_test, create_signer_for_test}; + use std::event::was_event_emitted; + use std::fungible_asset::{Self, FungibleAsset, Metadata}; + use std::object::address_to_object; + use std::option; + use std::primary_fungible_store; + + use endpoint_v2::channels::{ + packet_delivered_event, + packet_sent_event, + packet_verified_event, + }; + use endpoint_v2::endpoint::{Self, register_oapp, wrap_guid}; + use endpoint_v2_common::bytes32::{Self, from_bytes32}; + use endpoint_v2_common::contract_identity::{Self, irrecoverably_destroy_contract_signer, make_call_ref_for_test}; + use endpoint_v2_common::guid::compute_guid; + use endpoint_v2_common::native_token_test_helpers::{burn_token_for_test, mint_native_token_for_test}; + use endpoint_v2_common::packet_raw::{Self, get_packet_bytes}; + use endpoint_v2_common::packet_v1_codec::{Self, new_packet_v1}; + use endpoint_v2_common::zro_test_helpers::create_fa; + use executor_fee_lib_0::executor_option::{append_executor_options, new_executor_options, new_lz_receive_option}; + use msglib_types::worker_options; + use worker_common::worker_config; + + #[test] + fun test_verifiable() { + // account setup + let src_eid = 1; + let dst_eid = 1; + let oapp_address = @9112; + let account = &create_signer_for_test(oapp_address); + let sender = bytes32::from_address(oapp_address); + let receiver = bytes32::from_address(oapp_address); + let nonce = 1u64; + let guid = compute_guid(nonce, src_eid, sender, dst_eid, receiver); + + // init test context + endpoint_v2::test_helpers::setup_layerzero_for_test_uln(dst_eid, src_eid); + + // DVN Config + let pubkey1 = x"618d6c6d3b7bd345563636f27db50a39a7fe80c712d22d92aa3485264c9d8446b68db4661092205507a2a39cf3008c23e416cc4f41d87f4431a7cbc0d516f1a4"; + dvn::dvn::init_module_for_test(); + dvn::dvn::initialize( + &create_account_for_test(@dvn), + @dvn, + vector[@111], + vector[pubkey1], + 1, + vector[@uln_302], + @dvn_fee_lib_0, + ); + worker_config::set_dvn_dst_config( + &make_call_ref_for_test(@dvn), + src_eid, + 1000, + 1000, + 1000, + ); + // opt into using worker config for dvn feelib routing + uln_302::msglib::set_worker_config_for_fee_lib_routing_opt_in(&create_signer_for_test(@dvn), true); + + let feed_address = @1234; + worker_config::set_price_feed( + &make_call_ref_for_test(@dvn), + @price_feed_module_0, + feed_address, + ); + + let contract_signer = contract_identity::create_contract_signer(account); + let call_ref = &contract_identity::make_call_ref(&contract_signer); + register_oapp(account, std::string::utf8(b"test_oapp")); + endpoint::register_receive_pathway( + call_ref, + dst_eid, + receiver, + ); + + let options = worker_options::new_empty_type_3_options(); + append_executor_options(&mut options, &new_executor_options( + vector[ + new_lz_receive_option(100, 0), + ], + vector[], + vector[], + false, + )); + + let message = vector[1, 2, 3, 4]; + + let (fee_in_native, _) = endpoint::quote( + oapp_address, + dst_eid, + bytes32::from_address(oapp_address), + message, + options, + false, // pay_in_zro + ); + + let packet = new_packet_v1( + src_eid, + sender, + dst_eid, + receiver, + nonce, + guid, + message, + ); + + let payload_hash = packet_v1_codec::get_payload_hash(&packet); + + // send test + + // deposit into account + let fee = mint_native_token_for_test(fee_in_native); + primary_fungible_store::ensure_primary_store_exists(oapp_address, fungible_asset::asset_metadata(&fee)); + burn_token_for_test(fee); + let zro_fee = option::none(); + + let native_metadata = address_to_object(@native_token_metadata_address); + let native_fee = primary_fungible_store::withdraw( + account, + native_metadata, + fee_in_native, + ); + + let sending_call_ref = &contract_identity::make_call_ref(&contract_signer); + // send(oapp, dst_eid, type, options, native_fee); + endpoint::send( + sending_call_ref, + dst_eid, + receiver, + message, + options, + &mut native_fee, + &mut zro_fee, + ); + burn_token_for_test(native_fee); + option::destroy_none(zro_fee); + + assert!(was_event_emitted(&packet_sent_event(packet, options, @uln_302)), 0); + let admin = &create_signer_for_test(@111); + dvn::dvn::verify( + admin, + packet_raw::get_packet_bytes(packet_v1_codec::extract_header(&packet)), + bytes32::from_bytes32(payload_hash), + 100, + @uln_302, + 123456789123, + x"a13c94e82fc009f71f152f137bed7fb799fa7d75a91a0e3a4ed2000fd408ba052743f3b91ee00cf6a5e98cd4d12b3b2e4984213c0c1c5a251b4e98eeec54f7a800", + ); + + // - verify packet + let packet_header = packet_v1_codec::extract_header(&packet); + endpoint_v2::endpoint::verify( + @uln_302, + get_packet_bytes(packet_header), + bytes32::from_bytes32(payload_hash), + b"", + ); + + assert!(was_event_emitted( + &packet_verified_event(src_eid, from_bytes32(sender), nonce, oapp_address, from_bytes32(payload_hash)) + ), 1); + + let receiving_call_ref = &contract_identity::make_call_ref(&contract_signer); + // - execute packet + // - check that counter has incremented + // doesn't matter who the caller of lz_receive + endpoint::clear( + receiving_call_ref, + src_eid, + sender, + nonce, + wrap_guid(guid), + message, + ); + assert!(was_event_emitted(&packet_delivered_event(src_eid, from_bytes32(sender), nonce, oapp_address)), 2); + irrecoverably_destroy_contract_signer(contract_signer) + } + + #[test] + fun test_verifiable_with_zro() { + // account setup + let src_eid = 1; + let dst_eid = 1; + let oapp_address = @9112; + let account = &create_signer_for_test(oapp_address); + let sender = bytes32::from_address(oapp_address); + let receiver = bytes32::from_address(oapp_address); + let nonce = 1u64; + let guid = compute_guid(nonce, src_eid, sender, dst_eid, receiver); + + // init test context + endpoint_v2::test_helpers::setup_layerzero_for_test_uln(dst_eid, src_eid); + + // DVN Config + let pubkey1 = x"618d6c6d3b7bd345563636f27db50a39a7fe80c712d22d92aa3485264c9d8446b68db4661092205507a2a39cf3008c23e416cc4f41d87f4431a7cbc0d516f1a4"; + dvn::dvn::init_module_for_test(); + dvn::dvn::initialize( + &create_account_for_test(@dvn), + @dvn, + vector[@111], + vector[pubkey1], + 1, + vector[@uln_302], + @dvn_fee_lib_0, + ); + worker_config::set_dvn_dst_config( + &make_call_ref_for_test(@dvn), + src_eid, + 1000, + 1000, + 1000, + ); + // opt into using worker config for dvn feelib routing + uln_302::msglib::set_worker_config_for_fee_lib_routing_opt_in(&create_signer_for_test(@dvn), true); + + let feed_address = @1234; + worker_config::set_price_feed( + &make_call_ref_for_test(@dvn), + @price_feed_module_0, + feed_address, + ); + + let contract_signer = contract_identity::create_contract_signer(account); + let call_ref = &contract_identity::make_call_ref(&contract_signer); + register_oapp(account, std::string::utf8(b"test_oapp")); + endpoint::register_receive_pathway( + call_ref, + dst_eid, + receiver, + ); + + + let options = worker_options::new_empty_type_3_options(); + append_executor_options(&mut options, &new_executor_options( + vector[ + new_lz_receive_option(100, 0), + ], + vector[], + vector[], + false, + )); + + let message = vector[1, 2, 3, 4]; + + let (zro_metadata_addr, _zro_metadata, zro_mint_ref) = create_fa(b"ZRO"); + + endpoint_v2_common::universal_config::set_zro_address( + &create_signer_for_test(@layerzero_admin), + zro_metadata_addr, + ); + + endpoint_v2_common::universal_config::lock_zro_address(&create_signer_for_test(@layerzero_admin)); + + treasury::treasury::set_zro_enabled( + &create_signer_for_test(@layerzero_treasury_admin), + true, + ); + + treasury::treasury::set_zro_fee( + &create_signer_for_test(@layerzero_treasury_admin), + 100, + ); + + let (fee_in_native, fee_in_zro) = endpoint::quote( + oapp_address, + dst_eid, + bytes32::from_address(oapp_address), + message, + options, + true, // pay_in_zro + ); + assert!(fee_in_zro == 100, 0); + + let packet = new_packet_v1( + src_eid, + sender, + dst_eid, + receiver, + nonce, + guid, + message, + ); + + let payload_hash = packet_v1_codec::get_payload_hash(&packet); + + // send test + + // deposit into account + let fee = mint_native_token_for_test(fee_in_native); + primary_fungible_store::ensure_primary_store_exists(oapp_address, fungible_asset::asset_metadata(&fee)); + burn_token_for_test(fee); + + let zro_fee = option::some(fungible_asset::mint(&zro_mint_ref, fee_in_zro)); + + let native_metadata = address_to_object(@native_token_metadata_address); + let native_fee = primary_fungible_store::withdraw( + account, + native_metadata, + fee_in_native, + ); + + let sending_call_ref = &contract_identity::make_call_ref(&contract_signer); + // send(oapp, dst_eid, type, options, native_fee); + endpoint::send( + sending_call_ref, + dst_eid, + receiver, + message, + options, + &mut native_fee, + &mut zro_fee, + ); + burn_token_for_test(native_fee); + option::destroy(zro_fee, |fa| burn_token_for_test(fa)); + + assert!(was_event_emitted(&packet_sent_event(packet, options, @uln_302)), 0); + let admin = &create_signer_for_test(@111); + dvn::dvn::verify( + admin, + packet_raw::get_packet_bytes(packet_v1_codec::extract_header(&packet)), + bytes32::from_bytes32(payload_hash), + 100, + @uln_302, + 123456789123, + x"a13c94e82fc009f71f152f137bed7fb799fa7d75a91a0e3a4ed2000fd408ba052743f3b91ee00cf6a5e98cd4d12b3b2e4984213c0c1c5a251b4e98eeec54f7a800", + ); + + // - verify packet + let packet_header = packet_v1_codec::extract_header(&packet); + endpoint_v2::endpoint::verify( + @uln_302, + get_packet_bytes(packet_header), + bytes32::from_bytes32(payload_hash), + b"", + ); + + assert!(was_event_emitted( + &packet_verified_event(src_eid, from_bytes32(sender), nonce, oapp_address, from_bytes32(payload_hash)) + ), 1); + + let receiving_call_ref = &contract_identity::make_call_ref(&contract_signer); + // - execute packet + // - check that counter has incremented + // doesn't matter who the caller of lz_receive + endpoint::clear( + receiving_call_ref, + src_eid, + sender, + nonce, + wrap_guid(guid), + message, + ); + assert!(was_event_emitted(&packet_delivered_event(src_eid, from_bytes32(sender), nonce, oapp_address)), 2); + irrecoverably_destroy_contract_signer(contract_signer) + } +} diff --git a/packages/layerzero-v2/aptos/contracts/msglib/libs/blocked_msglib/Move.toml b/packages/layerzero-v2/aptos/contracts/msglib/libs/blocked_msglib/Move.toml new file mode 100644 index 00000000..964f022a --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/msglib/libs/blocked_msglib/Move.toml @@ -0,0 +1,23 @@ +[package] +name = "blocked_msglib" +version = "1.0.0" +authors = [] + +[addresses] +blocked_msglib = "_" +endpoint_v2 = "_" +layerzero_treasury_admin = "_" +endpoint_v2_common = "_" +layerzero_admin = "_" + +[dev-addresses] +blocked_msglib = "0x9003" +endpoint_v2 = "0x12345678" +layerzero_treasury_admin = "0x1894312499" +endpoint_v2_common = "0x94535243" +layerzero_admin = "0x18943124" + +[dependencies] +endpoint_v2_common = { local = "../../../endpoint_v2_common" } + +[dev-dependencies] diff --git a/packages/layerzero-v2/aptos/contracts/msglib/libs/blocked_msglib/sources/router_calls.move b/packages/layerzero-v2/aptos/contracts/msglib/libs/blocked_msglib/sources/router_calls.move new file mode 100644 index 00000000..d2078ea5 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/msglib/libs/blocked_msglib/sources/router_calls.move @@ -0,0 +1,73 @@ +module blocked_msglib::router_calls { + use std::any::Any; + use std::fungible_asset::FungibleAsset; + use std::option::Option; + + use endpoint_v2_common::bytes32::Bytes32; + use endpoint_v2_common::contract_identity::DynamicCallRef; + use endpoint_v2_common::packet_raw::RawPacket; + use endpoint_v2_common::send_packet::SendPacket; + + const ENOT_IMPLEMENTED: u64 = 1; + + public fun quote( + _packet: SendPacket, + _options: vector, + _pay_in_zro: bool, + ): (u64, u64) { + abort ENOT_IMPLEMENTED + } + + public fun send( + _call_ref: &DynamicCallRef, + _packet: SendPacket, + _options: vector, + _native_token: &mut FungibleAsset, + _zro_token: &mut Option, + ): (u64, u64, RawPacket) { + abort ENOT_IMPLEMENTED + } + + public fun commit_verification( + _call_ref: &DynamicCallRef, + _packet_header: RawPacket, + _payload_hash: Bytes32, + _extra_data: vector, + ): (address, u32, Bytes32, u64) { + abort ENOT_IMPLEMENTED + } + + public fun dvn_verify(_call_ref: &DynamicCallRef, _params: Any) { + abort ENOT_IMPLEMENTED + } + + public fun set_config( + _call_ref: &DynamicCallRef, + _oapp: address, + _eid: u32, + _config_type: u32, + _config: vector + ) { + abort ENOT_IMPLEMENTED + } + + #[view] + public fun get_config(_oapp: address, _eid: u32, _config_type: u32): vector { + abort ENOT_IMPLEMENTED + } + + #[view] + public fun version(): (u64 /*major*/, u8 /*minor*/, u8 /*endpoint_version*/) { + (0xffffffffffffffff, 0xff, 2) + } + + #[view] + public fun is_supported_send_eid(_eid: u32): bool { + true + } + + #[view] + public fun is_supported_receive_eid(_eid: u32): bool { + true + } +} diff --git a/packages/layerzero-v2/aptos/contracts/msglib/libs/simple_msglib/Move.toml b/packages/layerzero-v2/aptos/contracts/msglib/libs/simple_msglib/Move.toml new file mode 100644 index 00000000..b79f7d8e --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/msglib/libs/simple_msglib/Move.toml @@ -0,0 +1,26 @@ +[package] +name = "simple_msglib" +version = "1.0.0" +authors = [] + +[addresses] +simple_msglib = "_" +endpoint_v2 = "_" +layerzero_treasury_admin = "_" +endpoint_v2_common = "_" +layerzero_admin = "_" + +[dev-addresses] +simple_msglib = "0x9002" +endpoint_v2 = "0x12345678" +layerzero_treasury_admin = "0x1894312499" +endpoint_v2_common = "0x9098" +layerzero_admin = "0x18943124" + +[dependencies.AptosFramework] +git = "https://github.com/aptos-labs/aptos-framework.git" +rev = "mainnet" +subdir = "aptos-framework" + +[dependencies] +endpoint_v2_common = { local = "../../../endpoint_v2_common" } \ No newline at end of file diff --git a/packages/layerzero-v2/aptos/contracts/msglib/libs/simple_msglib/sources/msglib.move b/packages/layerzero-v2/aptos/contracts/msglib/libs/simple_msglib/sources/msglib.move new file mode 100644 index 00000000..b9917780 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/msglib/libs/simple_msglib/sources/msglib.move @@ -0,0 +1,39 @@ +module simple_msglib::msglib { + const ADMIN: address = @layerzero_admin; + + // Simple message lib has globally fixed fees + struct SimpleMessageLibConfig has key { + native_fee: u64, + zro_fee: u64, + } + + fun init_module(account: &signer) { + move_to(move account, SimpleMessageLibConfig { native_fee: 0, zro_fee: 0 }); + } + + #[test_only] + public fun initialize_for_test() { + init_module(&std::account::create_signer_for_test(@simple_msglib)); + } + + public entry fun set_messaging_fee( + account: &signer, + native_fee: u64, + zro_fee: u64, + ) acquires SimpleMessageLibConfig { + assert!(std::signer::address_of(move account) == ADMIN, EUNAUTHORIZED); + let msglib_config = borrow_global_mut(@simple_msglib); + msglib_config.native_fee = native_fee; + msglib_config.zro_fee = zro_fee; + } + + #[view] + public fun get_messaging_fee(): (u64, u64) acquires SimpleMessageLibConfig { + let msglib_config = borrow_global(@simple_msglib); + (msglib_config.native_fee, msglib_config.zro_fee) + } + + // ================================================== Error Codes ================================================= + + const EUNAUTHORIZED: u64 = 1; +} diff --git a/packages/layerzero-v2/aptos/contracts/msglib/libs/simple_msglib/sources/router_calls.move b/packages/layerzero-v2/aptos/contracts/msglib/libs/simple_msglib/sources/router_calls.move new file mode 100644 index 00000000..7a60fce7 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/msglib/libs/simple_msglib/sources/router_calls.move @@ -0,0 +1,113 @@ +module simple_msglib::router_calls { + use std::any::Any; + use std::fungible_asset::FungibleAsset; + use std::option::{Self, Option}; + + use endpoint_v2_common::bytes32::{Self, Bytes32}; + use endpoint_v2_common::contract_identity::{DynamicCallRef, get_dynamic_call_ref_caller}; + use endpoint_v2_common::packet_raw::RawPacket; + use endpoint_v2_common::packet_v1_codec; + use endpoint_v2_common::send_packet::{Self, SendPacket}; + use endpoint_v2_common::universal_config; + use simple_msglib::msglib; + + public fun quote(_packet: SendPacket, _options: vector, pay_in_zro: bool): (u64, u64) { + let (native_fee, zro_fee) = msglib::get_messaging_fee(); + if (pay_in_zro) (0, zro_fee) else (native_fee, 0) + } + + public fun send( + call_ref: &DynamicCallRef, + packet: SendPacket, + _options: vector, + _native_token: &mut FungibleAsset, + zro_token: &mut Option, + ): (u64, u64, RawPacket) { + assert!(get_dynamic_call_ref_caller(call_ref, @simple_msglib, b"send") == @endpoint_v2, EINVALID_CALLER); + + let ( + nonce, + src_eid, + sender, + dst_eid, + receiver, + guid, + message, + ) = send_packet::unpack_send_packet(packet); + + let (native_fee, zro_fee) = msglib::get_messaging_fee(); + let packet = packet_v1_codec::new_packet_v1( + src_eid, + sender, + dst_eid, + receiver, + nonce, + guid, + message, + ); + if (option::is_some(zro_token)) (0, zro_fee, packet) else (native_fee, 0, packet) + } + + public fun commit_verification( + call_ref: &DynamicCallRef, + packet_header: RawPacket, + _payload_hash: Bytes32, + _extra_data: vector, + ): (address, u32, Bytes32, u64) { + assert!( + get_dynamic_call_ref_caller(call_ref, @simple_msglib, b"commit_verification") == @endpoint_v2, + EINVALID_CALLER, + ); + + // Assert header + packet_v1_codec::assert_receive_header(&packet_header, universal_config::eid()); + + // No check for verifiable in simple message lib + + // Decode the header + let receiver = bytes32::to_address(packet_v1_codec::get_receiver(&packet_header)); + let src_eid = packet_v1_codec::get_src_eid(&packet_header); + let sender = packet_v1_codec::get_sender(&packet_header); + let nonce = packet_v1_codec::get_nonce(&packet_header); + (receiver, src_eid, sender, nonce) + } + + public fun dvn_verify(_call_ref: &DynamicCallRef, _params: Any) { + abort ENOT_IMPLEMENTED + } + + public fun set_config( + _call_ref: &DynamicCallRef, + __oapp: address, + _eid: u32, + _config_type: u32, + _config: vector + ) { + abort ENOT_IMPLEMENTED + } + + #[view] + public fun get_config(_oapp: address, _eid: u32, _config_type: u32): vector { + abort ENOT_IMPLEMENTED + } + + #[view] + public fun version(): (u64 /*major*/, u8 /*minor*/, u8 /*endpoint_version*/) { + (0, 0, 2) + } + + #[view] + public fun is_supported_send_eid(_eid: u32): bool { + true + } + + #[view] + public fun is_supported_receive_eid(_eid: u32): bool { + true + } + + // ================================================== Error Codes ================================================= + + const EINVALID_CALLER: u64 = 1; + const ENOT_IMPLEMENTED: u64 = 2; +} diff --git a/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/Move.toml b/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/Move.toml new file mode 100644 index 00000000..726332dd --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/Move.toml @@ -0,0 +1,53 @@ +[package] +name = "uln_302" +version = "1.0.0" +authors = [] + +[addresses] +uln_302 = "_" +endpoint_v2_common = "_" +endpoint_v2 = "_" +layerzero_admin = "_" +layerzero_treasury_admin = "_" + +msglib_types = "_" +treasury = "_" +worker_common = "_" +price_feed_router_0 = "_" +price_feed_router_1 = "_" +executor_fee_lib_0 = "_" +dvn_fee_lib_0 = "_" +executor_fee_lib_router_0 = "_" +executor_fee_lib_router_1 = "_" +dvn_fee_lib_router_0 = "_" +dvn_fee_lib_router_1 = "_" +dvn = "_" + +[dev-addresses] +uln_302 = "0x9005" +endpoint_v2_common = "0x9098" +endpoint_v2 = "0x12345678" +layerzero_admin = "0x18943124" +layerzero_treasury_admin = "0x1894312499" +msglib_types = "0x97329841" +treasury = "0x123432432" +worker_common = "0x3999" +price_feed_router_0 = "0x65DD71A" +price_feed_router_1 = "0x65DD71AB" +price_feed_module_0 = "0x65DD71" +executor_fee_lib_0 = "0x13245135" +dvn_fee_lib_0 = "0x13245135a" +executor_fee_lib_router_0 = "0x30001" +executor_fee_lib_router_1 = "0x30002" +dvn_fee_lib_router_0 = "0x30001a" +dvn_fee_lib_router_1 = "0x30002a" +dvn = "0x324241" + +[dependencies] +msglib_types = { local = "../../msglib_types" } +endpoint_v2_common = { local = "../../../endpoint_v2_common" } +treasury = { local = "../../../treasury" } +executor_fee_lib_router_0 = { local = "../../../worker_peripherals/fee_lib_routers/executor_fee_lib_router_0" } +dvn_fee_lib_router_0 = { local = "../../../worker_peripherals/fee_lib_routers/dvn_fee_lib_router_0" } + +[dev-dependencies] diff --git a/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/sources/admin.move b/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/sources/admin.move new file mode 100644 index 00000000..bdd57df2 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/sources/admin.move @@ -0,0 +1,78 @@ +/// Entrypoint for administrative functions for the ULN 302 module. The functions in this module can only be called by +/// ULN administrators +module uln_302::admin { + use std::signer::address_of; + + use msglib_types::configs_executor::new_executor_config; + use msglib_types::configs_uln::new_uln_config; + use uln_302::configuration; + + /// Sets the default send config for an EID + public entry fun set_default_uln_send_config( + account: &signer, + dst_eid: u32, + confirmations: u64, + optional_dvn_threshold: u8, + required_dvns: vector
, + optional_dvns: vector
, + ) { + assert_admin(address_of(move account)); + configuration::set_default_send_uln_config( + dst_eid, + new_uln_config( + confirmations, + optional_dvn_threshold, + required_dvns, + optional_dvns, + false, + false, + false, + ) + ) + } + + /// Sets the default receive config for an EID + public entry fun set_default_uln_receive_config( + account: &signer, + src_eid: u32, + confirmations: u64, + optional_dvn_threshold: u8, + required_dvns: vector
, + optional_dvns: vector
, + ) { + assert_admin(address_of(move account)); + configuration::set_default_receive_uln_config( + src_eid, + new_uln_config( + confirmations, + optional_dvn_threshold, + required_dvns, + optional_dvns, + false, + false, + false, + ) + ) + } + + /// Sets the default executor config for an EID + public entry fun set_default_executor_config( + account: &signer, + eid: u32, + max_message_size: u32, + executor_address: address, + ) { + assert_admin(address_of(move account)); + let config = new_executor_config(max_message_size, executor_address); + configuration::set_default_executor_config(eid, config) + } + + /// Internal function to assert that the caller is the admin + fun assert_admin(admin: address) { + assert!(admin == @layerzero_admin, ENOT_AUTHORIZED); + } + + // ================================================== Error Codes ================================================= + + const ENOT_AUTHORIZED: u64 = 1; +} diff --git a/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/sources/internal/assert_valid_default_uln_config.move b/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/sources/internal/assert_valid_default_uln_config.move new file mode 100644 index 00000000..9794e384 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/sources/internal/assert_valid_default_uln_config.move @@ -0,0 +1,51 @@ +module uln_302::assert_valid_default_uln_config { + use endpoint_v2_common::assert_no_duplicates; + use msglib_types::configs_uln::{Self, get_optional_dvn_threshold, UlnConfig}; + + friend uln_302::configuration; + + #[test_only] + friend uln_302::assert_valid_default_uln_config_tests; + + const MAX_DVNS: u64 = 127; + + public(friend) fun assert_valid_default_uln_config(config: &UlnConfig) { + let use_default_for_required_dvns = configs_uln::get_use_default_for_required_dvns(config); + let use_default_for_optional_dvns = configs_uln::get_use_default_for_optional_dvns(config); + let use_default_for_confirmations = configs_uln::get_use_default_for_confirmations(config); + assert!(!use_default_for_required_dvns, EREQUESTING_USE_DEFAULT_REQUIRED_DVNS_FOR_DEFAULT_CONFIG); + assert!(!use_default_for_optional_dvns, EREQUESTING_USE_DEFAULT_OPTIONAL_DVNS_FOR_DEFAULT_CONFIG); + assert!(!use_default_for_confirmations, EREQUESTING_USE_DEFAULT_CONFIRMATIONS_FOR_DEFAULT_CONFIG); + let required_dvn_count = configs_uln::get_required_dvn_count(config); + let optional_dvn_count = configs_uln::get_optional_dvn_count(config); + let optional_dvn_threshold = get_optional_dvn_threshold(config); + + // Make sure there is an effective DVN threshold (required + optional threshold) >= 1 + assert!(required_dvn_count > 0 || optional_dvn_threshold > 0, ENO_EFFECTIVE_DVN_THRESHOLD); + + // Optional threshold should not be greater than count of optional dvns + assert!((optional_dvn_threshold as u64) <= optional_dvn_count, EOPTIONAL_DVN_THRESHOLD_EXCEEDS_COUNT); + + // If there are optional dvns, there should be an effective threshold + assert!(optional_dvn_count == 0 || optional_dvn_threshold > 0, EINVALID_DVN_THRESHOLD); + + // Make sure there are no duplicates in the required and optional DVNs + // This does not check for duplicates across required and optional DVN lists because of unclear outcomes + // with partial default configurations. The admin should take care to avoid duplicates between lists + assert!(required_dvn_count <= MAX_DVNS, ETOO_MANY_REQUIRED_DVNS); + assert!(optional_dvn_count <= MAX_DVNS, ETOO_MANY_OPTIONAL_DVNS); + assert_no_duplicates::assert_no_duplicates(&configs_uln::get_required_dvns(config)); + assert_no_duplicates::assert_no_duplicates(&configs_uln::get_optional_dvns(config)); + } + + // ================================================== Error Codes ================================================= + + const EINVALID_DVN_THRESHOLD: u64 = 1; + const ENO_EFFECTIVE_DVN_THRESHOLD: u64 = 2; + const EOPTIONAL_DVN_THRESHOLD_EXCEEDS_COUNT: u64 = 3; + const EREQUESTING_USE_DEFAULT_CONFIRMATIONS_FOR_DEFAULT_CONFIG: u64 = 4; + const EREQUESTING_USE_DEFAULT_OPTIONAL_DVNS_FOR_DEFAULT_CONFIG: u64 = 5; + const EREQUESTING_USE_DEFAULT_REQUIRED_DVNS_FOR_DEFAULT_CONFIG: u64 = 6; + const ETOO_MANY_OPTIONAL_DVNS: u64 = 7; + const ETOO_MANY_REQUIRED_DVNS: u64 = 8; +} diff --git a/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/sources/internal/assert_valid_uln_config.move b/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/sources/internal/assert_valid_uln_config.move new file mode 100644 index 00000000..206dfd15 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/sources/internal/assert_valid_uln_config.move @@ -0,0 +1,65 @@ +module uln_302::assert_valid_uln_config { + use endpoint_v2_common::assert_no_duplicates; + use msglib_types::configs_uln::{Self, UlnConfig}; + + friend uln_302::configuration; + + #[test_only] + friend uln_302::assert_valid_oapp_uln_config_tests; + + const MAX_DVNS: u64 = 127; + + public(friend) fun assert_valid_uln_config(oapp_config: &UlnConfig, default_config: &UlnConfig) { + let use_default_for_required_dvns = configs_uln::get_use_default_for_required_dvns(oapp_config); + let use_default_for_optional_dvns = configs_uln::get_use_default_for_optional_dvns(oapp_config); + let use_default_for_confirmations = configs_uln::get_use_default_for_confirmations(oapp_config); + let required_dvn_count = configs_uln::get_required_dvn_count(oapp_config); + let optional_dvn_count = configs_uln::get_optional_dvn_count(oapp_config); + let optional_dvn_threshold = configs_uln::get_optional_dvn_threshold(oapp_config); + if (use_default_for_required_dvns) { + assert!(required_dvn_count == 0, ENONEMPTY_REQUIRED_DVNS_WITH_USE_DEFAULT); + }; + if (use_default_for_optional_dvns) { + assert!(optional_dvn_count == 0, ENONEMPTY_OPTIONAL_DVNS_WITH_USE_DEFAULT); + }; + if (use_default_for_confirmations) { + assert!(configs_uln::get_confirmations(oapp_config) == 0, + ENONZERO_CONFIRMATIONS_PROVIDED_FOR_DEFAULT_CONFIG, + ); + }; + + assert!(required_dvn_count <= MAX_DVNS, ETOO_MANY_REQUIRED_DVNS); + assert!(optional_dvn_count <= MAX_DVNS, ETOO_MANY_OPTIONAL_DVNS); + + // Make sure there are no duplicates in the required and optional DVNs + // The admin should take care to avoid duplicates between lists, which is not checked here + assert_no_duplicates::assert_no_duplicates(&configs_uln::get_required_dvns(oapp_config)); + assert_no_duplicates::assert_no_duplicates(&configs_uln::get_optional_dvns(oapp_config)); + + // Optional threshold should not be greater than count of optional dvns + assert!((optional_dvn_threshold as u64) <= optional_dvn_count, EOPTIONAL_DVN_THRESHOLD_EXCEEDS_COUNT); + + // If there are optional dvns, there should be an effective threshold + assert!(optional_dvn_count == 0 || optional_dvn_threshold > 0, EINVALID_DVN_THRESHOLD); + + // make sure there is an effective DVN threshold (required + optional threshold) >= 1 + let effective_optional_threshold = if (!use_default_for_optional_dvns) optional_dvn_threshold else { + configs_uln::get_optional_dvn_threshold(default_config) + }; + let effective_required_count = if (!use_default_for_required_dvns) required_dvn_count else { + configs_uln::get_required_dvn_count(default_config) + }; + assert!(effective_optional_threshold > 0 || effective_required_count > 0, ENO_EFFECTIVE_DVN_THRESHOLD); + } + + // ================================================== Error Codes ================================================= + + const EINVALID_DVN_THRESHOLD: u64 = 1; + const ENONEMPTY_OPTIONAL_DVNS_WITH_USE_DEFAULT: u64 = 2; + const ENONEMPTY_REQUIRED_DVNS_WITH_USE_DEFAULT: u64 = 3; + const ENONZERO_CONFIRMATIONS_PROVIDED_FOR_DEFAULT_CONFIG: u64 = 4; + const ENO_EFFECTIVE_DVN_THRESHOLD: u64 = 5; + const EOPTIONAL_DVN_THRESHOLD_EXCEEDS_COUNT: u64 = 6; + const ETOO_MANY_OPTIONAL_DVNS: u64 = 7; + const ETOO_MANY_REQUIRED_DVNS: u64 = 8; +} \ No newline at end of file diff --git a/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/sources/internal/configuration.move b/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/sources/internal/configuration.move new file mode 100644 index 00000000..bf3e8939 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/sources/internal/configuration.move @@ -0,0 +1,304 @@ +/// This module handles the configuration of the ULN for both the send and receive sides +module uln_302::configuration { + use std::event::emit; + use std::option; + use std::vector; + + use endpoint_v2_common::serde; + use msglib_types::configs_executor::{Self, ExecutorConfig, new_executor_config}; + use msglib_types::configs_uln::{ + Self, get_confirmations, get_optional_dvn_threshold, get_optional_dvns, get_required_dvns, + get_use_default_for_confirmations, get_use_default_for_optional_dvns, get_use_default_for_required_dvns, + new_uln_config, UlnConfig, + }; + use uln_302::assert_valid_default_uln_config::assert_valid_default_uln_config; + use uln_302::assert_valid_uln_config::assert_valid_uln_config; + use uln_302::uln_302_store; + + friend uln_302::sending; + friend uln_302::verification; + friend uln_302::msglib; + friend uln_302::admin; + friend uln_302::router_calls; + + #[test_only] + friend uln_302::verification_tests; + + #[test_only] + friend uln_302::msglib_tests; + + #[test_only] + friend uln_302::configuration_tests; + + // This is needed for tests of inline functions in the sending module, which call friend functions in this module + #[test_only] + friend uln_302::sending_tests; + + // Configuration Types as used in this message library + public inline fun CONFIG_TYPE_EXECUTOR(): u32 { 1 } + + public inline fun CONFIG_TYPE_SEND_ULN(): u32 { 2 } + + public inline fun CONFIG_TYPE_RECV_ULN(): u32 { 3 } + + const ULN_SEND_SIDE: u8 = 0; + const ULN_RECEIVE_SIDE: u8 = 1; + + // ===================================================== OApp ===================================================== + + /// Gets the serialized configuration for an OApp eid and config type, returning the default if the OApp + /// configuration is not set + public(friend) fun get_config(oapp: address, eid: u32, config_type: u32): vector { + if (config_type == CONFIG_TYPE_SEND_ULN()) { + let config = get_send_uln_config(oapp, eid); + serde::bytes_of(|buf| configs_uln::append_uln_config(buf, config)) + } else if (config_type == CONFIG_TYPE_RECV_ULN()) { + let config = get_receive_uln_config(oapp, eid); + serde::bytes_of(|buf| configs_uln::append_uln_config(buf, config)) + } else if (config_type == CONFIG_TYPE_EXECUTOR()) { + let config = get_executor_config(oapp, eid); + serde::bytes_of(|buf| configs_executor::append_executor_config(buf, config)) + } else { + abort EUNKNOWN_CONFIG_TYPE + } + } + + public(friend) fun set_config(oapp: address, eid: u32, config_type: u32, config: vector) { + let pos = 0; + if (config_type == CONFIG_TYPE_SEND_ULN()) { + let uln_config = configs_uln::extract_uln_config(&config, &mut pos); + set_send_uln_config(oapp, eid, uln_config); + } else if (config_type == CONFIG_TYPE_RECV_ULN()) { + let uln_config = configs_uln::extract_uln_config(&config, &mut pos); + set_receive_uln_config(oapp, eid, uln_config); + } else if (config_type == CONFIG_TYPE_EXECUTOR()) { + let executor_config = configs_executor::extract_executor_config(&config, &mut pos); + set_executor_config(oapp, eid, executor_config); + } else { + abort EUNKNOWN_CONFIG_TYPE + }; + + // If the entire config was not consumed, the config is invalid + assert!(vector::length(&config) == pos, EINVALID_CONFIG_LENGTH); + } + + // ================================================= Receive Side ================================================= + + public(friend) fun supports_receive_eid(eid: u32): bool { + uln_302_store::has_default_receive_uln_config(eid) + } + + public(friend) fun set_default_receive_uln_config(eid: u32, config: UlnConfig) { + assert_valid_default_uln_config(&config); + uln_302_store::set_default_receive_uln_config(eid, config); + emit(DefaultUlnConfigSet { eid, config, send_or_receive: ULN_RECEIVE_SIDE }); + } + + public(friend) fun set_receive_uln_config(receiver: address, eid: u32, config: UlnConfig) { + let default = uln_302_store::get_default_receive_uln_config(eid); + assert!(option::is_some(&default), EEID_NOT_CONFIGURED); + assert_valid_uln_config(&config, option::borrow(&default)); + uln_302_store::set_receive_uln_config(receiver, eid, config); + emit(UlnConfigSet { oapp: receiver, eid, config, send_or_receive: ULN_RECEIVE_SIDE }); + } + + public(friend) fun get_receive_uln_config(receiver: address, eid: u32): UlnConfig { + let oapp_config = uln_302_store::get_receive_uln_config(receiver, eid); + let default_config = uln_302_store::get_default_receive_uln_config(eid); + + if (option::is_some(&oapp_config) && option::is_some(&default_config)) { + merge_uln_configs(option::borrow(&default_config), option::borrow(&oapp_config)) + } else if (option::is_some(&default_config)) { + option::extract(&mut default_config) + } else { + abort EEID_NOT_CONFIGURED + } + } + + // =================================================== Send Side ================================================== + + public(friend) fun supports_send_eid(eid: u32): bool { + uln_302_store::has_default_send_uln_config(eid) + } + + public(friend) fun set_default_send_uln_config(eid: u32, config: UlnConfig) { + assert_valid_default_uln_config(&config); + uln_302_store::set_default_send_uln_config(eid, config); + emit(DefaultUlnConfigSet { eid, config, send_or_receive: ULN_SEND_SIDE }); + } + + public(friend) fun set_send_uln_config(oapp: address, eid: u32, config: UlnConfig) { + let default = uln_302_store::get_default_send_uln_config(eid); + assert!(option::is_some(&default), EEID_NOT_CONFIGURED); + assert_valid_uln_config(&config, option::borrow(&default)); + uln_302_store::set_send_uln_config(oapp, eid, config); + emit(UlnConfigSet { oapp, eid, config, send_or_receive: ULN_SEND_SIDE }); + } + + public(friend) fun get_send_uln_config(sender: address, eid: u32): UlnConfig { + let oapp_config = uln_302_store::get_send_uln_config(sender, eid); + let default_config = uln_302_store::get_default_send_uln_config(eid); + + if (option::is_some(&oapp_config) && option::is_some(&default_config)) { + merge_uln_configs(option::borrow(&default_config), option::borrow(&oapp_config)) + } else if (option::is_some(&default_config)) { + option::extract(&mut default_config) + } else { + abort EEID_NOT_CONFIGURED + } + } + + // =================================================== Executor =================================================== + + public(friend) fun get_executor_config(sender: address, eid: u32): ExecutorConfig { + let oapp_config = uln_302_store::get_executor_config(sender, eid); + let default_config = uln_302_store::get_default_executor_config(eid); + + if (option::is_some(&oapp_config) && option::is_some(&default_config)) { + // get correct merged config if oapp and default are both set + merge_executor_configs(option::borrow(&default_config), option::borrow(&oapp_config)) + } else if (option::is_some(&default_config)) { + option::extract(&mut default_config) + } else { + abort EEID_NOT_CONFIGURED + } + } + + public(friend) fun set_default_executor_config(eid: u32, config: ExecutorConfig) { + assert_valid_default_executor_config(&config); + uln_302_store::set_default_executor_config(eid, config); + emit(DefaultExecutorConfigSet { eid, config }); + } + + public(friend) fun set_executor_config(sender: address, eid: u32, config: ExecutorConfig) { + uln_302_store::set_executor_config(sender, eid, config); + emit(ExecutorConfigSet { oapp: sender, eid, config }); + } + + public(friend) fun merge_executor_configs( + default_config: &ExecutorConfig, + oapp_config: &ExecutorConfig, + ): ExecutorConfig { + let default_max_message_size = configs_executor::get_max_message_size(default_config); + let default_executor_address = configs_executor::get_executor_address(default_config); + let oapp_max_message_size = configs_executor::get_max_message_size(oapp_config); + let oapp_executor_address = configs_executor::get_executor_address(oapp_config); + + new_executor_config( + if (oapp_max_message_size > 0) { oapp_max_message_size } else { default_max_message_size }, + if (oapp_executor_address != @0x0) { oapp_executor_address } else { default_executor_address } + ) + } + + public(friend) fun assert_valid_default_executor_config(config: &ExecutorConfig) { + let max_message_size = configs_executor::get_max_message_size(config); + let executor_address = configs_executor::get_executor_address(config); + assert!(max_message_size > 0, EMAX_MESSAGE_SIZE_ZERO); + assert!(executor_address != @0x0, EEXECUTOR_ADDRESS_IS_ZERO); + } + + // =================================================== Internal =================================================== + + /// Merges parts of the oapp config with the default config, where specified + public(friend) fun merge_uln_configs(default_config: &UlnConfig, oapp_config: &UlnConfig): UlnConfig { + let default_for_confirmations = get_use_default_for_confirmations(oapp_config); + let default_for_required = get_use_default_for_required_dvns(oapp_config); + let default_for_optional = get_use_default_for_optional_dvns(oapp_config); + + // handle situations where there the configuration requests a fallback to default + let optional_dvn_threshold = if (!default_for_optional) { + get_optional_dvn_threshold(oapp_config) + } else { + get_optional_dvn_threshold(default_config) + }; + let optional_dvns = if (!default_for_optional) { + get_optional_dvns(oapp_config) + } else { + get_optional_dvns(default_config) + }; + let required_dvns = if (!default_for_required) { + get_required_dvns(oapp_config) + } else { + get_required_dvns(default_config) + }; + + // Check that there is at least one DVN configured. This is also checked on setting the configuration, but this + // could cease to be the case if the default configuration is changed after the OApp configuration is set. + assert!((optional_dvn_threshold as u64) + vector::length(&required_dvns) > 0, EINSUFFICIENT_DVNS_CONFIGURED); + + let confirmations = if (!default_for_confirmations) { + get_confirmations(oapp_config) + } else { + get_confirmations(default_config) + }; + + new_uln_config( + confirmations, + optional_dvn_threshold, + required_dvns, + optional_dvns, + default_for_confirmations, + default_for_required, + default_for_optional, + ) + } + + // ==================================================== Events ==================================================== + + #[event] + struct DefaultUlnConfigSet has store, drop { + eid: u32, + config: UlnConfig, + send_or_receive: u8 + } + + #[event] + struct DefaultExecutorConfigSet has store, drop { + eid: u32, + config: ExecutorConfig, + } + + #[event] + struct UlnConfigSet has store, drop { + oapp: address, + eid: u32, + config: UlnConfig, + send_or_receive: u8 + } + + #[event] + struct ExecutorConfigSet has store, drop { + oapp: address, + eid: u32, + config: ExecutorConfig, + } + + #[test_only] + public fun default_uln_config_set_event(eid: u32, config: UlnConfig, send_or_receive: u8): DefaultUlnConfigSet { + DefaultUlnConfigSet { eid, config, send_or_receive } + } + + #[test_only] + public fun default_executor_config_set_event(eid: u32, config: ExecutorConfig): DefaultExecutorConfigSet { + DefaultExecutorConfigSet { eid, config } + } + + #[test_only] + public fun uln_config_set_event(oapp: address, eid: u32, config: UlnConfig, send_or_receive: u8): UlnConfigSet { + UlnConfigSet { oapp, eid, config, send_or_receive } + } + + #[test_only] + public fun executor_config_set_event(oapp: address, eid: u32, config: ExecutorConfig): ExecutorConfigSet { + ExecutorConfigSet { oapp, eid, config } + } + + // ================================================== Error Codes ================================================= + + const EEID_NOT_CONFIGURED: u64 = 1; + const EEXECUTOR_ADDRESS_IS_ZERO: u64 = 2; + const EINVALID_CONFIG_LENGTH: u64 = 3; + const EINSUFFICIENT_DVNS_CONFIGURED: u64 = 4; + const EMAX_MESSAGE_SIZE_ZERO: u64 = 5; + const EUNKNOWN_CONFIG_TYPE: u64 = 6; +} diff --git a/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/sources/internal/for_each_dvn.move b/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/sources/internal/for_each_dvn.move new file mode 100644 index 00000000..0a4859ab --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/sources/internal/for_each_dvn.move @@ -0,0 +1,30 @@ +module uln_302::for_each_dvn { + use std::vector; + + friend uln_302::verification; + friend uln_302::sending; + + #[test_only] + friend uln_302::for_each_dvn_tests; + + /// Loop through each dvn and provide the dvn and the dvn index + /// If a required dvn is duplicated as an optional dvn, it will be called twice + public(friend) inline fun for_each_dvn( + required_dvns: &vector
, + optional_dvns: &vector
, + f: |address, u64|(), + ) { + let count_required = vector::length(required_dvns); + let count_optional = vector::length(optional_dvns); + + for (i in 0..(count_required + count_optional)) { + let dvn = if (i < count_required) { + vector::borrow(required_dvns, i) + } else { + vector::borrow(optional_dvns, i - count_required) + }; + + f(*dvn, i) + } + } +} diff --git a/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/sources/internal/sending.move b/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/sources/internal/sending.move new file mode 100644 index 00000000..a460107f --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/sources/internal/sending.move @@ -0,0 +1,506 @@ +module uln_302::sending { + use std::event; + use std::fungible_asset::{Self, FungibleAsset}; + use std::option::{Self, Option}; + use std::primary_fungible_store; + use std::vector; + + use dvn_fee_lib_router_0::dvn_fee_lib_router::get_dvn_fee as fee_lib_router_get_dvn_fee; + use endpoint_v2_common::bytes32::{Self, Bytes32}; + use endpoint_v2_common::packet_raw::{Self, RawPacket}; + use endpoint_v2_common::packet_v1_codec; + use endpoint_v2_common::send_packet::{Self, SendPacket, unpack_send_packet}; + use executor_fee_lib_router_0::executor_fee_lib_router::get_executor_fee as fee_lib_router_get_executor_fee; + use msglib_types::configs_executor; + use msglib_types::configs_uln::{Self, unpack_uln_config}; + use msglib_types::worker_options::get_matching_options; + use treasury::treasury; + use uln_302::configuration; + use uln_302::for_each_dvn::for_each_dvn; + use uln_302::msglib::worker_config_for_fee_lib_routing_opt_in; + use worker_common::worker_config; + + friend uln_302::router_calls; + + #[test_only] + friend uln_302::sending_tests; + + // ==================================================== Sending =================================================== + + /// Makes payment to workers and triggers the packet send + public(friend) fun send( + packet: SendPacket, + options: vector, + native_token: &mut FungibleAsset, + zro_token: &mut Option, + ): (u64, u64, RawPacket) { + let dst_eid = send_packet::get_dst_eid(&packet); + let sender = bytes32::to_address(send_packet::get_sender(&packet)); + let message_length = send_packet::get_message_length(&packet); + send_internal( + packet, + options, + native_token, + zro_token, + // Get Executor Fee + |executor_address, executor_options| fee_lib_router_get_executor_fee( + @uln_302, + get_worker_fee_lib(executor_address), + executor_address, + dst_eid, + sender, + message_length, + executor_options, + ), + // Get DVN Fee + |dvn, confirmations, dvn_options, packet_header, payload_hash| fee_lib_router_get_dvn_fee( + @uln_302, + get_worker_fee_lib(dvn), + dvn, + dst_eid, + sender, + packet_raw::get_packet_bytes(packet_header), + bytes32::from_bytes32(payload_hash), + confirmations, + dvn_options, + ) + ) + } + + /// Internal function to send a packet. The fee library function calls are injected as parameters so that this + /// behavior can be mocked in tests + /// + /// @param packet: the packet details that will be sent + /// @param options: combined executor and DVN options for the packet + /// @param native_token: the native token fungible asset mutable ref to pay the workers + /// @param zro_token: optional ZRO token fungible asset mutable ref to pay the workers + /// @param get_executor_fee: function that gets the fee for the executor + /// |executor, executor_options| (fee, deposit_address) + /// @param get_dvn_fee: function that gets the fee for one DVN + /// |dvn, confirmations, dvn_option, packet_header, payload_hash| (fee, deposit_address) + public(friend) inline fun send_internal( + packet: SendPacket, + options: vector, + native_token: &mut FungibleAsset, + zro_token: &mut Option, + get_executor_fee: |address, vector| (u64, address), + get_dvn_fee: |address, u64, vector, RawPacket, Bytes32| (u64, address), + ): (u64 /*native*/, u64 /*zro*/, RawPacket /*encoded_packet*/) { + // Convert the Send Packet in the a Packet of the Codec V1 format + let (nonce, src_eid, sender, dst_eid, receiver, guid, message) = unpack_send_packet(packet); + let sender_address = bytes32::to_address(sender); + let raw_packet = packet_v1_codec::new_packet_v1( + src_eid, + sender, + dst_eid, + receiver, + nonce, + guid, + message, + ); + + // Calculate and Pay Worker Fees (DVNs and Executor) + let packet_header = packet_v1_codec::extract_header(&raw_packet); + let payload_hash = packet_v1_codec::get_payload_hash(&raw_packet); + let message_length = vector::length(&message); + let worker_native_fee = pay_workers( + sender_address, + dst_eid, + message_length, + options, + native_token, + // Get Executor Fee + |executor, executor_options| get_executor_fee(executor, executor_options), + // Get DVN Fee - Partially apply the parameters that are known in this scope + |dvn, confirmations, dvn_options| get_dvn_fee( + dvn, + confirmations, + dvn_options, + packet_header, + payload_hash, + ), + ); + + // Calculate and Pay Treasury Fee + let treasury_zro_fee = 0; + let treasury_native_fee = 0; + if (option::is_some(zro_token)) { + treasury_zro_fee = treasury::pay_fee(worker_native_fee, option::borrow_mut(zro_token)); + } else { + treasury_native_fee = treasury::pay_fee(worker_native_fee, native_token); + }; + + // Return the total Native and ZRO paid + let total_native_fee = worker_native_fee + treasury_native_fee; + (total_native_fee, treasury_zro_fee, raw_packet) + } + + // ==================================================== Quoting =================================================== + + // Quote the price of a send in native and ZRO tokens (if `pay_in_zro` is true) + public(friend) fun quote(packet: SendPacket, options: vector, pay_in_zro: bool): (u64, u64) { + let dst_eid = send_packet::get_dst_eid(&packet); + let sender = bytes32::to_address(send_packet::get_sender(&packet)); + let message_length = send_packet::get_message_length(&packet); + + quote_internal( + packet, + options, + pay_in_zro, + // Get Executor Fee + |executor_address, executor_options| fee_lib_router_get_executor_fee( + @uln_302, + get_worker_fee_lib(executor_address), + executor_address, + dst_eid, + sender, + message_length, + executor_options, + ), + // Get DVN Fee + |dvn, confirmations, dvn_options, packet_header, payload_hash| fee_lib_router_get_dvn_fee( + @uln_302, + get_worker_fee_lib(dvn), + dvn, + dst_eid, + sender, + packet_raw::get_packet_bytes(packet_header), + bytes32::from_bytes32(payload_hash), + confirmations, + dvn_options, + ) + ) + } + + /// Provides a quote for sending a packet + /// + /// @param packet: the packet to be sent + /// @param options: combined executor and DVN options for the packet + /// @param pay_in_zro: whether the fees should be paid in ZRO or native token + /// @param get_executor_fee: function that gets the fee for the executor + /// |executor, executor_option| (fee, deposit_address) + /// @param get_dvn_fee function: that gets the fee for one DVN + /// |dvn, confirmations, dvn_option, packet_header, payload_hash| (fee, deposit_address) + public(friend) inline fun quote_internal( + packet: SendPacket, + options: vector, + pay_in_zro: bool, + get_executor_fee: |address, vector| (u64, address), + get_dvn_fee: |address, u64, vector, RawPacket, Bytes32| (u64, address), + ): (u64, u64) { + let (nonce, src_eid, sender, dst_eid, receiver, guid, message) = unpack_send_packet(packet); + let sender_address = bytes32::to_address(sender); + + // Extract the packet header and payload hash + let packet_header = packet_v1_codec::new_packet_v1_header_only( + src_eid, + sender, + dst_eid, + receiver, + nonce, + ); + + // Get the Executor Configuration + let executor_config = uln_302::configuration::get_executor_config(sender_address, dst_eid); + let executor_address = configs_executor::get_executor_address(&executor_config); + let max_msg_size = configs_executor::get_max_message_size(&executor_config); + + // Split provided options into executor and DVN options + let (executor_options, all_dvn_options) = msglib_types::worker_options::extract_and_split_options(&options); + + // Assert message size is less than what is configured for the OApp + assert!(vector::length(&message) <= (max_msg_size as u64), err_EMESSAGE_SIZE_EXCEEDS_MAX()); + + // Get Executor Fee + let (executor_fee, _deposit_address) = get_executor_fee(executor_address, executor_options); + + // Get Fee for all DVNs + let payload_hash = packet_v1_codec::compute_payload_hash(guid, message); + let config = configuration::get_send_uln_config(sender_address, dst_eid); + let (confirmations, _, required_dvns, optional_dvns, _, _, _) = unpack_uln_config(config); + let (dvn_fee, _dvn_fees, _dvn_deposit_addresses) = get_all_dvn_fees( + &required_dvns, + &optional_dvns, + all_dvn_options, + // Get DVN Fee - Partially apply the parameters that are known in this scope + |dvn, dvn_options| get_dvn_fee( + dvn, + confirmations, + dvn_options, + packet_header, + payload_hash, + ), + ); + + let worker_native_fee = executor_fee + dvn_fee; + + // Calculate Treasury Fee + let treasury_fee = treasury::get_fee(worker_native_fee, pay_in_zro); + let treasury_zro_fee = if (pay_in_zro) { treasury_fee } else { 0 }; + let treasury_native_fee = if (!pay_in_zro) { treasury_fee } else { 0 }; + + // Return Native and ZRO Fees + let total_native_fee = worker_native_fee + treasury_native_fee; + (total_native_fee, treasury_zro_fee) + } + + // ==================================================== DVN Fees ================================================== + + /// Calculates the fees for all the DVNs + /// + /// @param required_dvns: list of required DVNs + /// @param optional_dvns: list of optional DVNs + /// @param validation_options: concatinated options for all DVNs + /// @param get_dvn_fee: function that gets the fee for one DVN + /// |dvn, dvn_option| (u64, address) + /// @return (total_fee, dvn_fees, dvn_deposit_addresses) + public(friend) inline fun get_all_dvn_fees( + required_dvns: &vector
, + optional_dvns: &vector
, + validation_options: vector, // options for all dvns, + get_dvn_fee: |address, vector| (u64, address), + ): (u64, vector, vector
) { + let index_option_pairs = msglib_types::worker_options::group_dvn_options_by_index(&validation_options); + + // Calculate the fee for each DVN and accumulate total DVN fee + let total_dvn_fee = 0; + let dvn_fees = vector[]; + let dvn_deposit_addresses = vector
[]; + + for_each_dvn(required_dvns, optional_dvns, |dvn, i| { + let options = get_matching_options(&index_option_pairs, (i as u8)); + let (fee, deposit_address) = get_dvn_fee(dvn, options); + vector::push_back(&mut dvn_fees, fee); + vector::push_back(&mut dvn_deposit_addresses, deposit_address); + total_dvn_fee = total_dvn_fee + fee; + }); + + (total_dvn_fee, dvn_fees, dvn_deposit_addresses) + } + + // ==================================================== Payment =================================================== + + /// Pay the workers for the message + /// + /// @param sender the address of the sender + /// @param dst_eid the destination endpoint id + /// @param message_length the length of the message + /// @param options the options for the workers + /// @param native_token the native token fungible asset to pay the workers + /// @param get_executor_fee function that gets the fee for the Executor + /// |executor, executor_option| (fee, deposit_address) + /// @param get_dvn_fee function that gets the fee for one DVN + /// |dvn, confirmations, dvn_options| (fee, deposit_address) + public(friend) inline fun pay_workers( + sender: address, + dst_eid: u32, + message_length: u64, + options: vector, + native_token: &mut FungibleAsset, + get_executor_fee: |address, vector| (u64, address), + get_dvn_fee: |address, u64, vector| (u64, address), + ): u64 /*native fee*/ { + // Split serialized options into separately concatenated executor options and dvn options + let (executor_options, dvn_options) = msglib_types::worker_options::extract_and_split_options(&options); + + // Pay Executor + let executor_native_fee = pay_executor_and_assert_size( + sender, + native_token, + dst_eid, + message_length, + executor_options, + // Get Executor Fee + |executor, options| get_executor_fee(executor, options), + ); + + // Pay Verifier + let verifier_native_fee = pay_verifier( + sender, + dst_eid, + native_token, + dvn_options, + // Get DVN Fee + |dvn, confirmations, options| get_dvn_fee(dvn, confirmations, options) + ); + + let total_native_fee = executor_native_fee + verifier_native_fee; + total_native_fee + } + + /// Pay the executor for the message and check the size against the configured maximum + /// + /// @param sender: the address of the sender + /// @param native_token: the native token fungible asset to pay the executor + /// @param dst_eid: the destination endpoint id + /// @param message_length: the length of the message + /// @param executor_options: the options for the executor + /// @param get_executor_fee: function that gets the fee for the Executor + /// |executor, executor_option| (fee, deposit_address) + /// @return the fee paid to the executor + inline fun pay_executor_and_assert_size( + sender: address, + native_token: &mut FungibleAsset, + dst_eid: u32, + message_length: u64, + executor_options: vector, + get_executor_fee: |address, vector| (u64, address), + ): u64 { + // Load config + let executor_config = configuration::get_executor_config(sender, dst_eid); + let max_msg_size = configs_executor::get_max_message_size(&executor_config); + let executor_address = configs_executor::get_executor_address(&executor_config); + + // Assert message size is less than what is configured for the OApp + assert!(message_length <= (max_msg_size as u64), err_EMESSAGE_SIZE_EXCEEDS_MAX()); + + // Extract and Deposit the Executor Fee + let (fee, deposit_address) = get_executor_fee(executor_address, executor_options); + let fee_fa = fungible_asset::extract(native_token, fee); + primary_fungible_store::deposit(deposit_address, fee_fa); + + // Emit a record of the fee paid to the executor + emit_executor_fee_paid(executor_address, deposit_address, fee); + + fee + } + + /// Pay the DVNs for verification + /// Emits a DvnFeePaidEvent to notify the DVNs that they have been paid + /// + /// @param sender: the address of the sender + /// @param dst_eid: the destination endpoint id + /// @param native_token: the native token fungible asset to pay the DVNs + /// @param dvn_options: the options for the DVNs + /// @param get_dvn_fee: function that gets the dvn fee for one DVN + /// |dvn, confirmations, dvn_option| (fee, deposit_address) + /// @return the total fee paid to all DVNs + inline fun pay_verifier( + sender: address, + dst_eid: u32, + native_token: &mut FungibleAsset, + dvn_options: vector, + get_dvn_fee: |address, u64, vector| (u64, address), + ): u64 /*native_fee*/ { + let config = configuration::get_send_uln_config(sender, dst_eid); + let required_dvns = configs_uln::get_required_dvns(&config); + let optional_dvns = configs_uln::get_optional_dvns(&config); + let confirmations = configs_uln::get_confirmations(&config); + + let (total_fee, dvn_fees, dvn_deposit_addresses) = get_all_dvn_fees( + &required_dvns, + &optional_dvns, + dvn_options, + // Get DVN Fee - Partially apply the parameters that are known in this scope + |dvn, options| get_dvn_fee( + dvn, + confirmations, + options, + ), + ); + + // Deposit the appropriate fee into each of the DVN deposit addresses + for_each_dvn(&required_dvns, &optional_dvns, |_dvn, i| { + let fee = vector::borrow(&dvn_fees, i); + let deposit_address = *vector::borrow(&dvn_deposit_addresses, i); + let fee_fa = fungible_asset::extract(native_token, *fee); + primary_fungible_store::deposit(deposit_address, fee_fa); + }); + + // Emit a record of the fees paid to the DVNs + emit_dvn_fee_paid(required_dvns, optional_dvns, dvn_fees, dvn_deposit_addresses); + + total_fee + } + + // ==================================================== Routing =================================================== + + /// Provides the feelib address for a worker + /// Defer to worker_common::worker_config if opting in to worker config. Otherwise, use the worker itself. + fun get_worker_fee_lib(worker: address): address { + if (worker_config_for_fee_lib_routing_opt_in(worker)) { + worker_config::get_worker_fee_lib(worker) + } else { + worker + } + } + + // ==================================================== Events ==================================================== + + #[event] + struct DvnFeePaid has store, copy, drop { + required_dvns: vector
, + optional_dvns: vector
, + dvn_deposit_addresses: vector
, + dvn_fees: vector, + } + + #[event] + struct ExecutorFeePaid has store, copy, drop { + executor_address: address, + executor_deposit_address: address, + executor_fee: u64, + } + + public(friend) fun emit_executor_fee_paid( + executor_address: address, + executor_deposit_address: address, + executor_fee: u64, + ) { + event::emit(ExecutorFeePaid { + executor_address, + executor_deposit_address, + executor_fee, + }); + } + + public(friend) fun emit_dvn_fee_paid( + required_dvns: vector
, + optional_dvns: vector
, + dvn_fees: vector, + dvn_deposit_addresses: vector
, + ) { + event::emit(DvnFeePaid { + required_dvns, + optional_dvns, + dvn_fees, + dvn_deposit_addresses, + }); + } + + #[test_only] + public fun dvn_fee_paid_event( + required_dvns: vector
, + optional_dvns: vector
, + dvn_fees: vector, + dvn_deposit_addresses: vector
, + ): DvnFeePaid { + DvnFeePaid { + required_dvns, + optional_dvns, + dvn_fees, + dvn_deposit_addresses, + } + } + + #[test_only] + public fun executor_fee_paid_event( + executor_address: address, + executor_deposit_address: address, + executor_fee: u64, + ): ExecutorFeePaid { + ExecutorFeePaid { + executor_address, + executor_deposit_address, + executor_fee, + } + } + + // ================================================== Error Codes ================================================= + + const EMESSAGE_SIZE_EXCEEDS_MAX: u64 = 1; + + public(friend) fun err_EMESSAGE_SIZE_EXCEEDS_MAX(): u64 { + EMESSAGE_SIZE_EXCEEDS_MAX + } +} \ No newline at end of file diff --git a/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/sources/internal/uln_302_store.move b/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/sources/internal/uln_302_store.move new file mode 100644 index 00000000..0a634dff --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/sources/internal/uln_302_store.move @@ -0,0 +1,270 @@ +module uln_302::uln_302_store { + use std::option::{Self, Option}; + use std::table::{Self, Table}; + + use endpoint_v2_common::bytes32::Bytes32; + use msglib_types::configs_executor::ExecutorConfig; + use msglib_types::configs_uln::UlnConfig; + + friend uln_302::configuration; + friend uln_302::verification; + friend uln_302::msglib; + + #[test_only] + friend uln_302::configuration_tests; + + #[test_only] + friend uln_302::verification_tests; + + #[test_only] + friend uln_302::sending_tests; + + struct Store has key { + default_configs: Table, + oapp_configs: Table, + confirmations: Table, + worker_config_opt_in_out: Table, + } + + struct OAppEid has store, drop, copy { + oapp: address, + eid: u32, + } + + struct DefaultConfig has store, drop, copy { + executor_config: Option, + receive_uln_config: Option, + send_uln_config: Option, + } + + struct OAppConfig has store, drop, copy { + executor_config: Option, + receive_uln_config: Option, + send_uln_config: Option, + } + + struct ConfirmationsKey has store, drop, copy { + header_hash: Bytes32, + payload_hash: Bytes32, + dvn_address: address, + } + + fun init_module(account: &signer) { + move_to(account, Store { + default_configs: table::new(), + oapp_configs: table::new(), + confirmations: table::new(), + worker_config_opt_in_out: table::new(), + }); + } + + // ============================================= General Configuration ============================================ + + // the following functions are used to facilitate working with a nested data structure in the store. The + // initialization of certain table fields does not have a mapping to an exposed initialization process and are + // strictly implementation details. + + #[test_only] + // Initializes the module for testing purposes + public(friend) fun init_module_for_test() { + let admin = &std::account::create_signer_for_test(@uln_302); + init_module(admin); + } + + /// Internal function that checks whether the default config table is initialized for a given EID + fun is_eid_default_config_table_initialized(eid: u32): bool acquires Store { + table::contains(&store().default_configs, eid) + } + + /// Internal function that checks whether the oapp config table is initialized for a given EID and OApp + fun is_eid_oapp_config_table_initialized(eid: u32, oapp: address): bool acquires Store { + table::contains(&store().oapp_configs, OAppEid { oapp, eid }) + } + + /// Internal function that initializes the default config table for a given EID if it is not already + fun ensure_eid_default_config_table_initialized(eid: u32) acquires Store { + if (!is_eid_default_config_table_initialized(eid)) { + table::add(&mut store_mut().default_configs, eid, DefaultConfig { + executor_config: option::none(), + receive_uln_config: option::none(), + send_uln_config: option::none(), + }); + } + } + + /// Internal function that initializes the oapp config table for a given EID and OApp if it is not already + fun ensure_oapp_config_table_initialized(eid: u32, oapp: address) acquires Store { + if (!is_eid_oapp_config_table_initialized(eid, oapp)) { + table::add(&mut store_mut().oapp_configs, OAppEid { eid, oapp }, OAppConfig { + executor_config: option::none(), + receive_uln_config: option::none(), + send_uln_config: option::none(), + }); + } + } + + // ================================================= Worker Config ================================================ + + + public(friend) fun set_worker_config_for_fee_lib_routing_opt_in(worker: address, opt_in_out: bool) acquires Store { + table::upsert(&mut store_mut().worker_config_opt_in_out, worker, opt_in_out) + } + + public(friend) fun get_worker_config_for_fee_lib_routing_opt_in(worker: address): bool acquires Store { + *table::borrow_with_default(&store().worker_config_opt_in_out, worker, &false) + } + + // ================================================ Send ULN Config =============================================== + + public(friend) fun has_default_send_uln_config(dst_eid: u32): bool acquires Store { + is_eid_default_config_table_initialized(dst_eid) && + option::is_some(&default_configs(dst_eid).send_uln_config) + } + + /// Gets the default send configuration for a given EID + public(friend) fun get_default_send_uln_config(dst_eid: u32): Option acquires Store { + if (!is_eid_default_config_table_initialized(dst_eid)) { return option::none() }; + default_configs(dst_eid).send_uln_config + } + + /// Sets the default send configuration for a given EID. This is a raw setter and should not be used directly + public(friend) fun set_default_send_uln_config(eid: u32, config: UlnConfig) acquires Store { + ensure_eid_default_config_table_initialized(eid); + default_configs_mut(eid).send_uln_config = option::some(config); + } + + /// Sets the send configuration for a given EID. This is a raw setter and should not be used directly + public(friend) fun set_send_uln_config(oapp_address: address, eid: u32, config: UlnConfig) acquires Store { + ensure_oapp_config_table_initialized(eid, oapp_address); + oapp_configs_mut(eid, oapp_address).send_uln_config = option::some(config); + } + + /// Gets the oapp send configuration for an EID if it is set. This is a raw getter and should not be used directly + public(friend) fun get_send_uln_config(sender: address, dst_eid: u32): Option acquires Store { + if (!is_eid_oapp_config_table_initialized(dst_eid, sender)) { return option::none() }; + oapp_configs(dst_eid, sender).send_uln_config + } + + // ============================================== Receive ULN Config ============================================== + + public(friend) fun has_default_receive_uln_config(eid: u32): bool acquires Store { + is_eid_default_config_table_initialized(eid) && + option::is_some(&default_configs(eid).receive_uln_config) + } + + /// Gets the default receive configuration for a given EID + public(friend) fun get_default_receive_uln_config(eid: u32): Option acquires Store { + if (!is_eid_default_config_table_initialized(eid)) { return option::none() }; + default_configs(eid).receive_uln_config + } + + /// Sets the default receive configuration for a given EID. This is a raw setter and should not be used directly + public(friend) fun set_default_receive_uln_config(eid: u32, config: UlnConfig) acquires Store { + ensure_eid_default_config_table_initialized(eid); + default_configs_mut(eid).receive_uln_config = option::some(config); + } + + /// Gets the oapp receive configuration for an EID if it is set. This is a raw getter and should not be used + /// directly + public(friend) fun get_receive_uln_config(oapp_address: address, eid: u32): Option acquires Store { + if (!is_eid_oapp_config_table_initialized(eid, oapp_address)) { return option::none() }; + oapp_configs(eid, oapp_address).receive_uln_config + } + + /// Sets the oapp receive configuration for an EID. This is a raw setter and should not be used directly + public(friend) fun set_receive_uln_config(oapp: address, eid: u32, config: UlnConfig) acquires Store { + ensure_oapp_config_table_initialized(eid, oapp); + oapp_configs_mut(eid, oapp).receive_uln_config = option::some(config); + } + + // ================================================ Executor Config =============================================== + + /// Gets the default executor configuration for a given EID + public(friend) fun get_default_executor_config(eid: u32): Option acquires Store { + if (!is_eid_default_config_table_initialized(eid)) { return option::none() }; + default_configs(eid).executor_config + } + + /// Sets the default executor configuration for a given EID. This is a raw setter and should not be used directly + public(friend) fun set_default_executor_config(eid: u32, config: ExecutorConfig) acquires Store { + ensure_eid_default_config_table_initialized(eid); + default_configs_mut(eid).executor_config = option::some(config); + } + + /// Gets the oapp executor configuration for an EID if it is set. This is a raw getter and should not be used + public(friend) fun get_executor_config(sender: address, eid: u32): Option acquires Store { + if (!is_eid_oapp_config_table_initialized(eid, sender)) { return option::none() }; + oapp_configs(eid, sender).executor_config + } + + /// Sets the oapp executor configuration for an EID. This is a raw setter and should not be used directly + public(friend) fun set_executor_config(sender: address, eid: u32, config: ExecutorConfig) acquires Store { + ensure_oapp_config_table_initialized(eid, sender); + oapp_configs_mut(eid, sender).executor_config = option::some(config); + } + + // ============================================== Receive Side State ============================================== + + /// Checks if a message has received confirmations from a given DVN + public(friend) fun has_verification_confirmations( + header_hash: Bytes32, payload_hash: Bytes32, dvn_address: address, + ): bool acquires Store { + table::contains(confirmations(), ConfirmationsKey { header_hash, payload_hash, dvn_address }) + } + + /// Gets the number of verification confirmations received for a message from a given DVN + public(friend) fun get_verification_confirmations( + header_hash: Bytes32, payload_hash: Bytes32, dvn_address: address, + ): u64 acquires Store { + *table::borrow(confirmations(), ConfirmationsKey { header_hash, payload_hash, dvn_address }) + } + + /// Sets the number of verification confirmations received for a message from a given DVN. This is a raw setter and + /// should not be used directly + public(friend) fun set_verification_confirmations( + header_hash: Bytes32, payload_hash: Bytes32, dvn_address: address, confirmations: u64, + ) acquires Store { + table::upsert( + confirmations_mut(), + ConfirmationsKey { header_hash, payload_hash, dvn_address }, + confirmations, + ) + } + + /// Removes the verification confirmations for a message from a given DVN. This is a raw setter and should not be + /// used directly + public(friend) fun remove_verification_confirmations( + header_hash: Bytes32, payload_hash: Bytes32, dvn_address: address, + ): u64 acquires Store { + table::remove(confirmations_mut(), ConfirmationsKey { header_hash, payload_hash, dvn_address }) + } + + // ==================================================== Helpers =================================================== + + inline fun store(): &Store { borrow_global(@uln_302) } + + inline fun store_mut(): &mut Store { borrow_global_mut(@uln_302) } + + inline fun default_configs(eid: u32): &DefaultConfig { table::borrow(&store().default_configs, eid) } + + inline fun default_configs_mut(eid: u32): &mut DefaultConfig { + table::borrow_mut(&mut store_mut().default_configs, eid) + } + + inline fun oapp_configs(eid: u32, oapp: address): &OAppConfig { + table::borrow(&store().oapp_configs, OAppEid { oapp, eid }) + } + + inline fun oapp_configs_mut(eid: u32, oapp: address): &mut OAppConfig { + table::borrow_mut(&mut store_mut().oapp_configs, OAppEid { oapp, eid }) + } + + inline fun confirmations(): &Table { &store().confirmations } + + inline fun confirmations_mut(): &mut Table { &mut store_mut().confirmations } + + // ================================================== Error Codes ================================================= + + const EALREADY_SET: u64 = 1; + const EINVALID_INPUT: u64 = 2; +} diff --git a/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/sources/internal/verification.move b/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/sources/internal/verification.move new file mode 100644 index 00000000..b815c94e --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/sources/internal/verification.move @@ -0,0 +1,168 @@ +module uln_302::verification { + use std::event::emit; + use std::vector; + + use endpoint_v2_common::bytes32::{Self, Bytes32, from_bytes32}; + use endpoint_v2_common::packet_raw::{get_packet_bytes, RawPacket}; + use endpoint_v2_common::packet_v1_codec::{Self, assert_receive_header}; + use endpoint_v2_common::universal_config; + use msglib_types::configs_uln::{Self, UlnConfig}; + use uln_302::configuration; + use uln_302::for_each_dvn::for_each_dvn; + use uln_302::uln_302_store; + + friend uln_302::msglib; + friend uln_302::router_calls; + + #[test_only] + friend uln_302::verification_tests; + + // ================================================= Verification ================================================= + + /// Initiated from the DVN to give their verification of a payload as well as the number of confirmations + public(friend) fun verify( + dvn_address: address, + packet_header: RawPacket, + payload_hash: Bytes32, + confirmations: u64, + ) { + let packet_header_bytes = get_packet_bytes(packet_header); + let header_hash = bytes32::keccak256(packet_header_bytes); + uln_302_store::set_verification_confirmations(header_hash, payload_hash, dvn_address, confirmations); + emit(PayloadVerified { + dvn: dvn_address, + header: packet_header_bytes, + confirmations, + proof_hash: from_bytes32(payload_hash), + }); + } + + /// This checks if a message is verifiable by a sufficient group of DVNs to meet the requirements + public(friend) fun check_verifiable(config: &UlnConfig, header_hash: Bytes32, payload_hash: Bytes32): bool { + let required_dvn_count = configs_uln::get_required_dvn_count(config); + let optional_dvn_count = configs_uln::get_optional_dvn_count(config); + let optional_dvn_threshold = configs_uln::get_optional_dvn_threshold(config); + let required_confirmations = configs_uln::get_confirmations(config); + + if (required_dvn_count > 0) { + let required_dvns = configs_uln::get_required_dvns(config); + for (i in 0..required_dvn_count) { + let dvn = vector::borrow(&required_dvns, i); + if (!is_verified(*dvn, header_hash, payload_hash, required_confirmations)) { + return false + }; + }; + if (optional_dvn_threshold == 0) { + return true + }; + }; + + let count_optional_dvns_verified = 0; + let optional_dvns = configs_uln::get_optional_dvns(config); + for (i in 0..optional_dvn_count) { + let dvn = vector::borrow(&optional_dvns, i); + if (is_verified(*dvn, header_hash, payload_hash, required_confirmations)) { + count_optional_dvns_verified = count_optional_dvns_verified + 1; + if (count_optional_dvns_verified >= optional_dvn_threshold) { + return true + }; + }; + }; + + return false + } + + /// This checks if a DVN has verified a message with at least the required number of confirmations + public(friend) fun is_verified( + dvn_address: address, + header_hash: Bytes32, + payload_hash: Bytes32, + required_confirmations: u64, + ): bool { + if (!uln_302_store::has_verification_confirmations(header_hash, payload_hash, dvn_address)) { return false }; + let confirmations = uln_302_store::get_verification_confirmations(header_hash, payload_hash, dvn_address); + confirmations >= required_confirmations + } + + + /// Commits the verification of a payload + /// This uses the contents of a serialized packet header to then assert the complete verification of a payload and + /// then clear the storage related to it + public(friend) fun commit_verification( + packet_header: RawPacket, + payload_hash: Bytes32, + ): (address, u32, Bytes32, u64) { + assert_receive_header(&packet_header, universal_config::eid()); + verify_and_reclaim_storage( + &get_receive_uln_config_from_packet_header(&packet_header), + bytes32::keccak256(get_packet_bytes(packet_header)), + payload_hash, + ); + + // decode the header + let receiver = bytes32::to_address(packet_v1_codec::get_receiver(&packet_header)); + let src_eid = packet_v1_codec::get_src_eid(&packet_header); + let sender = packet_v1_codec::get_sender(&packet_header); + let nonce = packet_v1_codec::get_nonce(&packet_header); + (receiver, src_eid, sender, nonce) + } + + public(friend) fun get_receive_uln_config_from_packet_header(packet_header: &RawPacket): UlnConfig { + let src_eid = packet_v1_codec::get_src_eid(packet_header); + let oapp_address = bytes32::to_address(packet_v1_codec::get_receiver(packet_header)); + configuration::get_receive_uln_config(oapp_address, src_eid) + } + + /// If the message is verifiable, this function will remove the confirmations from the store + /// It will abort if the message is not verifiable + public(friend) fun verify_and_reclaim_storage(config: &UlnConfig, header_hash: Bytes32, payload_hash: Bytes32) { + assert!(check_verifiable(config, header_hash, payload_hash), ENOT_VERIFIABLE); + + reclaim_storage( + &configs_uln::get_required_dvns(config), + &configs_uln::get_optional_dvns(config), + header_hash, + payload_hash, + ); + } + + + /// Loop through the DVN addresses and clear confirmations for the given header and payload hash + public(friend) fun reclaim_storage( + required_dvns: &vector
, + optional_dvns: &vector
, + header_hash: Bytes32, + payload_hash: Bytes32, + ) { + for_each_dvn(required_dvns, optional_dvns, |dvn, _idx| { + if (uln_302_store::has_verification_confirmations(header_hash, payload_hash, dvn)) { + uln_302_store::remove_verification_confirmations(header_hash, payload_hash, dvn); + }; + }); + } + + // ==================================================== Events ==================================================== + + #[event] + struct PayloadVerified has drop, copy, store { + dvn: address, + header: vector, + confirmations: u64, + proof_hash: vector, + } + + #[test_only] + public fun payload_verified_event( + dvn: address, + header: vector, + confirmations: u64, + proof_hash: vector, + ): PayloadVerified { + PayloadVerified { dvn, header, confirmations, proof_hash } + } + + // ================================================== Error Codes ================================================= + + const ENO_DVNS: u64 = 1; + const ENOT_VERIFIABLE: u64 = 2; +} diff --git a/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/sources/msglib.move b/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/sources/msglib.move new file mode 100644 index 00000000..98d48d05 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/sources/msglib.move @@ -0,0 +1,145 @@ +/// View functions for the ULN-302 module +module uln_302::msglib { + use std::event::emit; + use std::option::Option; + use std::signer::address_of; + + use endpoint_v2_common::bytes32; + use endpoint_v2_common::packet_raw::bytes_to_raw_packet; + use endpoint_v2_common::packet_v1_codec::{Self, is_receive_header_valid}; + use endpoint_v2_common::universal_config; + use msglib_types::configs_executor::ExecutorConfig; + use msglib_types::configs_uln::UlnConfig; + use uln_302::configuration; + use uln_302::uln_302_store; + use uln_302::verification; + + #[test_only] + /// Initializes the uln for testing + public fun initialize_for_test() { + uln_302_store::init_module_for_test(); + } + + #[view] + /// Checks that a packet is verifiable and has received verification of the required confirmations from the needed + /// set of DVNs + public fun verifiable( + packet_header_bytes: vector, + payload_hash: vector, + ): bool { + let packet_header = bytes_to_raw_packet(packet_header_bytes); + let src_eid = packet_v1_codec::get_src_eid(&packet_header); + let receiver = bytes32::to_address( + packet_v1_codec::get_receiver(&packet_header) + ); + + is_receive_header_valid(&packet_header, universal_config::eid()) && + verification::check_verifiable( + &configuration::get_receive_uln_config(receiver, src_eid), + bytes32::keccak256(packet_header_bytes), + bytes32::to_bytes32(payload_hash) + ) + } + + #[view] + /// Gets the treasury address + public fun get_treasury(): address { + @treasury + } + + #[view] + /// Gets the default send ULN config for an EID + /// + /// This will return option::none() if it is not set. + public fun get_default_uln_send_config(eid: u32): Option { + uln_302_store::get_default_send_uln_config(eid) + } + + #[view] + /// Gets the default receive ULN config for an EID + /// + /// This will return option::none() if it is not set. + public fun get_default_uln_receive_config(eid: u32): Option { + uln_302_store::get_default_receive_uln_config(eid) + } + + #[view] + /// Gets the default executor config for an EID + /// + /// This will return option::none() if it is not set. + public fun get_default_executor_config(eid: u32): Option { + uln_302_store::get_default_executor_config(eid) + } + + #[view] + /// Gets the send ULN config for an OApp and EID + /// + /// This will return option::none() if it is not set. + public fun get_app_send_config(oapp: address, eid: u32): Option { + uln_302_store::get_send_uln_config(oapp, eid) + } + + #[view] + /// Gets the receive ULN config for an OApp and EID + /// + /// This will return option::none() if it is not set. + public fun get_app_receive_config(oapp: address, eid: u32): Option { + uln_302_store::get_receive_uln_config(oapp, eid) + } + + #[view] + /// Gets the executor config for an OApp and EID + /// + /// This will return option::none() if it is not set. + public fun get_app_executor_config(oapp: address, eid: u32): Option { + uln_302_store::get_executor_config(oapp, eid) + } + + #[view] + public fun get_verification_confirmations( + header_hash: vector, + payload_hash: vector, + dvn_address: address, + ): u64 { + let header_hash_bytes = bytes32::to_bytes32(header_hash); + let payload_hash_bytes = bytes32::to_bytes32(payload_hash); + + if (uln_302_store::has_verification_confirmations(header_hash_bytes, payload_hash_bytes, dvn_address)) { + uln_302_store::get_verification_confirmations(header_hash_bytes, payload_hash_bytes, dvn_address) + } else 0 + } + + /// Setter for a worker to opt in (true) or out (false) of using the worker_common::worker_config module to store + /// their fee library configuration + public entry fun set_worker_config_for_fee_lib_routing_opt_in(worker: &signer, opt_in: bool) { + let worker_address = address_of(move worker); + // Emit an event only if there is a change + if (uln_302_store::get_worker_config_for_fee_lib_routing_opt_in(worker_address) != opt_in) { + emit(WorkerConfigForFeeLibRoutingOptIn { worker: worker_address, opt_in }) + }; + + uln_302_store::set_worker_config_for_fee_lib_routing_opt_in(worker_address, opt_in); + } + + #[view] + /// Gets the opt in/out status of a worker + public fun worker_config_for_fee_lib_routing_opt_in(worker: address): bool { + uln_302_store::get_worker_config_for_fee_lib_routing_opt_in(worker) + } + + // ==================================================== Events ==================================================== + + #[event] + struct WorkerConfigForFeeLibRoutingOptIn has drop, store { + worker: address, + opt_in: bool, + } + + #[test_only] + public fun worker_config_for_fee_lib_routing_opt_in_event( + worker: address, + opt_in: bool, + ): WorkerConfigForFeeLibRoutingOptIn { + WorkerConfigForFeeLibRoutingOptIn { worker, opt_in } + } +} diff --git a/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/sources/router_calls.move b/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/sources/router_calls.move new file mode 100644 index 00000000..a87c2b4a --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/sources/router_calls.move @@ -0,0 +1,117 @@ +/// Entrypoint for all calls that come from the Message Library Router +module uln_302::router_calls { + use std::any::Any; + use std::fungible_asset::FungibleAsset; + use std::option::Option; + + use endpoint_v2_common::bytes32::Bytes32; + use endpoint_v2_common::contract_identity::{ + DynamicCallRef, + get_dynamic_call_ref_caller, + }; + use endpoint_v2_common::packet_raw::RawPacket; + use endpoint_v2_common::send_packet::SendPacket; + use msglib_types::dvn_verify_params; + use uln_302::configuration; + use uln_302::sending; + use uln_302::verification; + + // ==================================================== Sending =================================================== + + /// Provides a quote for sending a packet + public fun quote( + packet: SendPacket, + options: vector, + pay_in_zro: bool, + ): (u64, u64) { + sending::quote(packet, options, pay_in_zro) + } + + /// Takes payment for sending a packet and triggers offchain entities to verify and deliver the packet + public fun send( + call_ref: &DynamicCallRef, + packet: SendPacket, + options: vector, + native_token: &mut FungibleAsset, + zro_token: &mut Option, + ): (u64, u64, RawPacket) { + assert_caller_is_endpoint_v2(call_ref, b"send"); + sending::send( + packet, + options, + native_token, + zro_token, + ) + } + + // =================================================== Receiving ================================================== + + /// Commits a verification for a packet and clears the memory of that packet + /// + /// Once a packet is committed, it cannot be recommitted without receiving all verifications again. This will abort + /// if the packet has not been verified by all required parties. This is to be called in conjunction with the + /// endpoint's `verify()`, which identifies the message as ready to be delivered by storing the message hash. + public fun commit_verification( + call_ref: &DynamicCallRef, + packet_header: RawPacket, + payload_hash: Bytes32, + _extra_data: vector, + ): (address, u32, Bytes32, u64) { + assert_caller_is_endpoint_v2(call_ref, b"commit_verification"); + verification::commit_verification(packet_header, payload_hash) + } + + // ===================================================== DVNs ===================================================== + + /// This is called by the DVN to verify a packet + /// + /// This expects an Any of type DvnVerifyParams, which contains the packet header, payload hash, and the number of + /// confirmations. This is stored and the verifications are checked as a group when `commit_verification` is called. + public fun dvn_verify(contract_id: &DynamicCallRef, params: Any) { + let worker = get_dynamic_call_ref_caller(contract_id, @uln_302, b"dvn_verify"); + let (packet_header, payload_hash, confirmations) = dvn_verify_params::unpack_dvn_verify_params(params); + verification::verify(worker, packet_header, payload_hash, confirmations) + } + + // ================================================= Configuration ================================================ + + /// Sets the ULN and Executor configurations for an OApp + public fun set_config(call_ref: &DynamicCallRef, oapp: address, eid: u32, config_type: u32, config: vector) { + assert_caller_is_endpoint_v2(call_ref, b"set_config"); + configuration::set_config(oapp, eid, config_type, config) + } + + #[view] + /// Gets the ULN or Executor configuration for an eid on an OApp + public fun get_config(oapp: address, eid: u32, config_type: u32): vector { + configuration::get_config(oapp, eid, config_type) + } + + // ==================================================== Helper ==================================================== + + fun assert_caller_is_endpoint_v2(call_ref: &DynamicCallRef, authorization: vector) { + let caller = get_dynamic_call_ref_caller(call_ref, @uln_302, authorization); + assert!(caller == @endpoint_v2, EINVALID_CALLER); + } + + // ================================================ View Functions ================================================ + + #[view] + public fun version(): (u64 /*major*/, u8 /*minor*/, u8 /*endpoint_version*/) { + (3, 0, 2) + } + + #[view] + public fun is_supported_send_eid(eid: u32): bool { + configuration::supports_send_eid(eid) + } + + #[view] + public fun is_supported_receive_eid(eid: u32): bool { + configuration::supports_receive_eid(eid) + } + + // ================================================== Error Codes ================================================= + + const EINVALID_CALLER: u64 = 1; +} diff --git a/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/tests/internal/assert_valid_default_uln_config_tests.move b/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/tests/internal/assert_valid_default_uln_config_tests.move new file mode 100644 index 00000000..73a42631 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/tests/internal/assert_valid_default_uln_config_tests.move @@ -0,0 +1,110 @@ +#[test_only] +module uln_302::assert_valid_default_uln_config_tests { + use std::bcs::to_bytes; + use std::from_bcs::to_address; + use std::vector; + + use msglib_types::configs_uln::new_uln_config; + use uln_302::assert_valid_default_uln_config::assert_valid_default_uln_config; + + const MAX_DVNS: u64 = 127; + + #[test] + fun test_assert_valid_default_uln_config_valid_if_zero_required_dvs_if_optional_threshold_exists() { + let config = new_uln_config(1, 1, vector[], vector[@0x20], false, false, false); + assert_valid_default_uln_config(&config); + } + + #[test] + fun test_assert_valid_default_uln_config_allows_zero_confirmations() { + let config = new_uln_config(0, 1, vector[@0x20], vector[@0x20], false, false, false); + assert_valid_default_uln_config(&config); + } + + #[test] + #[expected_failure(abort_code = uln_302::assert_valid_default_uln_config::ENO_EFFECTIVE_DVN_THRESHOLD)] + fun test_assert_valid_default_uln_config_fails_in_no_required_dvns_and_no_effective_dvn_threshold() { + let config = new_uln_config(1, 0, vector[], vector[@0x10, @0x20], false, false, false); + assert_valid_default_uln_config(&config); + } + + #[test] + #[expected_failure(abort_code = uln_302::assert_valid_default_uln_config::EOPTIONAL_DVN_THRESHOLD_EXCEEDS_COUNT)] + fun test_assert_valid_default_uln_config_fails_if_optional_dvn_threshold_exceeds_optional_dvn_count() { + let config = new_uln_config(1, 3, vector[@0x20], vector[@0x10, @0x20], false, false, false); + assert_valid_default_uln_config(&config); + } + + + #[test] + #[expected_failure( + abort_code = uln_302::assert_valid_default_uln_config::EREQUESTING_USE_DEFAULT_REQUIRED_DVNS_FOR_DEFAULT_CONFIG + )] + fun test_assert_valid_default_uln_config_fails_if_enabled_default_for_required_dvns() { + let config = new_uln_config(1, 1, vector[@0x20], vector[@0x20], false, true, false); + assert_valid_default_uln_config(&config); + } + + #[test] + #[expected_failure( + abort_code = uln_302::assert_valid_default_uln_config::EREQUESTING_USE_DEFAULT_OPTIONAL_DVNS_FOR_DEFAULT_CONFIG + )] + fun test_assert_valid_default_uln_config_fails_if_enabled_use_default_for_optional_dvns() { + let config = new_uln_config(1, 1, vector[@0x20], vector[@0x20], false, false, true); + assert_valid_default_uln_config(&config); + } + + #[test] + #[expected_failure(abort_code = uln_302::assert_valid_default_uln_config::ETOO_MANY_REQUIRED_DVNS)] + fun test_assert_valid_default_uln_config_fails_if_required_dvns_exceeds_max() { + let required_dvns = vector
[]; + for (i in 0..(MAX_DVNS + 1)) { + vector::push_back(&mut required_dvns, to_address(to_bytes(&(i as u256)))); + }; + let config = new_uln_config(1, 1, required_dvns, vector[@0x20], false, false, false); + assert_valid_default_uln_config(&config); + } + + #[test] + #[expected_failure(abort_code = uln_302::assert_valid_default_uln_config::ETOO_MANY_OPTIONAL_DVNS)] + fun test_assert_valid_default_uln_config_fails_if_optional_dvns_exceeds_max() { + let optional_dvns = vector
[]; + for (i in 0..(MAX_DVNS + 1)) { + vector::push_back(&mut optional_dvns, to_address(to_bytes(&(i as u256)))); + }; + let config = new_uln_config(1, 1, vector[@0x10], optional_dvns, false, false, false); + assert_valid_default_uln_config(&config); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2_common::assert_no_duplicates::EDUPLICATE_ITEM)] + fun test_assert_valid_default_uln_config_fails_if_duplicate_required_dvn() { + let required_dvns = vector
[@1, @2, @3, @1, @5]; + let config = new_uln_config(2, 1, required_dvns, vector[@0x30], false, false, false); + assert_valid_default_uln_config(&config); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2_common::assert_no_duplicates::EDUPLICATE_ITEM)] + fun test_assert_valid_default_uln_config_fails_if_duplicate_optional_dvn() { + let optional_dvns = vector
[@1, @2, @3, @1, @5]; + let config = new_uln_config(2, 1, vector[@0x20], optional_dvns, false, false, false); + assert_valid_default_uln_config(&config); + } + + #[test] + #[expected_failure( + abort_code = uln_302::assert_valid_default_uln_config::EREQUESTING_USE_DEFAULT_CONFIRMATIONS_FOR_DEFAULT_CONFIG + )] + fun test_assert_valid_default_uln_config_fails_if_enabled_use_default_for_confirmations() { + let config = new_uln_config(1, 1, vector[@0x20], vector[@0x20], true, false, false); + assert_valid_default_uln_config(&config); + } + + #[test] + #[expected_failure(abort_code = uln_302::assert_valid_default_uln_config::EINVALID_DVN_THRESHOLD)] + fun test_assert_valid_default_uln_config_fails_if_no_threshold_with_optional_dvns_defined() { + let config = new_uln_config(1, 0, vector[@0x20], vector[@0x20], false, false, false); + assert_valid_default_uln_config(&config); + } +} diff --git a/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/tests/internal/assert_valid_oapp_uln_config_tests.move b/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/tests/internal/assert_valid_oapp_uln_config_tests.move new file mode 100644 index 00000000..2f73b9f6 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/tests/internal/assert_valid_oapp_uln_config_tests.move @@ -0,0 +1,125 @@ +#[test_only] +module uln_302::assert_valid_oapp_uln_config_tests { + use std::bcs::to_bytes; + use std::from_bcs::to_address; + use std::vector; + + use msglib_types::configs_uln::new_uln_config; + use uln_302::assert_valid_uln_config::assert_valid_uln_config; + + const MAX_DVNS: u64 = 127; + + #[test] + #[expected_failure(abort_code = uln_302::assert_valid_uln_config::ENO_EFFECTIVE_DVN_THRESHOLD)] + fun invalid_if_less_than_one_effective_dvn_theshold() { + let default_config = new_uln_config(2, 1, vector[@0x10], vector[@0x20], false, false, false); + let oapp_config = new_uln_config(2, 0, vector[], vector[], false, false, false); + assert_valid_uln_config(&oapp_config, &default_config); + } + + #[test] + #[expected_failure(abort_code = uln_302::assert_valid_uln_config::ENO_EFFECTIVE_DVN_THRESHOLD)] + fun invalid_if_no_effective_dvn_threshold_because_of_use_default_optional_dvns() { + let default_config = new_uln_config(2, 0, vector[@0x10], vector[@0x20], false, false, false); + let oapp_config = new_uln_config(2, 0, vector[], vector[], false, false, true); + assert_valid_uln_config(&oapp_config, &default_config); + } + + #[test] + #[expected_failure(abort_code = uln_302::assert_valid_uln_config::ENO_EFFECTIVE_DVN_THRESHOLD)] + fun invalid_if_no_effective_dvn_threshold_because_of_use_default_required_dvns() { + let default_config = new_uln_config(2, 1, vector[], vector[@0x20], false, false, false); + let oapp_config = new_uln_config(2, 0, vector[], vector[], false, true, false); + assert_valid_uln_config(&oapp_config, &default_config); + } + + #[test] + fun valid_if_one_effective_dvn_threshold_because_use_default_optional_dvns() { + let default_config = new_uln_config(2, 1, vector[@0x10], vector[@0x20], false, false, false); + let oapp_config = new_uln_config(2, 0, vector[], vector[], false, false, true); + assert_valid_uln_config(&oapp_config, &default_config); + } + + #[test] + fun valid_if_one_effective_dvn_threshold_because_use_default_required_dvns() { + let default_config = new_uln_config(2, 0, vector[@0x10], vector[@0x20], false, false, false); + let oapp_config = new_uln_config(2, 0, vector[], vector[], false, true, false); + assert_valid_uln_config(&oapp_config, &default_config); + } + + #[test] + #[expected_failure(abort_code = uln_302::assert_valid_uln_config::ENONEMPTY_REQUIRED_DVNS_WITH_USE_DEFAULT)] + fun invalid_if_required_dvns_specified_when_using_default() { + let default_config = new_uln_config(1, 1, vector[@0x20], vector[@0x30], false, false, false); + let oapp_config = new_uln_config(2, 3, vector[@0x10], vector[@0x20], false, true, false); + assert_valid_uln_config(&oapp_config, &default_config); + } + + #[test] + #[expected_failure(abort_code = uln_302::assert_valid_uln_config::ENONEMPTY_OPTIONAL_DVNS_WITH_USE_DEFAULT)] + fun invalid_if_optional_dvns_specified_when_using_default() { + let default_config = new_uln_config(1, 1, vector[@0x20], vector[@0x30], false, false, false); + let oapp_config = new_uln_config(2, 3, vector[@0x10], vector[@0x20], false, false, true); + assert_valid_uln_config(&oapp_config, &default_config); + } + + #[test] + #[expected_failure(abort_code = uln_302::assert_valid_uln_config::ETOO_MANY_REQUIRED_DVNS)] + fun invalid_if_more_than_max_required_dvns() { + let default_config = new_uln_config(1, 1, vector[@0x20], vector[@0x30], false, false, false); + let required_dvns = vector
[]; + for (i in 0..(MAX_DVNS + 1)) { + vector::push_back(&mut required_dvns, to_address(to_bytes(&(i as u256)))); + }; + let oapp_config = new_uln_config(2, 3, required_dvns, vector[@0x20], false, false, false); + assert_valid_uln_config(&oapp_config, &default_config); + } + + #[test] + #[expected_failure(abort_code = uln_302::assert_valid_uln_config::ETOO_MANY_OPTIONAL_DVNS)] + fun invalid_if_more_than_max_optional_dvns() { + let default_config = new_uln_config(1, 1, vector[@0x20], vector[@0x30], false, false, false); + let optional_dvns = vector
[]; + for (i in 0..(MAX_DVNS + 1)) { + vector::push_back(&mut optional_dvns, to_address(to_bytes(&(i as u256)))); + }; + let oapp_config = new_uln_config(2, 3, vector[@0x10], optional_dvns, false, false, false); + assert_valid_uln_config(&oapp_config, &default_config); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2_common::assert_no_duplicates::EDUPLICATE_ITEM)] + fun invalid_if_duplicate_addresses_in_required_dvns() { + let default_config = new_uln_config(1, 1, vector[@0x20], vector[@0x30], false, false, false); + let required_dvns = vector
[@1, @2, @3, @1, @5]; + let oapp_config = new_uln_config(2, 1, required_dvns, vector[@0x30], false, false, false); + assert_valid_uln_config(&oapp_config, &default_config); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2_common::assert_no_duplicates::EDUPLICATE_ITEM)] + fun invalid_if_duplicate_addresses_in_optional_dvns() { + let default_config = new_uln_config(1, 1, vector[@0x20], vector[@0x30], false, false, false); + let optional_dvns = vector
[@1, @2, @3, @1, @5]; + let oapp_config = new_uln_config(2, 1, vector[@0x20], optional_dvns, false, false, false); + assert_valid_uln_config(&oapp_config, &default_config); + } + + #[test] + #[expected_failure( + abort_code = uln_302::assert_valid_uln_config::ENONZERO_CONFIRMATIONS_PROVIDED_FOR_DEFAULT_CONFIG + )] + fun invalid_if_use_default_for_confirmations_but_provide_confirmations() { + let default_config = new_uln_config(1, 1, vector[@0x20], vector[@0x30], false, false, false); + let oapp_config = new_uln_config(2, 1, vector[@0x10], vector[@0x20], true, false, false); + assert_valid_uln_config(&oapp_config, &default_config); + } + + #[test] + #[expected_failure(abort_code = uln_302::assert_valid_uln_config::EINVALID_DVN_THRESHOLD)] + fun test_assert_valid_default_uln_config_fails_if_no_threshold_with_optional_dvns_defined() { + let default_config = new_uln_config(1, 1, vector[@0x20], vector[@0x30], false, false, false); + let config = new_uln_config(1, 0, vector[@0x20], vector[@0x20], false, false, false); + assert_valid_uln_config(&config, &default_config); + } +} diff --git a/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/tests/internal/configuration_tests.move b/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/tests/internal/configuration_tests.move new file mode 100644 index 00000000..5c57cbd3 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/tests/internal/configuration_tests.move @@ -0,0 +1,478 @@ +#[test_only] +module uln_302::configuration_tests { + use std::event::was_event_emitted; + use std::option; + + use endpoint_v2_common::serde::bytes_of; + use msglib_types::configs_executor; + use msglib_types::configs_executor::new_executor_config; + use msglib_types::configs_uln; + use msglib_types::configs_uln::new_uln_config; + use uln_302::configuration; + use uln_302::configuration::{ + assert_valid_default_executor_config, default_executor_config_set_event, default_uln_config_set_event, + executor_config_set_event, get_executor_config, get_receive_uln_config, get_send_uln_config, + merge_executor_configs, merge_uln_configs, set_default_executor_config, set_default_receive_uln_config, + set_default_send_uln_config, set_executor_config, set_receive_uln_config, set_send_uln_config, + supports_receive_eid, supports_send_eid, + }; + use uln_302::uln_302_store; + + const ULN_SEND_SIDE: u8 = 0; + const ULN_RECEIVE_SIDE: u8 = 1; + + fun setup() { + uln_302_store::init_module_for_test(); + } + + public fun enable_receive_eid_for_test(eid: u32) { + let config = configs_uln::new_uln_config(1, 0, vector[@dvn], vector[], false, false, false); + set_default_receive_uln_config(eid, config); + } + + public fun enable_send_eid_for_test(eid: u32) { + let config = configs_uln::new_uln_config(1, 0, vector[@dvn], vector[], false, false, false); + set_default_send_uln_config(eid, config); + } + + #[test] + fun test_set_default_receive_uln_config() { + setup(); + + // default config not set + assert!(!supports_receive_eid(1), 0); + let retrieved_config = uln_302_store::get_default_receive_uln_config(1); + assert!(option::is_none(&retrieved_config), 0); + + // set config + let config = new_uln_config(1, 1, vector[@0x20], vector[@0x30], false, false, false); + set_default_receive_uln_config(1, config); + assert!(was_event_emitted(&default_uln_config_set_event(1, config, ULN_RECEIVE_SIDE)), 0); + assert!(supports_receive_eid(1), 1); + + let retrieved_config = uln_302_store::get_default_receive_uln_config(1); + assert!(option::borrow(&retrieved_config) == &config, 2); + } + + #[test] + fun test_set_default_send_uln_config() { + setup(); + + // default config not set + let retrieved_config = uln_302_store::get_default_send_uln_config(1); + assert!(option::is_none(&retrieved_config), 0); + assert!(!supports_send_eid(1), 0); + + // set config + let config = new_uln_config(1, 1, vector[@0x20], vector[@0x30], false, false, false); + set_default_send_uln_config(1, config); + assert!(was_event_emitted(&default_uln_config_set_event(1, config, ULN_SEND_SIDE)), 0); + assert!(supports_send_eid(1), 1); + + let retrieved_config = uln_302_store::get_default_send_uln_config(1); + assert!(option::borrow(&retrieved_config) == &config, 2); + } + + + #[test] + #[expected_failure( + abort_code = uln_302::assert_valid_default_uln_config::EREQUESTING_USE_DEFAULT_REQUIRED_DVNS_FOR_DEFAULT_CONFIG + )] + fun test_set_default_receive_uln_config_fails_if_invalid() { + setup(); + // cannot "use default" for default config + let config = new_uln_config(1, 1, vector[@0x20], vector[@0x30], false, true, false); + set_default_receive_uln_config(1, config); + } + + #[test] + #[expected_failure( + abort_code = uln_302::assert_valid_default_uln_config::EREQUESTING_USE_DEFAULT_REQUIRED_DVNS_FOR_DEFAULT_CONFIG + )] + fun test_set_default_send_uln_config_fails_if_invalid() { + setup(); + // cannot "use default" for default config + let config = new_uln_config(1, 1, vector[@0x20], vector[@0x30], false, true, false); + set_default_send_uln_config(1, config); + } + + #[test] + fun test_set_send_uln_config() { + setup(); + let default_config = new_uln_config(1, 1, vector[@0x10], vector[@0x30, @0x50], false, false, false); + set_default_send_uln_config(1, default_config); + + // before setting oapp config, it should pull default + let retreived_config = get_send_uln_config(@9999, 1); + assert!(retreived_config == default_config, 0); + + // setting a different sender has no effect + let config = new_uln_config(2, 0, vector[@0x20], vector[], false, false, true); + set_send_uln_config(@1111, 1, config); + assert!(was_event_emitted(&uln_302::configuration::uln_config_set_event(@1111, 1, config, ULN_SEND_SIDE)), 0); + let retreived_config = get_send_uln_config(@9999, 1); + assert!(retreived_config == default_config, 0); + + // set same sender + set_send_uln_config(@9999, 1, config); + + // should return merged config - using configured required and default optional + let retrieved_config = get_send_uln_config(@9999, 1); + let expected_merged_config = new_uln_config(2, 1, vector[@0x20], vector[@0x30, @0x50], false, false, true); + assert!(retrieved_config == expected_merged_config, 1); + } + + #[test] + #[expected_failure(abort_code = uln_302::configuration::EEID_NOT_CONFIGURED)] + fun test_set_send_uln_config_fails_if_default_not_set() { + setup(); + let config = new_uln_config(2, 0, vector[@0x20], vector[], false, false, true); + set_send_uln_config(@9, 1, config); + } + + #[test] + #[expected_failure(abort_code = uln_302::assert_valid_uln_config::ENO_EFFECTIVE_DVN_THRESHOLD)] + fun test_set_send_uln_config_fails_if_default_and_config_merge_to_invalid_config() { + setup(); + let default_config = new_uln_config(1, 1, vector[], vector[@0x30], false, false, false); + set_default_send_uln_config(1, default_config); + + // combined, this has no dvns + let config = new_uln_config(10, 0, vector[], vector[], false, true, false); + set_send_uln_config(@9, 1, config); + } + + #[test] + fun test_set_receive_uln_config() { + setup(); + let default_config = new_uln_config(1, 1, vector[@0x10], vector[@0x30, @0x50], false, false, false); + set_default_receive_uln_config(1, default_config); + + // before setting oapp config, it should pull default + let retreived_config = get_receive_uln_config(@9999, 1); + assert!(retreived_config == default_config, 0); + + let config = new_uln_config(2, 0, vector[@0x20], vector[], false, false, true); + + // setting a different receiver has no effect + set_receive_uln_config(@1234, 1, config); + let retreived_config = get_receive_uln_config(@9999, 1); + assert!( + was_event_emitted(&uln_302::configuration::uln_config_set_event(@1234, 1, config, ULN_RECEIVE_SIDE)), + 0, + ); + assert!(retreived_config == default_config, 0); + + // set the intended receiver + set_receive_uln_config(@9999, 1, config); + assert!( + was_event_emitted(&uln_302::configuration::uln_config_set_event(@9999, 1, config, ULN_RECEIVE_SIDE)), + 0, + ); + + // should return merged config - using configured required and default optional + let retrieved_config = get_receive_uln_config(@9999, 1); + let expected_merged_config = new_uln_config(2, 1, vector[@0x20], vector[@0x30, @0x50], false, false, true); + assert!(retrieved_config == expected_merged_config, 1); + } + + #[test] + #[expected_failure(abort_code = uln_302::configuration::EEID_NOT_CONFIGURED)] + fun test_set_receive_uln_config_fails_if_default_not_set() { + setup(); + let config = new_uln_config(2, 0, vector[@0x20], vector[], false, false, true); + set_receive_uln_config(@9, 1, config); + } + + #[test] + #[expected_failure(abort_code = uln_302::assert_valid_uln_config::ENO_EFFECTIVE_DVN_THRESHOLD)] + fun test_set_receive_uln_config_fails_if_default_and_config_merge_to_invalid_config() { + setup(); + let default_config = new_uln_config(1, 1, vector[], vector[@0x30], false, false, false); + set_default_receive_uln_config(1, default_config); + + // combined, this has no dvns + let config = new_uln_config(10, 0, vector[], vector[], false, true, false); + set_receive_uln_config(@9, 1, config); + } + + #[test] + fun test_get_send_uln_config_gets_default_if_oapp_not_set() { + setup(); + let default_config = new_uln_config(1, 1, vector[@0x10], vector[@0x30, @0x50], false, false, false); + set_default_send_uln_config(1, default_config); + + let retrieved_config = get_send_uln_config(@9999, 1); + assert!(retrieved_config == default_config, 0); + } + + #[test] + fun test_get_receive_uln_config_gets_default_if_oapp_not_set() { + setup(); + let default_config = new_uln_config(1, 1, vector[@0x10], vector[@0x30, @0x50], false, false, false); + set_default_receive_uln_config(1, default_config); + + let retrieved_config = get_receive_uln_config(@9999, 1); + assert!(retrieved_config == default_config, 0); + } + + #[test] + fun test_get_send_uln_config_gets_merged_if_oapp_is_set() { + setup(); + let default_config = new_uln_config(1, 1, vector[@0x10], vector[@0x30, @0x50], false, false, false); + set_default_send_uln_config(1, default_config); + + let oapp_config = new_uln_config(2, 0, vector[@0x20], vector[], false, false, true); + set_send_uln_config(@9999, 1, oapp_config); + + let retrieved_config = get_send_uln_config(@9999, 1); + let expected_config = new_uln_config(2, 1, vector[@0x20], vector[@0x30, @0x50], false, false, true); + assert!(retrieved_config == expected_config, 0); + } + + #[test] + fun test_get_receive_uln_config_gets_merged_if_oapp_is_set() { + setup(); + let default_config = new_uln_config(1, 1, vector[@0x10], vector[@0x30, @0x50], false, false, false); + set_default_receive_uln_config(1, default_config); + + let oapp_config = new_uln_config(2, 0, vector[@0x20], vector[], false, false, true); + set_receive_uln_config(@9999, 1, oapp_config); + + let retrieved_config = get_receive_uln_config(@9999, 1); + let expected_config = new_uln_config(2, 1, vector[@0x20], vector[@0x30, @0x50], false, false, true); + assert!(retrieved_config == expected_config, 0); + } + + #[test] + #[expected_failure(abort_code = uln_302::configuration::EEID_NOT_CONFIGURED)] + fun test_get_send_uln_config_fails_if_eid_not_configured() { + setup(); + get_send_uln_config(@9999, 1); + } + + #[test] + #[expected_failure(abort_code = uln_302::configuration::EEID_NOT_CONFIGURED)] + fun test_get_receive_uln_config_fails_if_eid_not_configured() { + setup(); + get_receive_uln_config(@9999, 1); + } + + #[test] + fun test_set_default_executor_config() { + setup(); + + // default config not set + let retrieved_config = uln_302_store::get_default_executor_config(1); + assert!(option::is_none(&retrieved_config), 0); + + // set config + let config = new_executor_config(1000, @9001); + set_default_executor_config(1, config); + assert!(was_event_emitted(&default_executor_config_set_event(1, config)), 0); + + let retrieved_config = uln_302_store::get_default_executor_config(1); + assert!(option::borrow(&retrieved_config) == &config, 0); + } + + #[test] + #[expected_failure(abort_code = uln_302::configuration::EEXECUTOR_ADDRESS_IS_ZERO)] + fun test_set_default_executor_config_fails_if_invalid_executor_address() { + setup(); + let config = new_executor_config(1000, @0x0); + set_default_executor_config(1, config); + } + + #[test] + fun test_set_executor_config() { + setup(); + let default_config = new_executor_config(1000, @9001); + set_default_executor_config(1, default_config); + assert!(was_event_emitted(&default_executor_config_set_event(1, default_config)), 0); + + // before setting oapp config, it should pull default + let retreived_config = get_executor_config(@9999, 1); + assert!(retreived_config == default_config, 0); + + let config = new_executor_config(2000, @9002); + + // setting another receiver has no effect + set_executor_config(@123, 1, config); + let retreived_config = get_executor_config(@9999, 1); + assert!(retreived_config == default_config, 0); + + // setting the correct receiver + set_executor_config(@9999, 1, config); + assert!(was_event_emitted(&executor_config_set_event(@9999, 1, config)), 0); + + // should return merged config - using configured required and default optional + let retrieved_config = get_executor_config(@9999, 1); + assert!(retrieved_config == config, 1); + } + + #[test] + fun test_get_executor_config_gets_default_if_oapp_not_set() { + setup(); + let default_config = new_executor_config(1000, @9001); + set_default_executor_config(1, default_config); + + let retrieved_config = get_executor_config(@9999, 1); + assert!(retrieved_config == default_config, 0); + } + + #[test] + fun test_get_executor_config_gets_oapp_if_set() { + setup(); + let default_config = new_executor_config(1000, @9001); + set_default_executor_config(1, default_config); + + let oapp_config = new_executor_config(2000, @9002); + set_executor_config(@9999, 1, oapp_config); + + let retrieved_config = get_executor_config(@9999, 1); + assert!(retrieved_config == oapp_config, 0); + } + + #[test] + fun test_get_executor_config_gets_merged_if_oapp_max_message_size_set_to_zero() { + setup(); + let default_config = new_executor_config(1000, @9001); + set_default_executor_config(1, default_config); + + let oapp_config = new_executor_config(0, @9002); + set_executor_config(@9999, 1, oapp_config); + + let retrieved_config = get_executor_config(@9999, 1); + let expected_config = new_executor_config(1000, @9002); + assert!(retrieved_config == expected_config, 0); + } + + #[test] + fun test_assert_valid_default_executor_config() { + let config = new_executor_config(1000, @9001); + assert_valid_default_executor_config(&config); + } + + #[test] + #[expected_failure(abort_code = uln_302::configuration::EMAX_MESSAGE_SIZE_ZERO)] + fun test_assert_valid_default_executor_config_fails_if_invalid_message_size() { + let config = new_executor_config(0, @9001); + assert_valid_default_executor_config(&config); + } + + #[test] + #[expected_failure(abort_code = uln_302::configuration::EEXECUTOR_ADDRESS_IS_ZERO)] + fun test_assert_valid_default_executor_config_fails_if_invalid_executor_address() { + let config = new_executor_config(1000, @0x0); + assert_valid_default_executor_config(&config); + } + + #[test] + fun test_merge_executor_configs_uses_oapp_is_complete() { + let default_config = new_executor_config(1000, @9001); + let oapp_config = new_executor_config(2000, @9002); + let merged_config = merge_executor_configs(&default_config, &oapp_config); + assert!(merged_config == oapp_config, 0); + } + + #[test] + fun test_merge_executor_configs_uses_default_if_oapp_max_message_size_is_zero() { + let default_config = new_executor_config(1000, @9001); + let oapp_config = new_executor_config(0, @9002); + let merged_config = merge_executor_configs(&default_config, &oapp_config); + let expected_config = new_executor_config(1000, @9002); + assert!(merged_config == expected_config, 0); + } + + #[test] + fun test_merge_executor_configs_uses_default_if_oapp_executor_address_is_zero() { + let default_config = new_executor_config(1000, @9001); + let oapp_config = new_executor_config(2000, @0x0); + let merged_config = merge_executor_configs(&default_config, &oapp_config); + let expected_config = new_executor_config(2000, @9001); + assert!(merged_config == expected_config, 0); + } + + #[test] + fun test_merge_configs_should_squash_the_default_required_dvns_when_use_default_for_required_dvns_is_set_to_true() { + let default_config = new_uln_config(2, 1, vector[@0x10], vector[@0x20], false, false, false); + let oapp_config = new_uln_config(0, 1, vector[], vector[@0x40], false, true, false); + let merged_config = merge_uln_configs(&default_config, &oapp_config); + let expected_config = new_uln_config(0, 1, vector[@0x10], vector[@0x40], false, true, false); + assert!(merged_config == expected_config, 0); + } + + #[test] + fun test_merge_configs_should_use_the_default_optional_dvns_and_threshold_when_use_default_for_optional_dvns_is_set_to_true( + ) { + let default_config = new_uln_config(2, 1, vector[@0x10], vector[@0x20], false, false, false); + let oapp_config = new_uln_config(3, 0, vector[@0x40], vector[], false, false, true); + let merged_config = merge_uln_configs(&default_config, &oapp_config); + let expected_config = new_uln_config(3, 1, vector[@0x40], vector[@0x20], false, false, true); + + assert!(merged_config == expected_config, 0); + } + + #[test] + fun test_merge_configs_should_use_the_default_confirmations_when_use_default_for_confirmations_is_set_to_true() { + let default_config = new_uln_config(2, 1, vector[@0x10], vector[@0x20], false, false, false); + let oapp_config = new_uln_config(0, 1, vector[@0x40], vector[@0x50], true, false, false); + let merged_config = merge_uln_configs(&default_config, &oapp_config); + let expected_config = new_uln_config(2, 1, vector[@0x40], vector[@0x50], true, false, false); + assert!(merged_config == expected_config, 0); + } + + #[test] + fun test_merge_configs_should_merge_multiple_fields() { + let default_config = new_uln_config(2, 1, vector[@0x10], vector[@0x20], false, false, false); + let oapp_config = new_uln_config(3, 0, vector[], vector[], true, true, true); + let merged_config = merge_uln_configs(&default_config, &oapp_config); + let expected_config = new_uln_config(2, 1, vector[@0x10], vector[@0x20], true, true, true); + assert!(merged_config == expected_config, 0); + } + + #[test] + fun get_config_should_get_the_send_uln_config_if_that_is_requested() { + setup(); + let send_config = new_uln_config(1, 1, vector[@0x10], vector[@0x30, @0x50], false, false, false); + set_default_send_uln_config(1, send_config); + let receive_config = new_uln_config(2, 1, vector[@0x20], vector[@0x40, @0x60], false, false, false); + set_default_receive_uln_config(1, receive_config); + let executor_config = new_executor_config(1000, @9001); + set_default_executor_config(1, executor_config); + + let retrieved_config = configuration::get_config(@9999, 1, 2); + let expected_config = bytes_of(|buf| configs_uln::append_uln_config(buf, send_config)); + assert!(expected_config == retrieved_config, 0); + } + + #[test] + fun get_config_should_get_the_receive_uln_config_if_that_is_requested() { + setup(); + let send_config = new_uln_config(1, 1, vector[@0x10], vector[@0x30, @0x50], false, false, false); + set_default_send_uln_config(1, send_config); + let receive_config = new_uln_config(2, 1, vector[@0x20], vector[@0x40, @0x60], false, false, false); + set_default_receive_uln_config(1, receive_config); + let executor_config = new_executor_config(1000, @9001); + set_default_executor_config(1, executor_config); + + let retrieved_config = configuration::get_config(@9999, 1, 3); + let expected_config = bytes_of(|buf| configs_uln::append_uln_config(buf, receive_config)); + assert!(expected_config == retrieved_config, 0); + } + + #[test] + fun get_config_should_get_the_executor_config_if_that_is_requested() { + setup(); + let send_config = new_uln_config(1, 1, vector[@0x10], vector[@0x30, @0x50], false, false, false); + set_default_send_uln_config(1, send_config); + let receive_config = new_uln_config(2, 1, vector[@0x20], vector[@0x40, @0x60], false, false, false); + set_default_receive_uln_config(1, receive_config); + let executor_config = new_executor_config(1000, @9001); + set_default_executor_config(1, executor_config); + + let retrieved_config = configuration::get_config(@9999, 1, 1); + let expected_config = bytes_of(|buf| configs_executor::append_executor_config(buf, executor_config)); + assert!(expected_config == retrieved_config, 0); + } +} diff --git a/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/tests/internal/for_each_dvn_tests.move b/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/tests/internal/for_each_dvn_tests.move new file mode 100644 index 00000000..e0c299e9 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/tests/internal/for_each_dvn_tests.move @@ -0,0 +1,18 @@ +#[test_only] +module uln_302::for_each_dvn_tests { + use std::vector; + + use uln_302::for_each_dvn::for_each_dvn; + + #[test] + fun test_for_each_dvn() { + let required_dvns = vector[@1, @2, @3]; + let optional_dvns = vector[@4, @5, @6]; + let expected_dvns = vector[@1, @2, @3, @4, @5, @6]; + + for_each_dvn(&required_dvns, &optional_dvns, |dvn, idx| { + let expected_dvn = *vector::borrow(&expected_dvns, idx); + assert!(dvn == expected_dvn, 1); + }); + } +} diff --git a/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/tests/internal/msglib_tests.move b/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/tests/internal/msglib_tests.move new file mode 100644 index 00000000..411d25d0 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/tests/internal/msglib_tests.move @@ -0,0 +1,102 @@ +#[test_only] +module uln_302::msglib_tests { + use std::option; + + use msglib_types::configs_executor::new_executor_config; + use msglib_types::configs_uln::new_uln_config; + use uln_302::configuration::{ + set_default_executor_config, set_default_receive_uln_config, set_default_send_uln_config, set_executor_config, + set_receive_uln_config, set_send_uln_config, + }; + use uln_302::msglib; + + fun setup() { + msglib::initialize_for_test(); + } + + #[test] + fun test_get_app_send_config() { + setup(); + let send_config = new_uln_config(1, 1, vector[@0x10], vector[@0x30, @0x50], false, false, false); + set_default_send_uln_config(1, send_config); + + set_send_uln_config(@9999, 1, send_config); + + let retrieved_config = msglib::get_app_send_config(@9999, 1); + assert!(retrieved_config == option::some(send_config), 0); + } + + #[test] + fun test_get_app_send_config_should_return_option_none_if_only_default_is_set() { + setup(); + let send_config = new_uln_config(1, 1, vector[@0x10], vector[@0x30, @0x50], false, false, false); + set_default_send_uln_config(1, send_config); + + let retrieved_config = msglib::get_app_send_config(@9999, 1); + assert!(retrieved_config == option::none(), 0); + } + + #[test] + fun get_app_send_config_should_return_option_none_if_default_not_set() { + setup(); + let retrieved_config = msglib::get_app_send_config(@9999, 1); + assert!(retrieved_config == option::none(), 0); + } + + #[test] + fun test_get_app_receive_config() { + setup(); + let receive_config = new_uln_config(2, 1, vector[@0x20], vector[@0x40, @0x60], false, false, false); + set_default_receive_uln_config(1, receive_config); + + set_receive_uln_config(@9999, 1, receive_config); + + let retrieved_config = msglib::get_app_receive_config(@9999, 1); + assert!(retrieved_config == option::some(receive_config), 0); + } + + #[test] + fun test_get_app_receive_config_should_return_option_none_if_only_default_is_set() { + setup(); + let receive_config = new_uln_config(2, 1, vector[@0x20], vector[@0x40, @0x60], false, false, false); + set_default_receive_uln_config(1, receive_config); + + let retrieved_config = msglib::get_app_receive_config(@9999, 1); + assert!(retrieved_config == option::none(), 0); + } + + #[test] + fun get_app_receive_config_should_return_option_none_if_default_not_set() { + setup(); + let retrieved_config = msglib::get_app_receive_config(@9999, 1); + assert!(retrieved_config == option::none(), 0); + } + + #[test] + fun test_get_app_executor_config() { + setup(); + let executor_config = new_executor_config(1000, @9001); + set_default_executor_config(1, executor_config); + set_executor_config(@9999, 1, executor_config); + + let retrieved_config = msglib::get_app_executor_config(@9999, 1); + assert!(retrieved_config == option::some(executor_config), 0); + } + + #[test] + fun test_get_app_executor_config_should_return_option_none_if_only_default_is_set() { + setup(); + let executor_config = new_executor_config(1000, @9001); + set_default_executor_config(1, executor_config); + + let retrieved_config = msglib::get_app_executor_config(@9999, 1); + assert!(retrieved_config == option::none(), 0); + } + + #[test] + fun get_app_executor_config_should_return_option_none_if_default_not_set() { + setup(); + let retrieved_config = msglib::get_app_executor_config(@9999, 1); + assert!(retrieved_config == option::none(), 0); + } +} \ No newline at end of file diff --git a/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/tests/internal/sending_tests.move b/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/tests/internal/sending_tests.move new file mode 100644 index 00000000..9c1e723b --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/tests/internal/sending_tests.move @@ -0,0 +1,331 @@ +#[test_only] +module uln_302::sending_tests { + use std::event::was_event_emitted; + use std::fungible_asset::FungibleAsset; + use std::option::{Self, destroy_none}; + use std::vector; + + use endpoint_v2_common::bytes32; + use endpoint_v2_common::guid; + use endpoint_v2_common::native_token_test_helpers::{burn_token_for_test, mint_native_token_for_test}; + use endpoint_v2_common::packet_raw; + use endpoint_v2_common::packet_v1_codec; + use endpoint_v2_common::send_packet; + use endpoint_v2_common::serde::flatten; + use msglib_types::configs_executor::new_executor_config; + use msglib_types::configs_uln::new_uln_config; + use msglib_types::worker_options::{ + append_dvn_option, + append_generic_type_3_executor_option, + new_empty_type_3_options, + }; + use treasury::treasury; + use uln_302::configuration; + use uln_302::sending::{dvn_fee_paid_event, executor_fee_paid_event, quote_internal, send_internal}; + use uln_302::uln_302_store; + + #[test] + fun test_send_internal() { + uln_302_store::init_module_for_test(); + treasury::init_module_for_test(); + configuration::set_default_send_uln_config(103, new_uln_config( + 5, + 1, + vector[@10001], + vector[@10002, @10003], + false, + false, + false, + )); + configuration::set_default_executor_config(103, new_executor_config( + 10000, + @10100, + )); + + let native_token = mint_native_token_for_test(10000); + let zro_token = option::none(); + + let send_packet = send_packet::new_send_packet( + 1, + 102, + bytes32::from_address(@1234), + 103, + bytes32::from_address(@5678), + b"payload", + ); + + let worker_options = new_empty_type_3_options(); + append_dvn_option(&mut worker_options, + 0, + 9, + x"aa00", + ); + append_dvn_option(&mut worker_options, + 2, + 9, + x"aa20", + ); + append_dvn_option(&mut worker_options, + 2, + 5, + x"aa21", + ); + append_generic_type_3_executor_option(&mut worker_options, b"777"); + + let get_executor_fee_call_count = 0; + let get_dvn_fee_call_count = 0; + let called_dvns = vector
[]; + + // needed to prevent the compiler warning + assert!(get_executor_fee_call_count == 0, 0); + assert!(get_dvn_fee_call_count == 0, 0); + assert!(vector::length(&called_dvns) == 0, 0); + + let expected_packet_header = packet_v1_codec::new_packet_v1_header_only( + 102, + bytes32::from_address(@1234), + 103, + bytes32::from_address(@5678), + 1, + ); + let guid = bytes32::from_bytes32(guid::compute_guid( + 1, + 102, + bytes32::from_address(@1234), + 103, + bytes32::from_address(@5678), + )); + + let expected_payload_hash = bytes32::keccak256(flatten(vector[guid, b"payload"])); + + let (native_fee, zro_fee, encoded_packet) = send_internal( + send_packet, + worker_options, + &mut native_token, + &mut zro_token, + |executor_address, executor_options| { + get_executor_fee_call_count = get_executor_fee_call_count + 1; + assert!(executor_address == @10100, 101); + let expected_option = flatten(vector[ + x"01", // executor option + x"0003", // length = 3 + b"777" // option + ]); + assert!(executor_options == expected_option, 102); + (101, @555) + }, + |dvn_address, confirmations, dvn_options, packet_header, payload_hash| { + assert!(packet_header == expected_packet_header, 0); + assert!(payload_hash == expected_payload_hash, 0); + get_dvn_fee_call_count = get_dvn_fee_call_count + 1; + vector::push_back(&mut called_dvns, dvn_address); + assert!(confirmations == 5, 0); + if (dvn_address == @10001) { + let expected_option = flatten(vector[ + x"02", // dvn option + x"0004", // length = 4 + x"00", // dvn index + x"09", // option type + x"aa00", // option + ]); + assert!(dvn_options == expected_option, 0); + (203, @777) + } else if (dvn_address == @10002) { + let expected_option = x""; // empty + assert!(dvn_options == expected_option, 0); + + (204, @778) + } else if (dvn_address == @10003) { + let expected_option = flatten(vector[ + // first option + x"02", // dvn option + x"0004", // length = 4 + x"02", // dvn index + x"09", // option type + x"aa20", // option + // second option + x"02", // dvn option + x"0004", // length = 4 + x"02", // dvn index + x"05", // option type + x"aa21", // option + ]); + assert!(dvn_options == expected_option, 0); + (205, @779) + } else { + (1, @111) + } + }, + ); + + // (203 + 204 + 205) + 101 = 713 + assert!(native_fee == 713, 0); + assert!(zro_fee == 0, 0); + + let expected_encoded_packet = flatten(vector[ + packet_raw::get_packet_bytes(expected_packet_header), + guid, + b"payload", + ]); + assert!(packet_raw::get_packet_bytes(encoded_packet) == expected_encoded_packet, 0); + + assert!(get_executor_fee_call_count == 1, 1); + assert!(get_dvn_fee_call_count == 3, 1); + assert!(vector::contains(&called_dvns, &@10001), 0); + assert!(vector::contains(&called_dvns, &@10002), 0); + assert!(vector::contains(&called_dvns, &@10003), 0); + + assert!(was_event_emitted(&executor_fee_paid_event( + @10100, + @555, + 101, + )), 0); + + assert!(was_event_emitted(&dvn_fee_paid_event( + vector[@10001], + vector[@10002, @10003], + vector[203, 204, 205], + vector[@777, @778, @779], + )), 0); + burn_token_for_test(native_token); + destroy_none(zro_token); + } + + #[test] + fun test_quote_internal() { + uln_302_store::init_module_for_test(); + treasury::init_module_for_test(); + configuration::set_default_send_uln_config(103, new_uln_config( + 5, + 1, + vector[@10001], + vector[@10002, @10003], + false, + false, + false, + )); + configuration::set_default_executor_config(103, new_executor_config( + 10000, + @10100, + )); + + let send_packet = send_packet::new_send_packet( + 1, + 102, + bytes32::from_address(@1234), + 103, + bytes32::from_address(@5678), + b"payload", + ); + + let worker_options = new_empty_type_3_options(); + append_dvn_option(&mut worker_options, + 0, + 9, + x"aa00", + ); + append_dvn_option(&mut worker_options, + 2, + 9, + x"aa20", + ); + append_dvn_option(&mut worker_options, + 2, + 5, + x"aa21", + ); + append_generic_type_3_executor_option(&mut worker_options, b"777"); + + let get_executor_fee_call_count = 0; + let get_dvn_fee_call_count = 0; + + // needed to prevent the compiler warning + assert!(get_executor_fee_call_count == 0, 0); + assert!(get_dvn_fee_call_count == 0, 0); + + let expected_packet_header = packet_v1_codec::new_packet_v1_header_only( + 102, + bytes32::from_address(@1234), + 103, + bytes32::from_address(@5678), + 1, + ); + let guid = bytes32::from_bytes32(guid::compute_guid( + 1, + 102, + bytes32::from_address(@1234), + 103, + bytes32::from_address(@5678), + )); + + let expected_payload_hash = bytes32::keccak256(flatten(vector[guid, b"payload"])); + let called_dvns = vector
[]; + assert!(vector::length(&called_dvns) == 0, 0); + + let (native_fee, zro_fee) = quote_internal( + send_packet, + worker_options, + false, + |executor_address, executor_options| { + get_executor_fee_call_count = get_executor_fee_call_count + 1; + assert!(executor_address == @10100, 101); + let expected_option = flatten(vector[ + x"01", // executor option + x"0003", // length = 3 + b"777" // option + ]); + assert!(executor_options == expected_option, 102); + (101, @555) + }, + |dvn_address, confirmations, dvn_options, packet_header, payload_hash| { + assert!(packet_header == expected_packet_header, 0); + assert!(payload_hash == expected_payload_hash, 0); + get_dvn_fee_call_count = get_dvn_fee_call_count + 1; + vector::push_back(&mut called_dvns, dvn_address); + assert!(confirmations == 5, 0); + if (dvn_address == @10001) { + let expected_option = flatten(vector[ + x"02", // dvn option + x"0004", // length = 4 + x"00", // dvn index + x"09", // option type + x"aa00", // option + ]); + assert!(dvn_options == expected_option, 0); + }; + if (dvn_address == @10002) { + let expected_option = x""; // empty + assert!(dvn_options == expected_option, 0); + }; + if (dvn_address == @10003) { + let expected_option = flatten(vector[ + // first option + x"02", // dvn option + x"0004", // length = 4 + x"02", // dvn index + x"09", // option type + x"aa20", // option + // second option + x"02", // dvn option + x"0004", // length = 4 + x"02", // dvn index + x"05", // option type + x"aa21", // option + ]); + assert!(dvn_options == expected_option, 0); + }; + (203, @777) + }, + ); + + // 203 * 3 + 101 = 710 + assert!(native_fee == 710, 0); + assert!(zro_fee == 0, 0); + + assert!(get_executor_fee_call_count == 1, 1); + assert!(get_dvn_fee_call_count == 3, 1); + assert!(vector::contains(&called_dvns, &@10001), 0); + assert!(vector::contains(&called_dvns, &@10002), 0); + assert!(vector::contains(&called_dvns, &@10003), 0); + } +} diff --git a/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/tests/internal/verification_tests.move b/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/tests/internal/verification_tests.move new file mode 100644 index 00000000..bfa52e26 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/msglib/libs/uln_302/tests/internal/verification_tests.move @@ -0,0 +1,378 @@ +#[test_only] +module uln_302::verification_tests { + use endpoint_v2_common::bytes32; + use endpoint_v2_common::guid::compute_guid; + use endpoint_v2_common::packet_raw::{bytes_to_raw_packet, get_packet_bytes}; + use endpoint_v2_common::packet_v1_codec; + use endpoint_v2_common::universal_config; + use msglib_types::configs_uln; + use uln_302::configuration; + use uln_302::uln_302_store; + use uln_302::verification::{ + check_verifiable, + commit_verification, + get_receive_uln_config_from_packet_header, + is_verified, + reclaim_storage, + verify, + verify_and_reclaim_storage, + }; + + fun setup() { + universal_config::init_module_for_test(33333); + uln_302_store::init_module_for_test(); + } + + #[test] + fun test_verify_saves_entry_and_emits_event() { + setup(); + + verify( + @1111, + bytes_to_raw_packet(b"header"), + bytes32::keccak256(b"payload_hash"), + 5, + ); + + let expected_header_hash = bytes32::to_bytes32( + x"1fe1673da51f096dc3720c34d3002519bd6c4e0d13dc62302f0d04c06d30786e" + ); + let confirmations = uln_302_store::get_verification_confirmations( + expected_header_hash, + bytes32::keccak256(b"payload_hash"), + @1111, + ); + + assert!(confirmations == 5, 0); + } + + #[test] + fun test_check_verifiable_returns_true_when_verified() { + setup(); + + let config = configs_uln::new_uln_config( + 2, + 0, + vector[@123], + vector[], + false, + false, + false, + ); + configuration::set_default_receive_uln_config(1, config); + + verify(@123, bytes_to_raw_packet(b"header"), bytes32::keccak256(b"payload_hash"), 2); + + let verifiable = check_verifiable( + &config, + bytes32::keccak256(b"header"), + bytes32::keccak256(b"payload_hash"), + ); + + assert!(verifiable, 0); + } + + #[test] + fun test_check_verifiable_returns_false_when_not_verified() { + setup(); + + let config = configs_uln::new_uln_config( + 2, + 2, + vector[@123], + vector[@456, @567, @678], + false, + false, + false, + ); + configuration::set_default_receive_uln_config(1, config); + + let packet_header = bytes_to_raw_packet(b"header"); + verify(@123, packet_header, bytes32::keccak256(b"payload_hash"), 2); + verify(@456, packet_header, bytes32::keccak256(b"payload_hash"), 3); + verify(@567, packet_header, bytes32::keccak256(b"payload_hash"), 1 /* insufficient confirmations */); + + let verifiable = check_verifiable( + &config, + bytes32::keccak256(b"header"), + bytes32::keccak256(b"payload_hash"), + ); + + assert!(!verifiable, 0); + } + + #[test] + fun test_is_verified_returns_false_when_not_verified() { + setup(); + + let verified = is_verified( + @1111, + bytes32::keccak256(b"header"), + bytes32::keccak256(b"payload_hash"), + 2, + ); + + assert!(!verified, 0); + } + + #[test] + fun test_commit_verification_reclaims_storage() { + setup(); + + let default_config = configs_uln::new_uln_config( + 2, + 2, + vector[@123], + vector[@456, @567, @678], + false, + false, + false, + ); + configuration::set_default_receive_uln_config(22222, default_config); + + // Store an oapp specific config + let oapp_config = configs_uln::new_uln_config( + 2, + 0, + vector[@712], // Intentionally different from default, to make sure Oapp config is used + vector[], + false, + false, + false, + ); + configuration::set_receive_uln_config(@0x999999, 22222, oapp_config); + let header = packet_v1_codec::new_packet_v1_header_only( + 22222, // matches eid in config + bytes32::to_bytes32(x"0000000000000000000000000000000000000000000000000000000001111111"), + 33333, + bytes32::to_bytes32( + x"0000000000000000000000000000000000000000000000000000000000999999" + ), // matches the oapp address + 123456, + ); + let header_hash = bytes32::keccak256(get_packet_bytes(header)); + let payload_hash = bytes32::keccak256(b"payload_hash"); + let dvn_address = @712; + + // Receive a verification from the oapp selected required DVN + verify( + dvn_address, + header, + payload_hash, + 2, + ); + + // Confirmations should be present before reclaiming + assert!(uln_302_store::has_verification_confirmations(header_hash, payload_hash, dvn_address), 0); + + commit_verification( + header, + bytes32::keccak256(b"payload_hash"), + ); + + // Confirmations should be removed after reclaiming + assert!(!uln_302_store::has_verification_confirmations(header_hash, payload_hash, dvn_address), 0); + + let verifiable = check_verifiable( + &oapp_config, + header_hash, + bytes32::keccak256(b"payload_hash"), + ); + + assert!(!verifiable, 0); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2_common::packet_v1_codec::EINVALID_PACKET_HEADER)] + fun test_commit_verification_asserts_packet_header() { + setup(); + + commit_verification( + bytes_to_raw_packet(b"header"), + bytes32::keccak256(b"payload_hash"), + ); + } + + #[test] + fun test_is_verified_returns_false_when_verified_but_insufficient_confirmations() { + setup(); + + verify( + @1111, + bytes_to_raw_packet(b"header"), + bytes32::keccak256(b"payload_hash"), + 1, + ); + + let verified = is_verified( + @1111, + bytes32::keccak256(b"header"), + bytes32::keccak256(b"payload_hash"), + 2, + ); + + assert!(!verified, 0); + } + + + #[test] + fun test_is_verified_returns_true_when_verified() { + setup(); + + verify( + @1111, + bytes_to_raw_packet(b"header"), + bytes32::keccak256(b"payload_hash"), + 5, + ); + + let verified = is_verified( + @1111, + bytes32::keccak256(b"header"), + bytes32::keccak256(b"payload_hash"), + 2, + ); + + assert!(verified, 0); + } + + #[test] + fun test_get_receive_uln_config_from_packet_header_returns_default_config() { + setup(); + + let default_config = configs_uln::new_uln_config( + 2, + 2, + vector[@123], + vector[@456, @567, @678], + false, + false, + false, + ); + configuration::set_default_receive_uln_config(22222, default_config); + + let guid = compute_guid( + 123456, + 22222, + bytes32::from_address(@0x654321), + 33333, + bytes32::from_address(@0x123456) + ); + let header = packet_v1_codec::new_packet_v1( + 22222, + bytes32::from_address(@0x654321), + 33333, + bytes32::from_address(@0x123456), + 123456, + guid, + b"", + ); + let config = get_receive_uln_config_from_packet_header(&header); + assert!(config == default_config, 0); + } + + + #[test] + fun test_verify_and_reclaim_storage_reclaims_when_verified() { + setup(); + + let config = configs_uln::new_uln_config( + 2, + 0, + vector[@123], + vector[], + false, + false, + false, + ); + configuration::set_default_receive_uln_config(1, config); + + let dvn_address = @123; + let payload_hash = bytes32::keccak256(b"payload"); + verify( + dvn_address, + bytes_to_raw_packet(b"header"), + payload_hash, + 2, + ); + + let header_hash = bytes32::keccak256(b"header"); + + // Confirmations should be present before reclaiming + assert!(uln_302_store::has_verification_confirmations(header_hash, payload_hash, dvn_address), 0); + assert!(uln_302_store::get_verification_confirmations(header_hash, payload_hash, dvn_address) == 2, 0); + + verify_and_reclaim_storage( + &config, + header_hash, + payload_hash, + ); + + // Confirmations should be removed after reclaiming + assert!(!uln_302_store::has_verification_confirmations(header_hash, payload_hash, dvn_address), 0); + + let verifiable = check_verifiable( + &config, + bytes32::keccak256(b"header"), + bytes32::keccak256(b"payload_hash"), + ); + + assert!(!verifiable, 0); + } + + #[test] + fun test_reclaim_storage_removes_required_and_optional_confirmations() { + setup(); + + let required_dvns = vector[@123, @456]; + let optional_dvns = vector[@789]; + let header = bytes_to_raw_packet(b"header"); + let header_hash = bytes32::keccak256(get_packet_bytes(header)); + let payload_hash = bytes32::keccak256(b"payload_hash"); + + verify(@123, header, payload_hash, 2); + verify(@456, header, payload_hash, 2); + verify(@789, header, payload_hash, 2); + + // Confirmations should be present before reclaiming + assert!(uln_302_store::has_verification_confirmations(header_hash, payload_hash, @123), 0); + assert!(uln_302_store::has_verification_confirmations(header_hash, payload_hash, @456), 1); + assert!(uln_302_store::has_verification_confirmations(header_hash, payload_hash, @789), 2); + + reclaim_storage(&required_dvns, &optional_dvns, header_hash, payload_hash); + + // Confirmations should be removed after reclaiming + assert!(!uln_302_store::has_verification_confirmations(header_hash, payload_hash, @123), 3); + assert!(!uln_302_store::has_verification_confirmations(header_hash, payload_hash, @456), 4); + assert!(!uln_302_store::has_verification_confirmations(header_hash, payload_hash, @789), 4); + } + + #[test] + fun test_check_verifiable_returns_true_verified_larger_set() { + setup(); + + let config = configs_uln::new_uln_config( + 2, + 2, + vector[@123], + vector[@456, @567, @678], + false, + false, + false, + ); + configuration::set_default_receive_uln_config(1, config); + + let header = bytes_to_raw_packet(b"header"); + verify(@123, header, bytes32::keccak256(b"payload_hash"), 2); + verify(@456, header, bytes32::keccak256(b"payload_hash"), 3); + verify(@567, header, bytes32::keccak256(b"payload_hash"), 2); + + let verifiable = check_verifiable( + &config, + bytes32::keccak256(b"header"), + bytes32::keccak256(b"payload_hash"), + ); + + assert!(verifiable, 0); + } +} \ No newline at end of file diff --git a/packages/layerzero-v2/aptos/contracts/msglib/msglib_types/Move.toml b/packages/layerzero-v2/aptos/contracts/msglib/msglib_types/Move.toml new file mode 100644 index 00000000..32d48f5d --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/msglib/msglib_types/Move.toml @@ -0,0 +1,26 @@ +[package] +name = "msglib_types" +version = "1.0.0" +authors = [] + +[addresses] +msglib_types = "_" +endpoint_v2_common = "_" +endpoint_v2 = "_" +layerzero_admin = "_" +layerzero_treasury_admin = "_" + +[dev-addresses] +msglib_types = "0x97324123" +endpoint_v2_common = "0x9098" +endpoint_v2 = "0x12345678" +layerzero_admin = "0x18943124" +layerzero_treasury_admin = "0x1894312499" + +[dependencies.AptosFramework] +git = "https://github.com/aptos-labs/aptos-framework.git" +rev = "mainnet" +subdir = "aptos-framework" + +[dependencies] +endpoint_v2_common = { local = "../../endpoint_v2_common" } diff --git a/packages/layerzero-v2/aptos/contracts/msglib/msglib_types/sources/configs_executor.move b/packages/layerzero-v2/aptos/contracts/msglib/msglib_types/sources/configs_executor.move new file mode 100644 index 00000000..b1f2fc53 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/msglib/msglib_types/sources/configs_executor.move @@ -0,0 +1,36 @@ +/// This module contains the serialization and deserialization logic for handling Executor configurations +/// +/// The serialized format is as follows: +/// [max_message_size: u32] +/// [executor_address: bytes32] +module msglib_types::configs_executor { + use endpoint_v2_common::serde::{append_address, append_u32, extract_address, extract_u32}; + + struct ExecutorConfig has drop, copy, store { + max_message_size: u32, + executor_address: address, + } + + public fun new_executor_config(max_message_size: u32, executor_address: address): ExecutorConfig { + ExecutorConfig { max_message_size, executor_address } + } + + // =================================================== Accessors ================================================== + + public fun get_max_message_size(self: &ExecutorConfig): u32 { self.max_message_size } + + public fun get_executor_address(self: &ExecutorConfig): address { self.executor_address } + + // ======================================== Serialization / Deserialization ======================================= + + public fun append_executor_config(bytes: &mut vector, config: ExecutorConfig) { + append_u32(bytes, config.max_message_size); + append_address(bytes, config.executor_address); + } + + public fun extract_executor_config(bytes: &vector, position: &mut u64): ExecutorConfig { + let max_message_size = extract_u32(bytes, position); + let executor_address = extract_address(bytes, position); + ExecutorConfig { max_message_size, executor_address } + } +} \ No newline at end of file diff --git a/packages/layerzero-v2/aptos/contracts/msglib/msglib_types/sources/configs_uln.move b/packages/layerzero-v2/aptos/contracts/msglib/msglib_types/sources/configs_uln.move new file mode 100644 index 00000000..77c67294 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/msglib/msglib_types/sources/configs_uln.move @@ -0,0 +1,127 @@ +/// This module contains the serialization and deserialization logic for handling Send and Receive ULN configurations +module msglib_types::configs_uln { + use std::vector; + + use endpoint_v2_common::serde::{ + append_address, append_u64, append_u8, extract_address, extract_u64, extract_u8, map_count, + }; + + struct UlnConfig has drop, copy, store { + confirmations: u64, + optional_dvn_threshold: u8, + required_dvns: vector
, + optional_dvns: vector
, + use_default_for_confirmations: bool, + use_default_for_required_dvns: bool, + use_default_for_optional_dvns: bool, + } + + public fun new_uln_config( + confirmations: u64, + optional_dvn_threshold: u8, + required_dvns: vector
, + optional_dvns: vector
, + use_default_for_confirmations: bool, + use_default_for_required_dvns: bool, + use_default_for_optional_dvns: bool, + ): UlnConfig { + UlnConfig { + confirmations, + optional_dvn_threshold, + required_dvns, + optional_dvns, + use_default_for_confirmations, + use_default_for_required_dvns, + use_default_for_optional_dvns, + } + } + + // ================================================ Field Accessors =============================================== + + public fun unpack_uln_config(config: UlnConfig): (u64, u8, vector
, vector
, bool, bool, bool) { + let UlnConfig { + confirmations, + optional_dvn_threshold, + required_dvns, + optional_dvns, + use_default_for_confirmations, + use_default_for_required_dvns, + use_default_for_optional_dvns, + } = config; + ( + confirmations, optional_dvn_threshold, required_dvns, optional_dvns, use_default_for_confirmations, + use_default_for_required_dvns, use_default_for_optional_dvns, + ) + } + + public fun get_confirmations(self: &UlnConfig): u64 { self.confirmations } + + public fun get_required_dvn_count(self: &UlnConfig): u64 { vector::length(&self.required_dvns) } + + public fun get_optional_dvn_count(self: &UlnConfig): u64 { vector::length(&self.optional_dvns) } + + public fun get_optional_dvn_threshold(self: &UlnConfig): u8 { self.optional_dvn_threshold } + + public fun get_required_dvns(self: &UlnConfig): vector
{ self.required_dvns } + + public fun borrow_required_dvns(self: &UlnConfig): &vector
{ &self.required_dvns } + + public fun get_optional_dvns(self: &UlnConfig): vector
{ self.optional_dvns } + + public fun borrow_optional_dvns(self: &UlnConfig): &vector
{ &self.optional_dvns } + + public fun get_use_default_for_confirmations(self: &UlnConfig): bool { self.use_default_for_confirmations } + + public fun get_use_default_for_required_dvns(self: &UlnConfig): bool { self.use_default_for_required_dvns } + + public fun get_use_default_for_optional_dvns(self: &UlnConfig): bool { self.use_default_for_optional_dvns } + + + // ======================================== Serialization / Deserialization ======================================= + + public fun append_uln_config(target: &mut vector, config: UlnConfig) { + append_u64(target, config.confirmations); + append_u8(target, config.optional_dvn_threshold); + append_u8(target, (vector::length(&config.required_dvns) as u8)); + vector::for_each(config.required_dvns, |address| append_address(target, address)); + append_u8(target, (vector::length(&config.optional_dvns) as u8)); + vector::for_each(config.optional_dvns, |address| append_address(target, address)); + append_u8(target, from_bool(config.use_default_for_confirmations)); + append_u8(target, from_bool(config.use_default_for_required_dvns)); + append_u8(target, from_bool(config.use_default_for_optional_dvns)); + } + + public fun extract_uln_config(input: &vector, position: &mut u64): UlnConfig { + let confirmations = extract_u64(input, position); + let optional_dvn_threshold = extract_u8(input, position); + let required_dvns_count = extract_u8(input, position); + let required_dvns = map_count((required_dvns_count as u64), |_i| extract_address(input, position)); + let optional_dvns_count = extract_u8(input, position); + let optional_dvns = map_count((optional_dvns_count as u64), |_i| extract_address(input, position)); + let use_default_for_confirmations = to_bool(extract_u8(input, position)); + let use_default_for_required_dvns = to_bool(extract_u8(input, position)); + let use_default_for_optional_dvns = to_bool(extract_u8(input, position)); + + UlnConfig { + confirmations, + optional_dvn_threshold, + required_dvns, + optional_dvns, + use_default_for_confirmations, + use_default_for_required_dvns, + use_default_for_optional_dvns, + } + } + + fun to_bool(uint: u8): bool { + if (uint == 1) { true } else if (uint == 0) { false } else { abort EINVALID_BOOLEAN } + } + + fun from_bool(bool: bool): u8 { + if (bool) { 1 } else { 0 } + } + + // ================================================== Error Codes ================================================= + + const EINVALID_BOOLEAN: u64 = 1; +} \ No newline at end of file diff --git a/packages/layerzero-v2/aptos/contracts/msglib/msglib_types/sources/dvn_verify_params.move b/packages/layerzero-v2/aptos/contracts/msglib/msglib_types/sources/dvn_verify_params.move new file mode 100644 index 00000000..37e907c4 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/msglib/msglib_types/sources/dvn_verify_params.move @@ -0,0 +1,29 @@ +/// These DVN Verify Params are used to verify the DVN packet for ULN302. +/// The format of this may change in future Message Libraries, so is sent through the Message Library router as an +/// `Any` type to ensure the format can be upgraded for future Message Libraries. +module msglib_types::dvn_verify_params { + use std::any::{Self, Any}; + + use endpoint_v2_common::bytes32::Bytes32; + use endpoint_v2_common::packet_raw::RawPacket; + + struct DvnVerifyParams has drop, store { + packet_header: RawPacket, + payload_hash: Bytes32, + confirmations: u64, + } + + public fun pack_dvn_verify_params( + packet_header: RawPacket, + payload_hash: Bytes32, + confirmations: u64, + ): Any { + any::pack(DvnVerifyParams { packet_header, payload_hash, confirmations }) + } + + public fun unpack_dvn_verify_params(params: Any): (RawPacket, Bytes32, u64) { + let params = any::unpack(params); + let DvnVerifyParams { packet_header, payload_hash, confirmations } = params; + (packet_header, payload_hash, confirmations) + } +} diff --git a/packages/layerzero-v2/aptos/contracts/msglib/msglib_types/sources/worker_options.move b/packages/layerzero-v2/aptos/contracts/msglib/msglib_types/sources/worker_options.move new file mode 100644 index 00000000..4dfebb5b --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/msglib/msglib_types/sources/worker_options.move @@ -0,0 +1,226 @@ +module msglib_types::worker_options { + use std::vector; + + use endpoint_v2_common::serde; + + public inline fun EXECUTOR_WORKER_ID(): u8 { 1 } + + public inline fun DVN_WORKER_ID(): u8 { 2 } + + const EXECUTOR_OPTION_TYPE_LZ_RECEIVE: u8 = 1; + const EXECUTOR_OPTION_TYPE_NATIVE_DROP: u8 = 2; + + /// Convenience structure to bind the DVN index and the serialized (concatted) options for that DVN index + struct IndexOptionsPair has copy, drop, store { + options: vector, + dvn_idx: u8, + } + + /// Unpacks the DVN index and the serialized (concatted) options for that DVN index + public fun unpack_index_option_pair(pair: IndexOptionsPair): (u8, vector) { + let IndexOptionsPair { options, dvn_idx } = pair; + (dvn_idx, options) + } + + /// Searches a vector of IndexOptionsPair and returns the concatinated options that matches the given DVN index + /// This returns an empty vector if no match is found + public fun get_matching_options(index_option_pairs: &vector, dvn_index: u8): vector { + for (i in 0..vector::length(index_option_pairs)) { + let pair = vector::borrow(index_option_pairs, i); + if (pair.dvn_idx == dvn_index) { + return pair.options + } + }; + vector[] + } + + // ============================================== Process DNV Options ============================================= + + /// Split Options into Executor and DVN Options + /// @return (executor_options, dvn_options) + public fun extract_and_split_options( + options: &vector, + ): (vector, vector) { + // Options must contain at least 2 bytes (the u16 "option type") to be considered valid + assert!(vector::length(options) >= 2, EINVALID_OPTIONS); + + let uln_options_type = serde::extract_u16(options, &mut 0); + + if (uln_options_type == 3) { + extract_type_3_options(options) + } else { + extract_legacy_options(uln_options_type, options) + } + } + + /// Extracts the current type 3 option format + /// Format: [worker_option][worker_option][worker_option]... + /// Worker Option Format: [worker_id: u8][option_size: u16][option: bytes(option_size)] + /// @return (executor_options, dvn_options) + public fun extract_type_3_options( + options: &vector, + ): (vector, vector) { + // start after the u16 option type + let position: u64 = 2; + + let executor_options = vector[]; + let dvn_options = vector[]; + + // serde extract methods will move the position cursor according to the size of the extracted value + let len = vector::length(options); + while (position < len) { + let internal_cursor = position; + let worker_id = serde::extract_u8(options, &mut internal_cursor); + let option_size = serde::extract_u16(options, &mut internal_cursor); + let total_option_size = (option_size as u64) + 3; // 1 byte for worker_id, 2 bytes for option_size + let option_bytes = serde::extract_fixed_len_bytes(options, &mut position, total_option_size); + if (worker_id == EXECUTOR_WORKER_ID()) { + vector::append(&mut executor_options, option_bytes); + } else if (worker_id == DVN_WORKER_ID()) { + vector::append(&mut dvn_options, option_bytes); + } else { + abort EINVALID_WORKER_ID + }; + }; + + (executor_options, dvn_options) + } + + /// This creates a stem for type 3 options after which a series of executor and/or DVN options can be appended + public fun new_empty_type_3_options(): vector { + x"0003" // type 3 + } + + #[test_only] + /// Test only function to append an executor option to a buffer. This is only for testing the general behavior + /// when the options don't matter. Please use the method provided by the executor fee lib to append fee-lib-specific + /// executor options when not testing + public fun append_generic_type_3_executor_option( + buf: &mut vector, + option: vector, + ) { + serde::append_u8(buf, EXECUTOR_WORKER_ID()); + serde::append_u16(buf, (vector::length(&option) as u16)); + serde::append_bytes(buf, option); + } + + // ============================================ Process Legacy Options ============================================ + + /// Extracts options in legacy formats + /// @return (executor_options, dvn_options) + public fun extract_legacy_options(option_type: u16, options: &vector): (vector, vector) { + // start after the u16 option type + let position: u64 = 2; // skip the option type + let total_options_size = vector::length(options); + let executor_options = vector[]; + + // type 1 and 2 lzReceive options use u256 but type 3 uses u128 + // casting operation is safe: will abort if too large + if (option_type == 1) { + assert!(total_options_size == 34, EINVALID_LEGACY_OPTIONS_TYPE_1); + let execution_gas = (serde::extract_u256(options, &mut position) as u128); + append_legacy_option_lz_receive(&mut executor_options, execution_gas); + } else if (option_type == 2) { + assert!(total_options_size > 66 && total_options_size <= 98, EINVALID_LEGACY_OPTIONS_TYPE_2); + let execution_gas = (serde::extract_u256(options, &mut position) as u128); + + // native_drop (amount + receiver) + let amount = (serde::extract_u256(options, &mut position) as u128); + // receiver addresses are not necessarily bytes32 + let receiver = serde::extract_bytes_until_end(options, &mut position); + receiver = serde::pad_zero_left(receiver, 32); + + append_legacy_option_lz_receive(&mut executor_options, execution_gas); + append_legacy_option_native_drop(&mut executor_options, amount, receiver); + } else { + abort EINVALID_OPTION_TYPE + }; + (executor_options, vector[]) + } + + fun append_legacy_option_lz_receive(buf: &mut vector, execution_gas: u128) { + serde::append_u8(buf, EXECUTOR_WORKER_ID()); + serde::append_u16(buf, 17); // 16 + 1, 16 for option_length, + 1 for option_type + serde::append_u8(buf, EXECUTOR_OPTION_TYPE_LZ_RECEIVE); + serde::append_u128(buf, execution_gas); + } + + fun append_legacy_option_native_drop(buf: &mut vector, amount: u128, receiver: vector) { + serde::append_u8(buf, EXECUTOR_WORKER_ID()); + serde::append_u16(buf, 49); // 48 + 1, 32 + 16 for option_length, + 1 for option_type + serde::append_u8(buf, EXECUTOR_OPTION_TYPE_NATIVE_DROP); + serde::append_u128(buf, amount); + serde::append_bytes(buf, receiver); + } + + // ====================================== Prepare DVN Options for Fee Library ===================================== + + /// Group DVN Options into IndexOptionsPairs, such that each element has a DVN index and a concatted vector of + /// serialized options + /// serialized options + /// Format: { dvn_idx: u8, options: [dvn_option][dvn_option][dvn_option]... + /// DVN Option format: [worker_id][option_size][dvn_idx][option_type][option] + public fun group_dvn_options_by_index(dvn_options_bytes: &vector): vector { + let index_option_pairs = vector[]; + let position: u64 = 0; + let len = vector::length(dvn_options_bytes); + while (position < len) { + let internal_cursor = position; + internal_cursor = internal_cursor + 1; // skip worker_id + let option_size = serde::extract_u16(dvn_options_bytes, &mut internal_cursor); + let dvn_idx = serde::extract_u8(dvn_options_bytes, &mut internal_cursor); + let total_option_size = (option_size as u64) + 3; // 1 byte for worker_id, 2 bytes for option_size + + let option = serde::extract_fixed_len_bytes(dvn_options_bytes, &mut position, total_option_size); + + assert!(option_size >= 2, EINVALID_OPTION_LENGTH); + assert!(dvn_idx != 255, EINVALID_DVN_IDX); + insert_dvn_option(&mut index_option_pairs, dvn_idx, option); + }; + + index_option_pairs + } + + /// Inserts a new DVN option into the vector of IndexOptionsPair, appending to the existing options of the DVN index + /// or creating a new entry if the DVN index does not exist + fun insert_dvn_option( + index_option_pairs: &mut vector, + dvn_idx: u8, + new_options: vector, + ) { + // If the dvn_idx already exists, append the new options to the existing options + let count = vector::length(index_option_pairs); + for (ii in 0..count) { + // Reverse the scan, to save gas when options are appended in ordered groups + let i = count - ii - 1; + let pair = vector::borrow(index_option_pairs, i); + if (pair.dvn_idx == dvn_idx) { + let existing_option = vector::borrow_mut(index_option_pairs, i); + vector::append(&mut existing_option.options, new_options); + return + } + }; + // Otherwise, create a new entry + vector::push_back(index_option_pairs, IndexOptionsPair { options: new_options, dvn_idx }); + } + + // This appends a dvn_option to the buffer + public fun append_dvn_option(buf: &mut vector, dvn_idx: u8, option_type: u8, option: vector) { + serde::append_u8(buf, DVN_WORKER_ID()); + let length = vector::length(&option) + 2; // 2 for option_type and dvn_idx + serde::append_u16(buf, (length as u16)); + serde::append_u8(buf, dvn_idx); + serde::append_u8(buf, option_type); + serde::append_bytes(buf, option); + } + + // ================================================== Error Codes ================================================= + + const EINVALID_DVN_IDX: u64 = 1; + const EINVALID_LEGACY_OPTIONS_TYPE_1: u64 = 2; + const EINVALID_LEGACY_OPTIONS_TYPE_2: u64 = 3; + const EINVALID_OPTIONS: u64 = 4; + const EINVALID_OPTION_LENGTH: u64 = 5; + const EINVALID_OPTION_TYPE: u64 = 6; + const EINVALID_WORKER_ID: u64 = 7; +} \ No newline at end of file diff --git a/packages/layerzero-v2/aptos/contracts/msglib/msglib_types/tests/test_dvn_verify_params.move b/packages/layerzero-v2/aptos/contracts/msglib/msglib_types/tests/test_dvn_verify_params.move new file mode 100644 index 00000000..4c6ac174 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/msglib/msglib_types/tests/test_dvn_verify_params.move @@ -0,0 +1,21 @@ +#[test_only] +module msglib_types::test_dvn_verify_params { + use endpoint_v2_common::bytes32; + use endpoint_v2_common::packet_raw; + + #[test] + fun test_dvn_verify_params_pack_and_unpack() { + let original_header = packet_raw::bytes_to_raw_packet(b"1234"); + let packed = msglib_types::dvn_verify_params::pack_dvn_verify_params( + original_header, + bytes32::to_bytes32(b"12345678901234567890123456789012"), + 42, + ); + let (packet_header, payload_hash, confirmations) = msglib_types::dvn_verify_params::unpack_dvn_verify_params( + packed, + ); + assert!(packet_header == original_header, 0); + assert!(payload_hash == bytes32::to_bytes32(b"12345678901234567890123456789012"), 1); + assert!(confirmations == 42, 2); + } +} diff --git a/packages/layerzero-v2/aptos/contracts/msglib/msglib_types/tests/worker_options_tests.move b/packages/layerzero-v2/aptos/contracts/msglib/msglib_types/tests/worker_options_tests.move new file mode 100644 index 00000000..2e7795a3 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/msglib/msglib_types/tests/worker_options_tests.move @@ -0,0 +1,159 @@ +#[test_only] +module msglib_types::worker_options_tests { + use std::vector; + + use endpoint_v2_common::serde; + use msglib_types::worker_options; + use msglib_types::worker_options::unpack_index_option_pair; + + #[test] + fun test_extract_and_split_options_dvn_only() { + let option_type = x"0003"; + let dvn_options_raw = x"020002000102000302ff0102000200010200020101"; + let options = serde::flatten(vector[ + option_type, + dvn_options_raw, + ]); + let (executor_options, dvn_options) = worker_options::extract_and_split_options( + &options, + ); + + assert!(executor_options == x"", 0); + assert!(dvn_options == dvn_options_raw, 1); + } + + #[test] + fun test_extract_and_split_options_executor_only() { + let option_type = x"0003"; + let executor_options_raw = x"0100110100000000000000000000000000009470010011010000000000000000000000000000ea60"; + let options = serde::flatten(vector[ + option_type, + executor_options_raw, + ]); + let (executor_options, dvn_options) = worker_options::extract_and_split_options( + &options, + ); + + assert!(executor_options == executor_options_raw, 0); + assert!(dvn_options == x"", 1); + } + + #[test] + fun test_extract_and_split_options() { + let option_type = x"0003"; + let executor_options_raw = x"0100110100000000000000000000000000009470010011010000000000000000000000000000ea60"; + let dvn_options_raw = x"020002000102000302ff0102000200010200020101"; + let options = serde::flatten(vector[ + option_type, + executor_options_raw, + dvn_options_raw, + ]); + + let (executor_options, dvn_options) = worker_options::extract_and_split_options( + &options, + ); + + assert!(executor_options == executor_options_raw, 0); + assert!(dvn_options == dvn_options_raw, 1); + } + + #[test] + fun test_decode_legacy_options_type_1() { + let option_type = 1; + let legacy_options = x"00010000000000000000000000000000000000000000000000000000000000030d40"; + let expected_options = x"0100110100000000000000000000000000030d40"; + + let (executor_options, _) = worker_options::extract_legacy_options(option_type, &legacy_options); + // assert that the new executor option follows: [worker_id][option_size][option_type][option] + assert!(executor_options == expected_options, 0); + let pos = &mut 0; + assert!(serde::extract_u8(&executor_options, pos) == 1, 1); // worker_id + assert!(serde::extract_u16(&executor_options, pos) == 17, 2); // option_size + assert!(serde::extract_u8(&executor_options, pos) == 1, 3); // option_type + assert!(serde::extract_u128(&executor_options, pos) == 200000, 4); // option value (execution gas) + } + + #[test] + fun test_decode_legacy_options_type_2() { + let option_type = 2; + let legacy_options = x"00020000000000000000000000000000000000000000000000000000000000030d400000000000000000000000000000000000000000000000000000000000989680f39fd6e51aad88f6f4ce6ab8827279cfffb92266"; + let expected_options = x"0100110100000000000000000000000000030d400100310200000000000000000000000000989680000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266"; + let (executor_options, _) = worker_options::extract_legacy_options(option_type, &legacy_options); + + // adapter params type 2 includes both 1 and 2 + assert!(executor_options == expected_options, 0); + let pos = &mut 0; + // adapter params type 1 + assert!(serde::extract_u8(&executor_options, pos) == 1, 1); // worker_id + assert!(serde::extract_u16(&executor_options, pos) == 17, 2); // option_size + assert!(serde::extract_u8(&executor_options, pos) == 1, 3); // option_type + assert!(serde::extract_u128(&executor_options, pos) == 200000, 4); // option value (execution gas) + // adapter params type 2 + assert!(serde::extract_u8(&executor_options, pos) == 1, 5); // worker_id + assert!(serde::extract_u16(&executor_options, pos) == 49, 6); // option_size + assert!(serde::extract_u8(&executor_options, pos) == 2, 7); // option_type + assert!(serde::extract_u128(&executor_options, pos) == 10000000, 8); // option value (amount) + let expected_receiver = endpoint_v2_common::bytes32::to_bytes32( + x"000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266" + ); + assert!(serde::extract_bytes32(&executor_options, pos) == expected_receiver, 9); // option value (receiver) + } + + #[test] + fun test_extract_and_split_options_using_legacy_option() { + let legacy_options = x"00020000000000000000000000000000000000000000000000000000000000030d400000000000000000000000000000000000000000000000000000000000989680f39fd6e51aad88f6f4ce6ab8827279cfffb92266"; + let expected_options = x"0100110100000000000000000000000000030d400100310200000000000000000000000000989680000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266"; + let (executor_options, _) = worker_options::extract_and_split_options(&legacy_options); + + // adapter params type 2 includes both 1 and 2 + assert!(executor_options == expected_options, 0); + let pos = &mut 0; + // adapter params type 1 + assert!(serde::extract_u8(&executor_options, pos) == 1, 1); // worker_id + assert!(serde::extract_u16(&executor_options, pos) == 17, 2); // option_size + assert!(serde::extract_u8(&executor_options, pos) == 1, 3); // option_type + assert!(serde::extract_u128(&executor_options, pos) == 200000, 4); // option value (execution gas) + // adapter params type 2 + assert!(serde::extract_u8(&executor_options, pos) == 1, 5); // worker_id + assert!(serde::extract_u16(&executor_options, pos) == 49, 6); // option_size + assert!(serde::extract_u8(&executor_options, pos) == 2, 7); // option_type + assert!(serde::extract_u128(&executor_options, pos) == 10000000, 8); // option value (amount) + let expected_receiver = endpoint_v2_common::bytes32::to_bytes32( + x"000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266" + ); + assert!(serde::extract_bytes32(&executor_options, pos) == expected_receiver, 9); // option value (receiver) + } + + #[test] + fun test_group_dvn_options_by_index() { + let dvn_option_bytes = x"020002000102000302ff0102000200010200020101"; + let expected_dvn_0_options = x"02000200010200020001"; + let expected_dvn_1_options = x"0200020101"; + let expected_dvn_2_options = x"02000302ff01"; + + let pairs = worker_options::group_dvn_options_by_index(&dvn_option_bytes); + + + let found_0 = false; + let found_1 = false; + let found_2 = false; + + // get_all_dvn_fee logic check + for (i in 0..vector::length(&pairs)) { + let (index, option) = unpack_index_option_pair(*vector::borrow(&pairs, i)); + if (index == 0) { + found_0 = true; + assert!(option == expected_dvn_0_options, 0); + }; + if (index == 1) { + found_1 = true; + assert!(option == expected_dvn_1_options, 1); + }; + if (index == 2) { + found_2 = true; + assert!(option == expected_dvn_2_options, 2); + }; + }; + assert!(found_0 && found_1 && found_2, 3); + } +} diff --git a/packages/layerzero-v2/aptos/contracts/msglib/routers/router_node_0/Move.toml b/packages/layerzero-v2/aptos/contracts/msglib/routers/router_node_0/Move.toml new file mode 100644 index 00000000..b1d9a4f3 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/msglib/routers/router_node_0/Move.toml @@ -0,0 +1,65 @@ +[package] +name = "router_node_0" +version = "1.0.0" +authors = [] + +[addresses] +router_node_0 = "_" +endpoint_v2_common = "_" +endpoint_v2 = "_" +layerzero_admin = "_" +layerzero_treasury_admin = "_" +simple_msglib = "_" +blocked_msglib = "_" +uln_302 = "_" +router_node_1 = "_" +msglib_types = "_" +treasury = "_" +price_feed_router_0 = "_" +price_feed_router_1 = "_" +price_feed_module_0 = "_" +worker_common = "_" +executor_fee_lib_0 = "_" +dvn_fee_lib_0 = "_" +executor_fee_lib_router_0 = "_" +executor_fee_lib_router_1 = "_" +dvn_fee_lib_router_0 = "_" +dvn_fee_lib_router_1 = "_" +dvn = "_" +native_token_metadata_address = "0xa" + +[dev-addresses] +router_node_0 = "0x9001" +endpoint_v2_common = "0x9098" +endpoint_v2 = "0x12345678" +layerzero_admin = "0x18943124" +layerzero_treasury_admin = "0x1894312499" +simple_msglib = "0x9002" +blocked_msglib = "0x9003" +uln_302 = "0x9005" +router_node_1 = "0x9007" +msglib_types = "0x52112234" +treasury = "0x123432432" +price_feed_router_0 = "0x65DD71A" +price_feed_router_1 = "0x65DD71AB" +price_feed_module_0 = "0x65DD71" +worker_common = "0x3999" +executor_fee_lib_0 = "0x3000" +dvn_fee_lib_0 = "0x3000a" +executor_fee_lib_router_0 = "0x30001" +executor_fee_lib_router_1 = "0x30002" +dvn_fee_lib_router_0 = "0x30001a" +dvn_fee_lib_router_1 = "0x30002a" +dvn = "0x234234" + +[dependencies] +simple_msglib = { local = "../../libs/simple_msglib" } +blocked_msglib = { local = "../../libs/blocked_msglib" } +uln_302 = { local = "../../libs/uln_302" } + +router_node_1 = { local = "../router_node_1_placeholder" } +msglib_types = { local = "../../msglib_types" } +endpoint_v2_common = { local = "../../../endpoint_v2_common" } +price_feed_module_0 = { local = "../../../worker_peripherals/price_feed_modules/price_feed_module_0" } + +[dev-dependencies] diff --git a/packages/layerzero-v2/aptos/contracts/msglib/routers/router_node_0/sources/router_node.move b/packages/layerzero-v2/aptos/contracts/msglib/routers/router_node_0/sources/router_node.move new file mode 100644 index 00000000..ab2098b2 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/msglib/routers/router_node_0/sources/router_node.move @@ -0,0 +1,158 @@ +/// This module provides the base branching mechanism for routing to the correct msglib implementation. +/// The design provides multiple msglib slots per router node. Each is checked against the msglib address until the +/// correct implementation is found. If no implementation is found, the request is forwarded to the next router node. +/// The final router node will always be a placeholder contract that will return an error stating that the desired +/// library was not found. +/// Any unused slot points to a upgradable placeholder contract, which makes appending new msglib implementations +/// possible while the router or any msglib contracts can remain permanently undisturbed. +module router_node_0::router_node { + use std::any::Any; + use std::fungible_asset::FungibleAsset; + use std::option::Option; + + use blocked_msglib::router_calls as blocked_msglib; + use endpoint_v2_common::bytes32::Bytes32; + use endpoint_v2_common::contract_identity::DynamicCallRef; + use endpoint_v2_common::packet_raw::RawPacket; + use endpoint_v2_common::send_packet::SendPacket; + use router_node_1::router_node as router_node_next; + use simple_msglib::router_calls as simple_msglib; + use uln_302::router_calls as uln_302; + + public fun quote( + msglib: address, + packet: SendPacket, + options: vector, + pay_in_zro: bool, + ): (u64, u64) { + if (msglib == @uln_302) { + uln_302::quote(packet, options, pay_in_zro) + } else if (msglib == @simple_msglib) { + simple_msglib::quote(packet, options, pay_in_zro) + } else if (msglib == @blocked_msglib) { + blocked_msglib::quote(packet, options, pay_in_zro) + } else { + router_node_next::quote(msglib, packet, options, pay_in_zro) + } + } + + public fun send( + msglib: address, + call_ref: &DynamicCallRef, + packet: SendPacket, + options: vector, + native_token: &mut FungibleAsset, + zro_token: &mut Option, + ): (u64, u64, RawPacket) { + if (msglib == @uln_302) { + uln_302::send(call_ref, packet, options, native_token, zro_token) + } else if (msglib == @simple_msglib) { + simple_msglib::send(call_ref, packet, options, native_token, zro_token) + } else if (msglib == @blocked_msglib) { + blocked_msglib::send(call_ref, packet, options, native_token, zro_token) + } else { + router_node_next::send(msglib, call_ref, packet, options, native_token, zro_token) + } + } + + public fun commit_verification( + msglib: address, + call_ref: &DynamicCallRef, + packet_header: RawPacket, + payload_hash: Bytes32, + extra_data: vector, + ): (address, u32, Bytes32, u64) { + if (msglib == @uln_302) { + uln_302::commit_verification(call_ref, packet_header, payload_hash, extra_data) + } else if (msglib == @simple_msglib) { + simple_msglib::commit_verification(call_ref, packet_header, payload_hash, extra_data) + } else if (msglib == @blocked_msglib) { + blocked_msglib::commit_verification(call_ref, packet_header, payload_hash, extra_data) + } else { + router_node_next::commit_verification(msglib, call_ref, packet_header, payload_hash, extra_data) + } + } + + public fun dvn_verify(msglib: address, call_ref: &DynamicCallRef, params: Any) { + if (msglib == @uln_302) { + uln_302::dvn_verify(call_ref, params) + } else if (msglib == @simple_msglib) { + simple_msglib::dvn_verify(call_ref, params) + } else if (msglib == @blocked_msglib) { + blocked_msglib::dvn_verify(call_ref, params) + } else { + router_node_next::dvn_verify(msglib, call_ref, params) + } + } + + public fun set_config( + msglib: address, + call_ref: &DynamicCallRef, + oapp: address, + eid: u32, + config_type: u32, + config: vector, + ) { + if (msglib == @uln_302) { + uln_302::set_config(call_ref, oapp, eid, config_type, config) + } else if (msglib == @simple_msglib) { + simple_msglib::set_config(call_ref, oapp, eid, config_type, config) + } else if (msglib == @blocked_msglib) { + blocked_msglib::set_config(call_ref, oapp, eid, config_type, config) + } else { + router_node_next::set_config(msglib, call_ref, oapp, eid, config_type, config) + } + } + + #[view] + public fun get_config(msglib: address, oapp: address, eid: u32, config_type: u32): vector { + if (msglib == @uln_302) { + uln_302::get_config(oapp, eid, config_type) + } else if (msglib == @simple_msglib) { + simple_msglib::get_config(oapp, eid, config_type) + } else if (msglib == @blocked_msglib) { + blocked_msglib::get_config(oapp, eid, config_type) + } else { + router_node_next::get_config(msglib, oapp, eid, config_type) + } + } + + #[view] + public fun version(msglib: address): (u64, u8, u8) { + if (msglib == @uln_302) { + uln_302::version() + } else if (msglib == @simple_msglib) { + simple_msglib::version() + } else if (msglib == @blocked_msglib) { + blocked_msglib::version() + } else { + router_node_next::version(msglib) + } + } + + #[view] + public fun is_supported_send_eid(msglib: address, eid: u32): bool { + if (msglib == @uln_302) { + uln_302::is_supported_send_eid(eid) + } else if (msglib == @simple_msglib) { + simple_msglib::is_supported_send_eid(eid) + } else if (msglib == @blocked_msglib) { + blocked_msglib::is_supported_send_eid(eid) + } else { + router_node_next::is_supported_send_eid(msglib, eid) + } + } + + #[view] + public fun is_supported_receive_eid(msglib: address, eid: u32): bool { + if (msglib == @uln_302) { + uln_302::is_supported_receive_eid(eid) + } else if (msglib == @simple_msglib) { + simple_msglib::is_supported_receive_eid(eid) + } else if (msglib == @blocked_msglib) { + blocked_msglib::is_supported_receive_eid(eid) + } else { + router_node_next::is_supported_receive_eid(msglib, eid) + } + } +} \ No newline at end of file diff --git a/packages/layerzero-v2/aptos/contracts/msglib/routers/router_node_1_placeholder/Move.toml b/packages/layerzero-v2/aptos/contracts/msglib/routers/router_node_1_placeholder/Move.toml new file mode 100644 index 00000000..f6186846 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/msglib/routers/router_node_1_placeholder/Move.toml @@ -0,0 +1,27 @@ +[package] +name = "router_node_1" +version = "1.0.0" +authors = [] + +[addresses] +router_node_1 = "_" +endpoint_v2_common = "_" +endpoint_v2 = "_" +layerzero_admin = "_" +layerzero_treasury_admin = "_" +msglib_types = "_" +worker_common = "_" + +[dev-addresses] +router_node_1 = "0x9007" +endpoint_v2_common = "0x9098" +endpoint_v2 = "0x12345678" +layerzero_admin = "0x18943124" +layerzero_treasury_admin = "0x1894312499" +msglib_types = "0x521234" +worker_common = "0x3204817234" + +[dependencies] +msglib_types = { local = "../../msglib_types" } +endpoint_v2_common = { local = "../../../endpoint_v2_common" } +worker_common = { local = "../../../worker_peripherals/worker_common" } \ No newline at end of file diff --git a/packages/layerzero-v2/aptos/contracts/msglib/routers/router_node_1_placeholder/sources/router_node.move b/packages/layerzero-v2/aptos/contracts/msglib/routers/router_node_1_placeholder/sources/router_node.move new file mode 100644 index 00000000..c588c120 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/msglib/routers/router_node_1_placeholder/sources/router_node.move @@ -0,0 +1,81 @@ +module router_node_1::router_node { + use std::any::Any; + use std::fungible_asset::FungibleAsset; + use std::option::Option; + + use endpoint_v2_common::bytes32::Bytes32; + use endpoint_v2_common::contract_identity::DynamicCallRef; + use endpoint_v2_common::packet_raw::RawPacket; + use endpoint_v2_common::send_packet::SendPacket; + + const ENOT_IMPLEMENTED: u64 = 1; + + public fun quote( + _msglib: address, + _packet: SendPacket, + _options: vector, + _pay_in_zro: bool, + ): (u64, u64) { + abort ENOT_IMPLEMENTED + } + + public fun send( + _msglib: address, + _call_ref: &DynamicCallRef, + _packet: SendPacket, + _options: vector, + _native_token: &mut FungibleAsset, + _zro_token: &mut Option, + ): (u64, u64, RawPacket) { + abort ENOT_IMPLEMENTED + } + + public fun commit_verification( + _msglib: address, + _call_ref: &DynamicCallRef, + _packet_header: RawPacket, + _payload_hash: Bytes32, + _extra_data: vector, + ): (address, u32, Bytes32, u64) { + abort ENOT_IMPLEMENTED + } + + public fun dvn_verify( + _msglib: address, + _call_ref: &DynamicCallRef, + _params: Any, + ) { + abort ENOT_IMPLEMENTED + } + + public fun set_config( + _msglib: address, + _call_ref: &DynamicCallRef, + _oapp: address, + _eid: u32, + _config_type: u32, + _config: vector, + ) { + abort ENOT_IMPLEMENTED + } + + #[view] + public fun get_config(_msglib: address, _oapp: address, _eid: u32, _config_type: u32): vector { + abort ENOT_IMPLEMENTED + } + + #[view] + public fun version(_msglib: address): (u64, u8, u8) { + abort ENOT_IMPLEMENTED + } + + #[view] + public fun is_supported_send_eid(_msglib: address, _eid: u32): bool { + abort ENOT_IMPLEMENTED + } + + #[view] + public fun is_supported_receive_eid(_msglib: address, _eid: u32): bool { + abort ENOT_IMPLEMENTED + } +} diff --git a/packages/layerzero-v2/aptos/contracts/oapps/oft_common/Move.toml b/packages/layerzero-v2/aptos/contracts/oapps/oft_common/Move.toml new file mode 100644 index 00000000..e144a7c1 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/oapps/oft_common/Move.toml @@ -0,0 +1,26 @@ +[package] +name = "oft_common" +version = "1.0.0" +authors = [] + +[addresses] +oft_common = "_" +endpoint_v2_common = "_" +endpoint_v2 = "_" +layerzero_admin = "_" +layerzero_treasury_admin = "_" + +[dev-addresses] +oft_common = "0x302814823" +endpoint_v2_common = "0x9098" +endpoint_v2 = "0x12345678" +layerzero_admin = "0x18943124" +layerzero_treasury_admin = "0x123456231423" + +[dependencies.AptosFramework] +git = "https://github.com/aptos-labs/aptos-framework.git" +rev = "mainnet" +subdir = "aptos-framework" + +[dependencies] +endpoint_v2_common = { local = "../../endpoint_v2_common" } diff --git a/packages/layerzero-v2/aptos/contracts/oapps/oft_common/sources/oft_compose_msg_codec.move b/packages/layerzero-v2/aptos/contracts/oapps/oft_common/sources/oft_compose_msg_codec.move new file mode 100644 index 00000000..ea4df400 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/oapps/oft_common/sources/oft_compose_msg_codec.move @@ -0,0 +1,63 @@ +/// This module provides functions to encode and decode OFT compose messages +module oft_common::oft_compose_msg_codec { + use std::vector; + + use endpoint_v2_common::bytes32::Bytes32; + use endpoint_v2_common::serde; + + const NONCE_OFFSET: u64 = 0; + const SRC_EID_OFFSET: u64 = 8; + const AMOUNT_LD_OFFSET: u64 = 12; + const COMPOSE_FROM_OFFSET: u64 = 44; + const COMPOSE_MSG_OFFSET: u64 = 76; + + /// Encode a compose message into a byte vector + /// @param nonce: The nonce of the LayerZero message that contains the compose message + /// @param src_eid: The source endpoint ID of the compose message + /// @param amount_ld: The amount in local decimals of the compose message + /// @param compose_payload: The compose message to encode [compose_payload_from][compose_payload_message] + public fun encode( + nonce: u64, + src_eid: u32, + amount_ld: u64, + compose_payload: vector, + ): vector { + let encoded = vector[]; + serde::append_u64(&mut encoded, nonce); + serde::append_u32(&mut encoded, src_eid); + serde::append_u256(&mut encoded, (amount_ld as u256)); + serde::append_bytes(&mut encoded, compose_payload); + encoded + } + + /// Get the nonce from an encoded compose message + public fun nonce(encoded: &vector): u64 { + serde::extract_u64(encoded, &mut NONCE_OFFSET) + } + + /// Get the source endpoint ID from an encoded compose message + public fun src_eid(encoded: &vector): u32 { + serde::extract_u32(encoded, &mut SRC_EID_OFFSET) + } + + /// Get the amount in local decimals from an encoded compose message + public fun amount_ld(encoded: &vector): u64 { + (serde::extract_u256(encoded, &mut AMOUNT_LD_OFFSET) as u64) + } + + /// Get the compose from address from an encoded compose message + public fun compose_payload_from(encoded: &vector): Bytes32 { + assert!(vector::length(encoded) >= COMPOSE_MSG_OFFSET, ENO_COMPOSE_MSG); + serde::extract_bytes32(encoded, &mut COMPOSE_FROM_OFFSET) + } + + /// Get the compose payload from an encoded compose message + public fun compose_payload_message(encoded: &vector): vector { + assert!(vector::length(encoded) >= COMPOSE_MSG_OFFSET, ENO_COMPOSE_MSG); + serde::extract_bytes_until_end(encoded, &mut COMPOSE_MSG_OFFSET) + } + + // ================================================== Error Codes ================================================= + + const ENO_COMPOSE_MSG: u64 = 1; +} diff --git a/packages/layerzero-v2/aptos/contracts/oapps/oft_common/sources/oft_fee_detail.move b/packages/layerzero-v2/aptos/contracts/oapps/oft_common/sources/oft_fee_detail.move new file mode 100644 index 00000000..98cfddd8 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/oapps/oft_common/sources/oft_fee_detail.move @@ -0,0 +1,31 @@ +// This module provides functions to encode and decode OFT fee detail struct +module oft_common::oft_fee_detail { + use std::string::String; + + struct OftFeeDetail has store, copy, drop { + // Amount of the fee in local decimals + fee_amount_ld: u64, + // If true, the fee is a reward; this means the fee should be taken as negative + is_reward: bool, + // Description of the fee + description: String, + } + + /// Create a new OftFeeDetail + public fun new_oft_fee_detail(fee_amount_ld: u64, is_reward: bool, description: String): OftFeeDetail { + OftFeeDetail { fee_amount_ld, is_reward, description } + } + + /// Get the amount of the fee in local decimals (if is_reward is true, the fee should be taken as negative) + public fun fee_amount_ld(fd: &OftFeeDetail): (u64, bool) { (fd.fee_amount_ld, fd.is_reward) } + + /// Get the description of the fee + public fun description(fd: &OftFeeDetail): String { fd.description } + + /// Get all the fields of the OftFeeDetail + /// @return (fee_amount_ld, is_reward, description) + public fun unpack_oft_fee_detail(fd: OftFeeDetail): (u64, bool, String) { + let OftFeeDetail { fee_amount_ld, is_reward, description } = fd; + (fee_amount_ld, is_reward, description) + } +} diff --git a/packages/layerzero-v2/aptos/contracts/oapps/oft_common/sources/oft_limit.move b/packages/layerzero-v2/aptos/contracts/oapps/oft_common/sources/oft_limit.move new file mode 100644 index 00000000..5455d070 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/oapps/oft_common/sources/oft_limit.move @@ -0,0 +1,33 @@ +/// This provides a struct that represents an OFT limit (min and max amount transferrable in local decimals) +module oft_common::oft_limit { + + const MAX_U64: u64 = 0xffffffffffffffff; + + struct OftLimit has store, copy, drop { + min_amount_ld: u64, + max_amount_ld: u64, + } + + /// Create a new OftLimit + public fun new_oft_limit(min_amount_ld: u64, max_amount_ld: u64): OftLimit { + OftLimit { min_amount_ld, max_amount_ld } + } + + /// Create a new unbounded OFT Limit + public fun new_unbounded_oft_limit(): OftLimit { + OftLimit { min_amount_ld: 0, max_amount_ld: MAX_U64 } + } + + /// Get the minimum amount in local decimals + public fun min_amount_ld(oft_limit: &OftLimit): u64 { oft_limit.min_amount_ld } + + /// Get the maximum amount in local decimals + public fun max_amount_ld(oft_limit: &OftLimit): u64 { oft_limit.max_amount_ld } + + /// Get all the fields of the OftLimit + /// @return (min_amount_ld, max_amount_ld) + public fun unpack_oft_limit(oft_limit: OftLimit): (u64, u64) { + let OftLimit { min_amount_ld, max_amount_ld } = oft_limit; + (min_amount_ld, max_amount_ld) + } +} diff --git a/packages/layerzero-v2/aptos/contracts/oapps/oft_common/sources/oft_msg_codec.move b/packages/layerzero-v2/aptos/contracts/oapps/oft_common/sources/oft_msg_codec.move new file mode 100644 index 00000000..a8f39055 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/oapps/oft_common/sources/oft_msg_codec.move @@ -0,0 +1,55 @@ +/// This module provides the encoding and decoding of OFT messages +module oft_common::oft_msg_codec { + use std::vector; + + use endpoint_v2_common::bytes32::Bytes32; + use endpoint_v2_common::serde; + + const SEND_TO_OFFSET: u64 = 0; + const SEND_AMOUNT_OFFSET: u64 = 32; + const COMPOSE_MESSAGE_OFFSET: u64 = 40; + + /// Create a new OFT Message with this codec + /// @param send_to: The address to send the message to + /// @param amount_shared: The amount in shared decimals to send + /// @param sender: The address of the sender (used as the compose_from in the compose message if present) + /// @param compose_msg: The compose message to send + /// @return The encoded OFT Message + public fun encode(send_to: Bytes32, amount_shared: u64, sender: Bytes32, compose_payload: vector): vector { + let encoded = vector[]; + serde::append_bytes32(&mut encoded, send_to); + serde::append_u64(&mut encoded, amount_shared); + if (!vector::is_empty(&compose_payload)) { + serde::append_bytes32(&mut encoded, sender); + vector::append(&mut encoded, compose_payload); + }; + encoded + } + + /// Check whether an encoded OFT Message includes a compose + public fun has_compose(message: &vector): bool { + vector::length(message) > COMPOSE_MESSAGE_OFFSET + } + + /// Check the send to address in an encoded OFT Message + public fun send_to(message: &vector): Bytes32 { + serde::extract_bytes32(message, &mut SEND_TO_OFFSET) + } + + /// Check the amount in shared decimals in an encoded OFT Message + public fun amount_sd(message: &vector): u64 { + serde::extract_u64(message, &mut SEND_AMOUNT_OFFSET) + } + + /// Check the sender in an encoded OFT Message + /// Make sure to check if the message `has_compose()` before calling this function, which will fail without a clear + /// error message if it is not present + public fun sender(message: &vector): Bytes32 { + serde::extract_bytes32(message, &mut COMPOSE_MESSAGE_OFFSET) + } + + /// Read the compose payload, including the sender, from an encoded OFT Message + public fun compose_payload(message: &vector): vector { + vector::slice(message, COMPOSE_MESSAGE_OFFSET, vector::length(message)) + } +} \ No newline at end of file diff --git a/packages/layerzero-v2/aptos/contracts/oapps/oft_common/sources/oft_v1_msg_codec.move b/packages/layerzero-v2/aptos/contracts/oapps/oft_common/sources/oft_v1_msg_codec.move new file mode 100644 index 00000000..080fed0b --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/oapps/oft_common/sources/oft_v1_msg_codec.move @@ -0,0 +1,97 @@ +/// This module provides the encoding and decoding of legacy OFT v1 (OFT on LayerZero V1 Endpoint) messages +module oft_common::oft_v1_msg_codec { + use std::vector; + + use endpoint_v2_common::bytes32::{Bytes32, from_bytes32}; + use endpoint_v2_common::serde; + use endpoint_v2_common::serde::flatten; + + const TYPE_OFFSET: u64 = 0; + const SEND_TO_OFFSET: u64 = 1; + const SEND_AMOUNT_OFFSET: u64 = 33; + const COMPOSE_MESSAGE_OFFSET_SENDER: u64 = 41; + const COMPOSE_MESSAGE_OFFSET_COMPOSE_GAS: u64 = 73; + const COMPOSE_MESSAGE_CONTENT_OFFSET: u64 = 81; + + public inline fun PT_SEND(): u8 { 0 } + + public inline fun PT_SEND_AND_CALL(): u8 { 1 } + + /// Create a new OFT (Endpoint V1) Message with this codec + /// @param message_type: The type of message to send (0 = PT_SEND, 1 = PT_SEND_AND_CALL). Please note that these + /// enums do not align with the SEND / SEND_AND_CALL consts used in the OFT + /// @param send_to: The address to send the message to + /// @param amount_shared: The amount in shared decimals to send + /// @param sender: The address of the sender (used as the compose_from in the compose message if present) + /// @param compose_msg: The compose message to send + /// @return The encoded OFT Message + public fun encode( + message_type: u8, + send_to: Bytes32, + amount_shared: u64, + sender: Bytes32, + compose_gas: u64, + compose_payload: vector, + ): vector { + assert!(message_type == PT_SEND() || message_type == PT_SEND_AND_CALL(), EUNKNOWN_MESSAGE_TYPE); + let encoded = vector[]; + serde::append_u8(&mut encoded, message_type); + serde::append_bytes32(&mut encoded, send_to); + serde::append_u64(&mut encoded, amount_shared); + if (message_type == PT_SEND_AND_CALL()) { + serde::append_bytes32(&mut encoded, sender); + serde::append_u64(&mut encoded, compose_gas); + vector::append(&mut encoded, compose_payload); + }; + encoded + } + + /// Check the message type in an encoded OFT Message + public fun message_type(message: &vector): u8 { + serde::extract_u8(message, &mut TYPE_OFFSET) + } + + /// Check whether an encoded OFT Message includes a compose + public fun has_compose(message: &vector): bool { + vector::length(message) > COMPOSE_MESSAGE_OFFSET_SENDER + } + + /// Check the send to address in an encoded OFT Message + public fun send_to(message: &vector): Bytes32 { + serde::extract_bytes32(message, &mut SEND_TO_OFFSET) + } + + /// Check the amount in shared decimals in an encoded OFT Message + public fun amount_sd(message: &vector): u64 { + serde::extract_u64(message, &mut SEND_AMOUNT_OFFSET) + } + + /// Check the sender in an encoded OFT Message + /// This function should only be called after verifying that the message has a compose message + public fun sender(message: &vector): Bytes32 { + serde::extract_bytes32(message, &mut COMPOSE_MESSAGE_OFFSET_SENDER) + } + + /// Check the compose gas in an encoded OFT Message + /// This function should only be called after verifying that the message has a compose message + public fun compose_gas(message: &vector): u64 { + serde::extract_u64(message, &mut COMPOSE_MESSAGE_OFFSET_COMPOSE_GAS) + } + + public fun compose_message_content(message: &vector): vector { + serde::extract_bytes_until_end(message, &mut COMPOSE_MESSAGE_CONTENT_OFFSET) + } + + /// Return the compose "payload", including the sender, from an encoded OFT Message + /// This will return an empty string if the message does not have a compose message + public fun v2_compatible_compose_payload(message: &vector): vector { + flatten(vector[ + from_bytes32(sender(message)), + compose_message_content(message), + ]) + } + + // ================================================== Error Codes ================================================= + + const EUNKNOWN_MESSAGE_TYPE: u64 = 1; +} \ No newline at end of file diff --git a/packages/layerzero-v2/aptos/contracts/oapps/oft_common/tests/oft_compose_codec_tests.move b/packages/layerzero-v2/aptos/contracts/oapps/oft_common/tests/oft_compose_codec_tests.move new file mode 100644 index 00000000..58aa3007 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/oapps/oft_common/tests/oft_compose_codec_tests.move @@ -0,0 +1,61 @@ +#[test_only] +module oft_common::oft_compose_codec_tests { + use endpoint_v2_common::bytes32; + use endpoint_v2_common::serde::flatten; + use oft_common::oft_compose_msg_codec; + + #[test] + fun test_encode_should_encode_and_decode_with_compose() { + let nonce = 123; + let src_eid = 456; + let amount_ld = 1000; + let compose_msg = flatten(vector[ + x"1234567890123456789012345678901234567890123456789012345678901234", + x"9999888855", + ]); + + let encoded = oft_compose_msg_codec::encode( + nonce, + src_eid, + amount_ld, + compose_msg, + ); + + assert!(oft_compose_msg_codec::nonce(&encoded) == nonce, 1); + assert!(oft_compose_msg_codec::src_eid(&encoded) == src_eid, 2); + assert!(oft_compose_msg_codec::amount_ld(&encoded) == amount_ld, 3); + assert!( + oft_compose_msg_codec::compose_payload_from(&encoded) == bytes32::to_bytes32( + x"1234567890123456789012345678901234567890123456789012345678901234" + ), + 5, + ); + assert!(oft_compose_msg_codec::compose_payload_message(&encoded) == x"9999888855", 6); + } + + #[test] + #[expected_failure(abort_code = oft_common::oft_compose_msg_codec::ENO_COMPOSE_MSG)] + fun test_compose_from_should_fail_when_no_compose() { + let encoded = oft_compose_msg_codec::encode( + 123, + 456, + 1000, + vector[], + ); + + oft_compose_msg_codec::compose_payload_from(&encoded); + } + + #[test] + #[expected_failure(abort_code = oft_common::oft_compose_msg_codec::ENO_COMPOSE_MSG)] + fun test_compose_msg_from_oft_compose_msg_should_fail_when_no_compose() { + let encoded = oft_compose_msg_codec::encode( + 123, + 456, + 1000, + vector[], + ); + + oft_compose_msg_codec::compose_payload_message(&encoded); + } +} diff --git a/packages/layerzero-v2/aptos/contracts/oapps/oft_common/tests/oft_msg_codec_tests.move b/packages/layerzero-v2/aptos/contracts/oapps/oft_common/tests/oft_msg_codec_tests.move new file mode 100644 index 00000000..5b9fabf0 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/oapps/oft_common/tests/oft_msg_codec_tests.move @@ -0,0 +1,40 @@ +#[test_only] +module oft_common::oft_msg_codec_tests { + use endpoint_v2_common::bytes32; + use oft_common::oft_msg_codec; + + #[test] + fun test_encode_should_encode_and_decode_without_compose() { + let send_to = bytes32::to_bytes32(b"12345678901234567890123456789012"); + let amount = 1000; + + let encoded = oft_msg_codec::encode( + send_to, + amount, + bytes32::from_address(@0x1234567890123456789012345678901234567890123456789012345678901234), + // empty compose message signifies no compose message + vector[], + ); + + assert!(!oft_msg_codec::has_compose(&encoded), 0); + assert!(oft_msg_codec::send_to(&encoded) == send_to, 1); + assert!(oft_msg_codec::amount_sd(&encoded) == amount, 2); + } + + #[test] + fun test_encode_should_encode_and_decode_with_compose() { + let sender = @0x1234567890123456789012345678901234567890123456789012345678901234; + let send_to = bytes32::to_bytes32(b"12345678901234567890123456789012"); + let amount = 1000; + let compose_msg = x"9999888855"; + + let encoded = oft_msg_codec::encode(send_to, amount, bytes32::from_address(sender), compose_msg); + + assert!(oft_msg_codec::has_compose(&encoded), 0); + assert!(oft_msg_codec::send_to(&encoded) == send_to, 1); + assert!(oft_msg_codec::amount_sd(&encoded) == amount, 2); + + let compose_packet = x"12345678901234567890123456789012345678901234567890123456789012349999888855"; + assert!(oft_msg_codec::compose_payload(&encoded) == compose_packet, 3); + } +} diff --git a/packages/layerzero-v2/aptos/contracts/oapps/oft_common/tests/oft_v1_msg_codec_tests.move b/packages/layerzero-v2/aptos/contracts/oapps/oft_common/tests/oft_v1_msg_codec_tests.move new file mode 100644 index 00000000..ad2e440f --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/oapps/oft_common/tests/oft_v1_msg_codec_tests.move @@ -0,0 +1,52 @@ +#[test_only] +module oft_common::oft_1_msg_codec_tests { + use endpoint_v2_common::bytes32; + use oft_common::oft_v1_msg_codec; + use oft_common::oft_v1_msg_codec::{PT_SEND, PT_SEND_AND_CALL}; + + #[test] + fun test_encode_should_encode_and_decode_without_compose() { + let send_to = bytes32::to_bytes32(b"12345678901234567890123456789012"); + let amount = 1000; + + let encoded = oft_v1_msg_codec::encode( + PT_SEND(), + send_to, + amount, + bytes32::from_address(@0x1234567890123456789012345678901234567890123456789012345678901234), + 0, + vector[], + ); + + assert!(oft_v1_msg_codec::message_type(&encoded) == PT_SEND(), 1); + assert!(!oft_v1_msg_codec::has_compose(&encoded), 0); + assert!(oft_v1_msg_codec::send_to(&encoded) == send_to, 1); + assert!(oft_v1_msg_codec::amount_sd(&encoded) == amount, 2); + } + + #[test] + fun test_encode_should_encode_and_decode_with_compose() { + let sender = @0x1234567890123456789012345678901234567890123456789012345678901234; + let send_to = bytes32::to_bytes32(b"12345678901234567890123456789012"); + let amount = 1000; + let compose_msg = x"9999888855"; + + let encoded = oft_v1_msg_codec::encode( + PT_SEND_AND_CALL(), + send_to, + amount, + bytes32::from_address(sender), + 0x101, + compose_msg, + ); + + assert!(oft_v1_msg_codec::message_type(&encoded) == PT_SEND_AND_CALL(), 1); + assert!(oft_v1_msg_codec::has_compose(&encoded), 0); + assert!(oft_v1_msg_codec::send_to(&encoded) == send_to, 1); + assert!(oft_v1_msg_codec::amount_sd(&encoded) == amount, 2); + assert!(oft_v1_msg_codec::compose_gas(&encoded) == 0x101, 3); + + let v2_style_compose_packet = x"12345678901234567890123456789012345678901234567890123456789012349999888855"; + assert!(oft_v1_msg_codec::v2_compatible_compose_payload(&encoded) == v2_style_compose_packet, 3); + } +} diff --git a/packages/layerzero-v2/aptos/contracts/run_tests.sh b/packages/layerzero-v2/aptos/contracts/run_tests.sh new file mode 100755 index 00000000..a5a4a41f --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/run_tests.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +# Find all directories containing Move.toml and run the command +find . -name 'Move.toml' -execdir sh -c 'echo "Running tests in $(pwd)" && aptos move test --dev' \; diff --git a/packages/layerzero-v2/aptos/contracts/treasury/Move.toml b/packages/layerzero-v2/aptos/contracts/treasury/Move.toml new file mode 100644 index 00000000..8085acc0 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/treasury/Move.toml @@ -0,0 +1,28 @@ +[package] +name = "treasury" +version = "1.0.0" +authors = [] + +[addresses] +treasury = "_" +endpoint_v2_common = "_" +endpoint_v2 = "_" +layerzero_admin = "_" +layerzero_treasury_admin = "_" +native_token_metadata_address = "0xa" +# For Initia: "0x8e4733bdabcf7d4afc3d14f0dd46c9bf52fb0fce9e4b996c939e195b8bc891d9" + +[dev-addresses] +treasury = "0x123432432" +endpoint_v2_common = "0x9098" +endpoint_v2 = "0x12345678" +layerzero_admin = "0x18943124" +layerzero_treasury_admin = "0x1894312499" + +[dependencies.AptosFramework] +git = "https://github.com/aptos-labs/aptos-framework.git" +rev = "mainnet" +subdir = "aptos-framework" + +[dependencies] +endpoint_v2_common = { local = "../endpoint_v2_common" } diff --git a/packages/layerzero-v2/aptos/contracts/treasury/sources/treasury.move b/packages/layerzero-v2/aptos/contracts/treasury/sources/treasury.move new file mode 100644 index 00000000..10ddc749 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/treasury/sources/treasury.move @@ -0,0 +1,192 @@ +module treasury::treasury { + use std::account; + use std::event::emit; + use std::fungible_asset::{Self, FungibleAsset}; + use std::object::object_address; + use std::primary_fungible_store; + use std::signer::address_of; + + use endpoint_v2_common::universal_config; + + #[test_only] + friend treasury::treasury_tests; + + const TREASURY_ADMIN: address = @layerzero_treasury_admin; + + // Treasury Fee cannot be set above 100% + const MAX_BPS: u64 = 10000; + + struct TreasuryConfig has key { + // The native treasury fee (in basis points of worker fee subtotal) + native_fee_bps: u64, + // The ZRO treasury fee (a fixed amount per message) + zro_fee: u64, + // Whether the treasury fee can be paid in ZRO + zro_enabled: bool, + // The address to which the treasury fee should be deposited + deposit_address: address, + } + + fun init_module(account: &signer) { + move_to(account, TreasuryConfig { + native_fee_bps: 0, + zro_fee: 0, + zro_enabled: false, + deposit_address: TREASURY_ADMIN, + }); + } + + #[test_only] + public fun init_module_for_test() { + let account = &std::account::create_signer_for_test(@treasury); + init_module(account); + } + + // ================================================== Admin Only ================================================== + + inline fun assert_admin(admin: address) { + assert!(admin == TREASURY_ADMIN, EUNAUTHORIZED); + } + + /// Updates the address to which the treasury fee is sent (must be a valid account) + public entry fun update_deposit_address(account: &signer, deposit_address: address) acquires TreasuryConfig { + assert!(account::exists_at(deposit_address), EINVALID_ACCOUNT_ADDRESS); + assert_admin(address_of(move account)); + config_mut().deposit_address = deposit_address; + emit(DepositAddressUpdated { new_deposit_address: deposit_address }); + } + + /// Enables receipt of ZRO + public entry fun set_zro_enabled(account: &signer, enabled: bool) acquires TreasuryConfig { + assert_admin(address_of(move account)); + config_mut().zro_enabled = enabled; + emit(ZroEnabledSet { enabled }); + } + + /// Sets the treasury fee in basis points of the worker fee subtotal + public entry fun set_native_bp(account: &signer, native_bps: u64) acquires TreasuryConfig { + assert_admin(address_of(move account)); + assert!(native_bps <= MAX_BPS, EINVALID_FEE); + config_mut().native_fee_bps = native_bps; + emit(NativeBpSet { native_bps }); + } + + /// Sets the treasury fee in ZRO (as a fixed amount) + public entry fun set_zro_fee(account: &signer, zro_fixed_fee: u64) acquires TreasuryConfig { + assert_admin(address_of(move account)); + config_mut().zro_fee = zro_fixed_fee; + emit(ZroFeeSet { zro_fee: zro_fixed_fee }); + } + + // =============================================== Public Functions =============================================== + + #[view] + /// Calculates the treasury fee based on the worker fee (excluding treasury) and whether the fee should be paid in + /// ZRO. If the fee should be paid in ZRO, the fee is returned in ZRO, otherwise the fee is returned is Native token + public fun get_fee(total_worker_fee: u64, pay_in_zro: bool): u64 acquires TreasuryConfig { + if (pay_in_zro) { + assert!(get_zro_enabled(), EPAY_IN_ZRO_NOT_ENABLED); + config().zro_fee + } else { + total_worker_fee * config().native_fee_bps / 10000 + } + } + + #[view] + public fun get_native_bp(): u64 acquires TreasuryConfig { config().native_fee_bps } + + #[view] + public fun get_zro_fee(): u64 acquires TreasuryConfig { config().zro_fee } + + #[view] + public fun get_zro_enabled(): bool acquires TreasuryConfig { config().zro_enabled } + + #[view] + public fun get_deposit_address(): address acquires TreasuryConfig { config().deposit_address } + + /// Pay the fee to the treasury. The fee is calculated based on the worker fee (excluding treasury), and whether + /// the FungibleAsset payment is in ZRO or Native token. The fee is extracted from the provided &mut FungibleAsset + public fun pay_fee( + total_worker_fee: u64, + payment: &mut FungibleAsset, + ): (u64) acquires TreasuryConfig { + let metadata = fungible_asset::asset_metadata(payment); + + if (object_address(&metadata) == @native_token_metadata_address) { + let fee = get_fee(total_worker_fee, false); + deposit_fungible_asset(fee, payment); + fee + } else if (config().zro_enabled && universal_config::is_zro_metadata(metadata)) { + let fee = get_fee(total_worker_fee, true); + deposit_fungible_asset(fee, payment); + fee + } else if (!config().zro_enabled) { + abort EPAY_IN_ZRO_NOT_ENABLED + } else { + abort EUNEXPECTED_TOKEN_TYPE + } + } + + /// Deposits the payment into the treasury + fun deposit_fungible_asset(charge: u64, payment: &mut FungibleAsset) acquires TreasuryConfig { + let deposit_address = config().deposit_address; + let deposit = fungible_asset::extract(payment, charge); + primary_fungible_store::deposit(deposit_address, deposit); + } + + // =============================================== Helper Functions =============================================== + + inline fun config(): &TreasuryConfig { borrow_global(@treasury) } + + inline fun config_mut(): &mut TreasuryConfig { borrow_global_mut(@treasury) } + + // ==================================================== Events ==================================================== + + #[event] + struct ZroEnabledSet has drop, store { + enabled: bool, + } + + #[event] + struct NativeBpSet has drop, store { + native_bps: u64, + } + + #[event] + struct ZroFeeSet has drop, store { + zro_fee: u64, + } + + #[event] + struct DepositAddressUpdated has drop, store { + new_deposit_address: address, + } + + #[test_only] + public fun zro_enabled_set_event(enabled: bool): ZroEnabledSet { + ZroEnabledSet { enabled } + } + + #[test_only] + public fun native_bp_set_event(native_bp: u64): NativeBpSet { + NativeBpSet { native_bps: native_bp } + } + + #[test_only] + public fun zro_fee_set_event(zro_fee: u64): ZroFeeSet { + ZroFeeSet { zro_fee } + } + + #[test_only] + public fun deposit_address_updated_event(new_deposit_address: address): DepositAddressUpdated { + DepositAddressUpdated { new_deposit_address } + } + + // ================================================== Error Codes ================================================= + + const EUNEXPECTED_TOKEN_TYPE: u64 = 1; + const EINVALID_ACCOUNT_ADDRESS: u64 = 2; + const EINVALID_FEE: u64 = 3; + const EPAY_IN_ZRO_NOT_ENABLED: u64 = 4; + const EUNAUTHORIZED: u64 = 5; +} diff --git a/packages/layerzero-v2/aptos/contracts/treasury/tests/treasury_tests.move b/packages/layerzero-v2/aptos/contracts/treasury/tests/treasury_tests.move new file mode 100644 index 00000000..c41ba84b --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/treasury/tests/treasury_tests.move @@ -0,0 +1,152 @@ +#[test_only] +module treasury::treasury_tests { + use std::account::{Self, create_signer_for_test}; + use std::event::was_event_emitted; + use std::fungible_asset; + use std::primary_fungible_store; + + use endpoint_v2_common::native_token_test_helpers::{burn_token_for_test, mint_native_token_for_test}; + use endpoint_v2_common::universal_config; + use endpoint_v2_common::zro_test_helpers::create_fa; + use treasury::treasury::{ + deposit_address_updated_event, get_native_bp, get_zro_fee, init_module_for_test, native_bp_set_event, pay_fee, + set_native_bp, set_zro_enabled, set_zro_fee, update_deposit_address, zro_enabled_set_event, zro_fee_set_event, + }; + + #[test] + fun test_fee_payment_using_native() { + let lz = &create_signer_for_test(@layerzero_treasury_admin); + init_module_for_test(); + let (zro_addr, _, _) = create_fa(b"ZRO"); + set_native_bp(lz, 100); + + universal_config::init_module_for_test(100); + let lz_admin = &create_signer_for_test(@layerzero_admin); + universal_config::set_zro_address(lz_admin, zro_addr); + set_zro_enabled(lz, true); + + let new_deposit_address = @0x1234; + account::create_account_for_test(new_deposit_address); + update_deposit_address(lz, new_deposit_address); + assert!(was_event_emitted(&deposit_address_updated_event(new_deposit_address)), 0); + + let payment_native = mint_native_token_for_test(2222); + pay_fee(2000, &mut payment_native); + // a 2000 worker fee * 100 BP = 20 native treasury fee + // remaining amount = 2222 - 20 = 2202 + assert!(fungible_asset::amount(&payment_native) == 2202, 0); + + let metadata = fungible_asset::metadata_from_asset(&payment_native); + let treasury_balance = primary_fungible_store::balance(new_deposit_address, metadata); + assert!(treasury_balance == 20, 1); + + // test cleanup + burn_token_for_test(payment_native); + } + + #[test] + #[expected_failure(abort_code = treasury::treasury::EPAY_IN_ZRO_NOT_ENABLED)] + fun test_fee_payment_using_zro_fails_when_zro_disabled() { + init_module_for_test(); + let (_, metadata, _) = create_fa(b"ZRO"); + let payment_zro = fungible_asset::zero(metadata); + pay_fee(1000, &mut payment_zro); + fungible_asset::destroy_zero(payment_zro); + } + + #[test] + fun test_pay_fee_works_in_zro() { + let lz = &create_signer_for_test(@layerzero_treasury_admin); + init_module_for_test(); + set_zro_fee(lz, 300); + let (zro_addr, metadata, mint_ref) = create_fa(b"ZRO"); + + universal_config::init_module_for_test(100); + let lz_admin = &create_signer_for_test(@layerzero_admin); + universal_config::set_zro_address(lz_admin, zro_addr); + set_zro_enabled(lz, true); + + let payment_zro = fungible_asset::mint(&mint_ref, 3000); + + pay_fee(3000, &mut payment_zro); + // 20 spent + assert!(fungible_asset::amount(&payment_zro) == 2700, 0); + let treasury_balance = primary_fungible_store::balance(@layerzero_treasury_admin, metadata); + assert!(treasury_balance == 300, 1); + + // cleanup + burn_token_for_test(payment_zro) + } + + #[test] + #[expected_failure(abort_code = treasury::treasury::EUNAUTHORIZED)] + fun test_update_deposit_address_fails_for_non_admin() { + init_module_for_test(); + let new_deposit_address = @0x1234; + account::create_account_for_test(new_deposit_address); + let non_admin = &create_signer_for_test(@0x1234); + update_deposit_address(non_admin, new_deposit_address); + } + + #[test] + #[expected_failure(abort_code = treasury::treasury::EINVALID_ACCOUNT_ADDRESS)] + fun test_update_deposit_address_fails_for_invalid_address() { + let lz = &create_signer_for_test(@layerzero_treasury_admin); + init_module_for_test(); + let new_deposit_address = @0x1234; + account::create_account_for_test(new_deposit_address); + update_deposit_address(lz, @0x0); + } + + #[test] + fun test_set_zro_enabled() { + let lz = &create_signer_for_test(@layerzero_treasury_admin); + init_module_for_test(); + let (zro_addr, _, _) = create_fa(b"ZRO"); + + universal_config::init_module_for_test(100); + let lz_admin = &create_signer_for_test(@layerzero_admin); + universal_config::set_zro_address(lz_admin, zro_addr); + + set_zro_enabled(lz, true); + assert!(was_event_emitted(&zro_enabled_set_event(true)), 0); + + set_zro_enabled(lz, false); + assert!(was_event_emitted(&zro_enabled_set_event(true)), 0); + } + + #[test] + fun test_set_zro_fee() { + let lz = &create_signer_for_test(@layerzero_treasury_admin); + init_module_for_test(); + let (zro_addr, _, _) = create_fa(b"ZRO"); + + universal_config::init_module_for_test(100); + let lz_admin = &create_signer_for_test(@layerzero_admin); + universal_config::set_zro_address(lz_admin, zro_addr); + set_zro_enabled(lz, true); + + set_zro_fee(lz, 120); + assert!(was_event_emitted(&zro_fee_set_event(120)), 0); + assert!(get_zro_fee() == 120, 1); + } + + #[test] + fun test_set_native_bp() { + let lz = &create_signer_for_test(@layerzero_treasury_admin); + init_module_for_test(); + set_native_bp(lz, 124); + + let bp = get_native_bp(); + assert!(bp == 124, 0); + assert!(was_event_emitted(&native_bp_set_event(124)), 1) + } + + #[test] + #[expected_failure(abort_code = treasury::treasury::EINVALID_FEE)] + fun test_set_native_bp_above_10000() { + let lz = &create_signer_for_test(@layerzero_treasury_admin); + init_module_for_test(); + set_native_bp(lz, 10001); + } +} \ No newline at end of file diff --git a/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_lib_routers/dvn_fee_lib_router_0/Move.toml b/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_lib_routers/dvn_fee_lib_router_0/Move.toml new file mode 100644 index 00000000..d4545010 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_lib_routers/dvn_fee_lib_router_0/Move.toml @@ -0,0 +1,38 @@ +[package] +name = "dvn_fee_lib_router_0" +version = "1.0.0" + +[addresses] +dvn_fee_lib_router_0 = "_" +dvn_fee_lib_router_1 = "_" +price_feed_router_0 = "_" +price_feed_router_1 = "_" +price_feed_module_0 = "_" +endpoint_v2_common = "_" +endpoint_v2 = "_" +layerzero_admin = "_" +layerzero_treasury_admin = "_" +worker_common = "_" +dvn_fee_lib_0 = "_" +msglib_types = "_" + +[dev-addresses] +dvn_fee_lib_router_0 = "0x30001" +dvn_fee_lib_router_1 = "0x30002" +price_feed_router_0 = "0x65DD71A" +price_feed_router_1 = "0x65DD71AB" +price_feed_module_0 = "0x65DD71" +endpoint_v2_common = "0x9098" +endpoint_v2 = "0x12345678" +layerzero_admin = "0x18943124" +layerzero_treasury_admin = "0x1894312499" +worker_common = "0x3999" +dvn_fee_lib_0 = "0x32123523a" +msglib_types = "0x13242342" + +[dependencies] +endpoint_v2_common = { local = "../../../endpoint_v2_common" } +worker_common = { local = "../../worker_common" } +dvn_fee_lib_0 = { local = "../../fee_libs/dvn_fee_lib_0" } +price_feed_module_0 = { local = "../../price_feed_modules/price_feed_module_0" } +dvn_fee_lib_router_1 = { local = "../dvn_fee_lib_router_1_placeholder" } \ No newline at end of file diff --git a/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_lib_routers/dvn_fee_lib_router_0/sources/dvn_fee_lib_router.move b/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_lib_routers/dvn_fee_lib_router_0/sources/dvn_fee_lib_router.move new file mode 100644 index 00000000..d244cd23 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_lib_routers/dvn_fee_lib_router_0/sources/dvn_fee_lib_router.move @@ -0,0 +1,40 @@ +module dvn_fee_lib_router_0::dvn_fee_lib_router { + use dvn_fee_lib_router_1::dvn_fee_lib_router as dvn_fee_lib_router_next; + + public fun get_dvn_fee( + msglib: address, + dvn_fee_lib: address, + worker: address, + dst_eid: u32, + sender: address, + packet_header: vector, + payload_hash: vector, + confirmations: u64, + options: vector, + ): (u64, address) { + if (dvn_fee_lib == @dvn_fee_lib_0) { + dvn_fee_lib_0::dvn_fee_lib::get_dvn_fee( + msglib, + worker, + dst_eid, + sender, + packet_header, + payload_hash, + confirmations, + options, + ) + } else { + dvn_fee_lib_router_next::get_dvn_fee( + msglib, + dvn_fee_lib, + worker, + dst_eid, + sender, + packet_header, + payload_hash, + confirmations, + options, + ) + } + } +} diff --git a/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_lib_routers/dvn_fee_lib_router_1_placeholder/Move.toml b/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_lib_routers/dvn_fee_lib_router_1_placeholder/Move.toml new file mode 100644 index 00000000..48374ddc --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_lib_routers/dvn_fee_lib_router_1_placeholder/Move.toml @@ -0,0 +1,16 @@ +[package] +name = "dvn_fee_lib_router_1" +version = "1.0.0" + +[addresses] +dvn_fee_lib_router_1 = "_" + +[dev-addresses] +dvn_fee_lib_router_1 = "0x3294873" + +[dependencies.AptosFramework] +git = "https://github.com/aptos-labs/aptos-framework.git" +rev = "mainnet" +subdir = "aptos-framework" + +[dev-dependencies] diff --git a/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_lib_routers/dvn_fee_lib_router_1_placeholder/sources/dvn_fee_lib_router.move b/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_lib_routers/dvn_fee_lib_router_1_placeholder/sources/dvn_fee_lib_router.move new file mode 100644 index 00000000..ee94c6da --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_lib_routers/dvn_fee_lib_router_1_placeholder/sources/dvn_fee_lib_router.move @@ -0,0 +1,17 @@ +module dvn_fee_lib_router_1::dvn_fee_lib_router { + const ENOT_IMPLEMENTED: u64 = 1; + + public fun get_dvn_fee( + _msglib: address, + _fee_lib: address, + _worker: address, + _dst_eid: u32, + _sender: address, + _packet_header: vector, + _payload_hash: vector, + _confirmations: u64, + _options: vector, + ): (u64, address) { + abort ENOT_IMPLEMENTED + } +} \ No newline at end of file diff --git a/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_lib_routers/executor_fee_lib_router_0/Move.toml b/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_lib_routers/executor_fee_lib_router_0/Move.toml new file mode 100644 index 00000000..dacb5664 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_lib_routers/executor_fee_lib_router_0/Move.toml @@ -0,0 +1,38 @@ +[package] +name = "executor_fee_lib_router_0" +version = "1.0.0" + +[addresses] +executor_fee_lib_router_0 = "_" +executor_fee_lib_router_1 = "_" +price_feed_router_0 = "_" +price_feed_router_1 = "_" +price_feed_module_0 = "_" +endpoint_v2_common = "_" +endpoint_v2 = "_" +layerzero_admin = "_" +layerzero_treasury_admin = "_" +worker_common = "_" +executor_fee_lib_0 = "_" +msglib_types = "_" + +[dev-addresses] +executor_fee_lib_router_0 = "0x30001" +executor_fee_lib_router_1 = "0x30001a" +price_feed_router_0 = "0x65DD71A" +price_feed_router_1 = "0x65DD71AB" +price_feed_module_0 = "0x65DD71" +endpoint_v2_common = "0x9098" +endpoint_v2 = "0x12345678" +layerzero_admin = "0x18943124" +layerzero_treasury_admin = "0x1894312499" +worker_common = "0x3999" +executor_fee_lib_0 = "0x32123523" +msglib_types = "0x13242342" + +[dependencies] +endpoint_v2_common = { local = "../../../endpoint_v2_common" } +worker_common = { local = "../../worker_common" } +executor_fee_lib_router_1 = { local = "../../fee_lib_routers/executor_fee_lib_router_1_placeholder" } +executor_fee_lib_0 = { local = "../../fee_libs/executor_fee_lib_0" } +price_feed_module_0 = { local = "../../price_feed_modules/price_feed_module_0" } \ No newline at end of file diff --git a/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_lib_routers/executor_fee_lib_router_0/sources/executor_fee_lib_router.move b/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_lib_routers/executor_fee_lib_router_0/sources/executor_fee_lib_router.move new file mode 100644 index 00000000..d65d6d28 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_lib_routers/executor_fee_lib_router_0/sources/executor_fee_lib_router.move @@ -0,0 +1,34 @@ +module executor_fee_lib_router_0::executor_fee_lib_router { + use executor_fee_lib_router_1::executor_fee_lib_router as executor_fee_lib_router_next; + + public fun get_executor_fee( + msglib: address, + executor_fee_lib: address, + worker: address, + dst_eid: u32, + sender: address, + message_size: u64, + options: vector, + ): (u64, address) { + if (executor_fee_lib == @executor_fee_lib_0) { + executor_fee_lib_0::executor_fee_lib::get_executor_fee( + msglib, + worker, + dst_eid, + sender, + message_size, + options, + ) + } else { + executor_fee_lib_router_next::get_executor_fee( + msglib, + executor_fee_lib, + worker, + dst_eid, + sender, + message_size, + options, + ) + } + } +} diff --git a/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_lib_routers/executor_fee_lib_router_1_placeholder/Move.toml b/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_lib_routers/executor_fee_lib_router_1_placeholder/Move.toml new file mode 100644 index 00000000..3115e79a --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_lib_routers/executor_fee_lib_router_1_placeholder/Move.toml @@ -0,0 +1,16 @@ +[package] +name = "executor_fee_lib_router_1" +version = "1.0.0" + +[addresses] +executor_fee_lib_router_1 = "_" + +[dev-addresses] +executor_fee_lib_router_1 = "0x93270987" + +[dependencies.AptosFramework] +git = "https://github.com/aptos-labs/aptos-framework.git" +rev = "mainnet" +subdir = "aptos-framework" + +[dev-dependencies] diff --git a/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_lib_routers/executor_fee_lib_router_1_placeholder/sources/executor_fee_lib_router.move b/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_lib_routers/executor_fee_lib_router_1_placeholder/sources/executor_fee_lib_router.move new file mode 100644 index 00000000..f072dc4f --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_lib_routers/executor_fee_lib_router_1_placeholder/sources/executor_fee_lib_router.move @@ -0,0 +1,15 @@ +module executor_fee_lib_router_1::executor_fee_lib_router { + const ENOT_IMPLEMENTED: u64 = 1; + + public fun get_executor_fee( + _msglib: address, + _fee_lib: address, + _worker: address, + _dst_eid: u32, + _sender: address, + _message_size: u64, + _options: vector, + ): (u64, address) { + abort ENOT_IMPLEMENTED + } +} diff --git a/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_libs/dvn_fee_lib_0/Move.toml b/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_libs/dvn_fee_lib_0/Move.toml new file mode 100644 index 00000000..d0bc3bf9 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_libs/dvn_fee_lib_0/Move.toml @@ -0,0 +1,33 @@ +[package] +name = "dvn_fee_lib_0" +version = "1.0.0" + +[addresses] +dvn_fee_lib_0 = "_" +endpoint_v2_common = "_" +endpoint_v2 = "_" +layerzero_admin = "_" +layerzero_treasury_admin = "_" +price_feed_router_0 = "_" +price_feed_router_1 = "_" +price_feed_module_0 = "_" +worker_common = "_" +msglib_types = "_" + +[dev-addresses] +dvn_fee_lib_0 = "0x3000a" +endpoint_v2_common = "0x9098" +endpoint_v2 = "0x12345678" +layerzero_admin = "0x18943124" +layerzero_treasury_admin = "0x1894312499" +worker_common = "0x3999" +msglib_types = "0x13242342" +price_feed_router_0 = "0x65DD71A" +price_feed_router_1 = "0x65DD71AB" +price_feed_module_0 = "0x65DD71" + +[dependencies] +endpoint_v2_common = { local = "../../../endpoint_v2_common" } +worker_common = { local = "../../worker_common" } +price_feed_router_0 = { local = "../../price_feed_routers/price_feed_router_0" } +msglib_types = { local = "../../../msglib/msglib_types" } \ No newline at end of file diff --git a/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_libs/dvn_fee_lib_0/sources/dvn_fee_lib.move b/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_libs/dvn_fee_lib_0/sources/dvn_fee_lib.move new file mode 100644 index 00000000..f581488a --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_libs/dvn_fee_lib_0/sources/dvn_fee_lib.move @@ -0,0 +1,122 @@ +module dvn_fee_lib_0::dvn_fee_lib { + use msglib_types::worker_options::DVN_WORKER_ID; + use price_feed_router_0::router as price_feed_router; + use worker_common::multisig; + use worker_common::worker_config; + + #[test_only] + friend dvn_fee_lib_0::dvn_fee_lib_tests; + + const EXECUTE_FIXED_BYTES: u64 = 68; + const SIGNATURE_RAW_BYTES: u64 = 65; + const VERIFY_BYTES: u64 = 320; + + #[view] + // Get the total fee, including premiums for a DVN worker to verify a message + public fun get_dvn_fee( + msglib: address, + worker: address, + dst_eid: u32, + sender: address, + _packet_header: vector, + _payload_hash: vector, + _confirmations: u64, + _options: vector, + ): (u64, address) { + worker_config::assert_fee_lib_supports_transaction(worker, DVN_WORKER_ID(), sender, msglib); + let calldata_size = get_calldata_size_for_fee(worker); + + let fee = get_dvn_fee_internal( + worker, + dst_eid, + // Price Feed Estimate Fee on Send - partially applying parameters available in scope + |price_feed, feed_address, total_gas| price_feed_router::estimate_fee_on_send( + price_feed, + feed_address, + dst_eid, + calldata_size, + total_gas, + ) + ); + + let deposit_address = worker_config::get_deposit_address(worker); + (fee, deposit_address) + } + + /// Get the total fee, including premiums for a DVN packet, while ensuring that this feelib is supported by the + /// worker, the sender is allowed, and the worker is unpaused + /// + /// @param worker_address: The address of the worker + /// @param dst_eid: The destination EID + /// @param estimate_fee_on_send: fee estimator (via price feed, with partially applied parameters) + /// |price_feed, feed_address, total_gas| (fee, price ratio, denominator, native token price in USD) + public(friend) inline fun get_dvn_fee_internal( + worker_address: address, + dst_eid: u32, + estimate_fee_on_send: |address, address, u128| (u128, u128, u128, u128), + ): u64 { + let (gas, multiplier_bps, floor_margin_usd) = worker_config::get_dvn_dst_config_values(worker_address, dst_eid); + assert!(gas != 0, err_EDVN_EID_NOT_SUPPORTED()); + + let (price_feed_module, feed_address) = worker_config::get_effective_price_feed(worker_address); + let (chain_fee, _, _, native_price_usd) = estimate_fee_on_send( + price_feed_module, + feed_address, + (gas as u128) + ); + + let default_multiplier_bps = worker_config::get_default_multiplier_bps(worker_address); + let native_decimals_rate = worker_config::get_native_decimals_rate(); + + (apply_premium( + chain_fee, + native_price_usd, + multiplier_bps, + floor_margin_usd, + default_multiplier_bps, + native_decimals_rate, + ) as u64) + } + + /// Apply the premium to the fee. It takes the higher of using the multiplier or the floor margin + public(friend) fun apply_premium( + chain_fee: u128, + native_price_usd: u128, // in native_decimals_rate + multiplier_bps: u16, + floor_margin_usd: u128, + default_multiplier_bps: u16, + native_decimals_rate: u128, + ): u128 { + let multiplier_bps = if (multiplier_bps == 0) default_multiplier_bps else multiplier_bps; + // multiplier bps is 1e5 e.g. 12000 is 120% + let fee_with_multiplier = chain_fee * (multiplier_bps as u128) / 10000; + + if (native_price_usd == 0 || floor_margin_usd == 0) { + return fee_with_multiplier + }; + + let fee_with_floor_margin = chain_fee + (floor_margin_usd * native_decimals_rate) / native_price_usd; + + if (fee_with_floor_margin > fee_with_multiplier) { fee_with_floor_margin } else { fee_with_multiplier } + } + + // =================================================== Internal =================================================== + + /// Get the calldata size for a fee; this scales with the number of quorum signatures required + public(friend) fun get_calldata_size_for_fee(worker_address: address): u64 { + let quorum = multisig::get_quorum(worker_address); + + let total_signature_bytes: u64 = quorum * SIGNATURE_RAW_BYTES; + if (total_signature_bytes % 32 != 0) { + total_signature_bytes = total_signature_bytes - (total_signature_bytes % 32) + 32; + }; + // Total includes 64 byte overhead + EXECUTE_FIXED_BYTES + VERIFY_BYTES + total_signature_bytes + 64 + } + + // ================================================== Error Codes ================================================= + + const EDVN_EID_NOT_SUPPORTED: u64 = 1; + + public(friend) fun err_EDVN_EID_NOT_SUPPORTED(): u64 { EDVN_EID_NOT_SUPPORTED } +} diff --git a/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_libs/dvn_fee_lib_0/tests/dvn_fee_lib_tests.move b/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_libs/dvn_fee_lib_0/tests/dvn_fee_lib_tests.move new file mode 100644 index 00000000..f0402e87 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_libs/dvn_fee_lib_0/tests/dvn_fee_lib_tests.move @@ -0,0 +1,452 @@ +#[test_only] +module dvn_fee_lib_0::dvn_fee_lib_tests { + use std::account::{Self, create_signer_for_test}; + + use dvn_fee_lib_0::dvn_fee_lib::{apply_premium, get_calldata_size_for_fee, get_dvn_fee, get_dvn_fee_internal}; + use endpoint_v2_common::contract_identity::make_call_ref_for_test; + use endpoint_v2_common::native_token_test_helpers::initialize_native_token_for_test; + use price_feed_module_0::eid_model_pair::{ + Self, + ARBITRUM_MODEL_TYPE, + DEFAULT_MODEL_TYPE, + EidModelPair, + new_eid_model_pair, + OPTIMISM_MODEL_TYPE + }; + use price_feed_module_0::price::{Self, EidTaggedPrice, tag_price_with_eid}; + use worker_common::worker_config::{Self, WORKER_ID_DVN}; + + const APTOS_NATIVE_DECIMAL_RATE: u128 = 100_000_000; + + // Test params + const CHAIN_FEE: u128 = 1 * 100_000_000; // 1 native * 1e18 e.g. on ethereum + + #[test] + fun test_get_fee() { + // 1. Set up the price feed (@price_feed_module_0, @1111) + use price_feed_module_0::feeds; + + initialize_native_token_for_test(); + let feed = &create_signer_for_test(@1111); + let updater = &create_signer_for_test(@9999); + feeds::initialize(feed); + feeds::enable_feed_updater(feed, @9999); + + // These prices are the same as used in the individual model tests + // We are testing whether we get the expected model response + // Using different price ratios for goerli, sepolia to see that arbitrum calcs are using the correct L2 price + let eth_price = price::new_price(4000, 51, 33); + let eth_goerli_price = price::new_price(40000, 51, 33); + let eth_sepolia_price = price::new_price(400000, 51, 33); + let arb_price = price::new_price(1222, 12, 3); + let opt_price = price::new_price(200, 43, 5); + + feeds::set_denominator(feed, 100); + feeds::set_arbitrum_compression_percent(feed, 47); + feeds::set_arbitrum_traits(updater, @1111, 5432, 11); + feeds::set_native_token_price_usd(updater, @1111, 6); + + // Test some non-hardcoded model types + let eid_model_pairs = vector[ + new_eid_model_pair( + 110, + DEFAULT_MODEL_TYPE() + ), // cannot override hardcoded type - this will still be "ARBITRUM" + new_eid_model_pair(11000, OPTIMISM_MODEL_TYPE()), // optimism using L1 sepolia + new_eid_model_pair(25555, ARBITRUM_MODEL_TYPE()), + new_eid_model_pair(26666, OPTIMISM_MODEL_TYPE()), + ]; + + let pairs_serialized = eid_model_pair::serialize_eid_model_pair_list(&eid_model_pairs); + feeds::set_eid_models(feed, pairs_serialized); + + let list = vector[ + tag_price_with_eid(101, eth_price), // First 6 EIDs are all of hardcoded types + tag_price_with_eid(110, arb_price), + tag_price_with_eid(111, opt_price), + tag_price_with_eid(10101, eth_price), + tag_price_with_eid(10143, arb_price), + tag_price_with_eid(10132, opt_price), + tag_price_with_eid(11000, opt_price), // optimism using L1 sepolia + tag_price_with_eid(10121, eth_goerli_price), // eth-goerli - used for arbitrum estimate + tag_price_with_eid(10161, eth_sepolia_price), // eth-sepolia - used for arbitrum estimate + + tag_price_with_eid(24444, eth_price), // not hardcoded and not set - should default to "DEFAULT" + tag_price_with_eid(25555, arb_price), // configured to "ARBITRUM" + tag_price_with_eid(26666, opt_price), // configured to "OPTIMISM" + tag_price_with_eid(20121, eth_goerli_price), // eth-goerli - used for arbitrum estimate + ]; + let prices_serialized = price::serialize_eid_tagged_price_list(&list); + feeds::set_price(updater, @1111, prices_serialized); + + let (fee, price_ratio, denominator, native_token_price) = feeds::estimate_fee_on_send( + @1111, + 10101, + 50, + 100, + ); + assert!(fee == 3570000, 0); + assert!(price_ratio == 4000, 1); + assert!(denominator == 100, 2); + assert!(native_token_price == 6, 3); + + // 2. Set up the worker (@1234) + let worker = @1234; + initialize_native_token_for_test(); + worker_config::initialize_for_worker_test_only( + worker, + WORKER_ID_DVN(), + worker, + @0x501ead, + vector[@111], + vector[@222], + @0xfee11b, + ); + worker_config::set_dvn_dst_config( + &make_call_ref_for_test(worker), + 10101, + 1000, + 50, + 100, + ); + worker_common::multisig::initialize_for_worker_test_only(worker, 1, vector[ + x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a" + ]); + worker_config::set_price_feed(&make_call_ref_for_test(worker), @price_feed_module_0, @1111); + + let (fee, deposit) = get_dvn_fee( + @222, + worker, + 10101, + @1234, + b"unused header", + b"unused hash", + 2, + b"1123", + ); + + assert!(fee != 0, 0); + assert!(deposit == @1234, 0); + + // test with a different deposit address + account::create_account_for_test(@4321); + worker_config::set_deposit_address(&make_call_ref_for_test(worker), @4321); + + let (_fee, deposit) = get_dvn_fee( + @222, + worker, + 10101, + @1234, + b"unused header", + b"unused hash", + 2, + b"1123", + ); + assert!(deposit == @4321, 0); + } + + #[test] + fun test_get_fee_internal() { + initialize_native_token_for_test(); + // Set up the worker (@1234) + let worker = @1234; + initialize_native_token_for_test(); + worker_config::initialize_for_worker_test_only( + worker, + WORKER_ID_DVN(), + worker, + @0x501ead, + vector[@111], + vector[@222], + @0xfee11b, + ); + worker_config::set_dvn_dst_config( + &make_call_ref_for_test(worker), + 10101, + 900, + 10050, + 1, + ); + worker_common::multisig::initialize_for_worker_test_only(worker, 1, vector[ + x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a" + ]); + worker_config::set_price_feed(&make_call_ref_for_test(worker), @1111, @2222); + + let called = false; + assert!(!called, 0); + + let fee = get_dvn_fee_internal( + worker, + 10101, + |price_feed, feed_address, total_gas| { + called = true; + assert!(price_feed == @1111, 0); + assert!(feed_address == @2222, 1); + // from the dvn_dst_config + assert!(total_gas == 900, 2); + + // 20_000_000 for APTOS 8-decimals - adjust for other native tokens + let native_price_usd = 20_000_000 * worker_config::get_native_decimals_rate() / 100_000_000; + (40000, 200, 1_000_000, native_price_usd) + }, + ); + + assert!(called, 1); + + // (10050 multiplier_bps) * (40000 chain_fee) / 10000 = 40200 + // vs. + // 40000 chain_fee + (1 floor margin) * (100_000_000 native_decimals_rate) / 20_000_000 native_price_usd = 40005 + // 40200 > 40005 + assert!(fee == 40200, 0); + } + + #[test] + fun test_get_fee_with_delegate() { + // other worker + initialize_native_token_for_test(); + worker_config::initialize_for_worker_test_only( + @5555, + 1, + @5555, + @0x501ead, + vector[@111], + vector[@222], + @0xfee11b, + ); + let other_worker_call_ref = &make_call_ref_for_test(@5555); + worker_config::set_price_feed( + other_worker_call_ref, + @0xabcd, + @1234, + ); + + // Set up the worker (@1234) + let worker = @1234; + initialize_native_token_for_test(); + worker_config::initialize_for_worker_test_only( + worker, + WORKER_ID_DVN(), + worker, + @0x501ead, + vector[@111], + vector[@222], + @0xfee11b, + ); + worker_config::set_dvn_dst_config( + &make_call_ref_for_test(worker), + 10101, + 900, + 10050, + 1, + ); + worker_common::multisig::initialize_for_worker_test_only(worker, 1, vector[ + x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a" + ]); + worker_config::set_price_feed_delegate( + &make_call_ref_for_test(worker), + @5555, + ); + + let called = false; + assert!(!called, 0); + + let fee = get_dvn_fee_internal( + worker, + 10101, + |price_feed, feed_address, total_gas| { + called = true; + assert!(price_feed == @0xabcd, 0); + assert!(feed_address == @1234, 1); + // from the dvn_dst_config + assert!(total_gas == 900, 2); + + // 200_000 for APTOS 8-decimals - adjust for other native tokens + let native_price_usd = 200_000 * worker_config::get_native_decimals_rate() / 100_000_000; + (40000, 200, 100_000, native_price_usd) + }, + ); + + assert!(called, 1); + + // (10050 multiplier_bps) * (40000 chain_fee) / 10000 = 40200 + // vs. + // 40000 chain_fee + (1 floor margin) * (100_000_000 native_decimals_rate) / 200_000 native_price_usd = 40500 + // 40200 < 40500 + assert!(fee == 40500, 0); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::EWORKER_PAUSED)] + fun test_get_fee_will_fail_if_worker_paused() { + let worker = @1234; + initialize_native_token_for_test(); + worker_common::worker_config::initialize_for_worker_test_only( + worker, + WORKER_ID_DVN(), + worker, + @0x501ead, + vector[@111], + vector[@222], + @0xfee11b, + ); + worker_config::set_worker_pause(&make_call_ref_for_test(worker), true); + + get_dvn_fee( + @555, + worker, + 12, + @1001, + b"123", + x"1234567890123456789012345678901234567890123456789012345678901234", + 2, + b"1123" + ); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::ESENDER_DENIED)] + fun test_get_fee_will_fail_if_sender_not_allowed() { + let worker = @1234; + initialize_native_token_for_test(); + worker_common::worker_config::initialize_for_worker_test_only( + worker, + WORKER_ID_DVN(), + worker, + @0x501ead, + vector[@111], + vector[@222], + @0xfee11b, + ); + // create an allowlist without the sender + worker_config::set_allowlist(&make_call_ref_for_test(worker), @55555555, true); + + get_dvn_fee( + @555, + worker, + 12, + @1001, // not on allowlist + b"123", + x"1234567890123456789012345678901234567890123456789012345678901234", + 2, + b"1123" + ); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::EWORKER_AUTH_UNSUPPORTED_MSGLIB)] + fun test_get_fee_will_fail_if_msglib_not_supported() { + let worker = @1234; + initialize_native_token_for_test(); + worker_common::worker_config::initialize_for_worker_test_only( + worker, + WORKER_ID_DVN(), + worker, + @0x501ead, + vector[@111], + vector[@222], + @0xfee11b, + ); + + // not selecting msglib as supported + + get_dvn_fee( + @555, + worker, + 12, + @1991, + b"123", + x"1234567890123456789012345678901234567890123456789012345678901234", + 2, + b"1123" + ); + } + + #[test] + fun test_apply_premium_no_native_price_usd_or_floor_margin_usd_set() { + // if native_price_usd is not set or floor_margin_usd is not set, fee = fee_with_multiplier + // fee = 100_000_000 * 120% = 120_000_000 + let expected_fee = 120_000_000; + assert!(apply_premium(CHAIN_FEE, 0, 12000, 0, 10500, APTOS_NATIVE_DECIMAL_RATE) == expected_fee, 0); + assert!( + apply_premium( + CHAIN_FEE, + 500_000_000_000_000_000_000, + 12000, + 0, + 10500, + APTOS_NATIVE_DECIMAL_RATE, + ) == expected_fee, + 1, + ); + assert!( + apply_premium( + CHAIN_FEE, + 0, + 12000, + 10_000_000_000_000_000_000 /* 0.10 usd */, + 10500, + APTOS_NATIVE_DECIMAL_RATE, + ) == expected_fee, + 2, + ); + } + + #[test] + fun test_apply_premium_with_floor_margin_greater() { + // chain_fee = 100_000_000 (1 native) + // native_price_usd = 1 USD = 100_000_000_000_000_000_000 + // floor_margin_usd = 2 USD = 200_000_000_000_000_000_000 + // floor_margin_in_native = + // 100_000_000 (chain_fee) + 200_000_000_000_000_000_000 (floor_margin_usd) * 100_000_000 (native_decimals_rate) / 100_000_000_000_000_000_000 (native_price_usd) + // = 300_000_000 + let fee = apply_premium( + CHAIN_FEE, + 100_000_000_000_000_000_000, + 12000, 200_000_000_000_000_000_000 /* 2usd */, + 10500, + APTOS_NATIVE_DECIMAL_RATE, + ); + let expected_fee = 300_000_000; + assert!(fee == expected_fee, 0); + } + + #[test] + fun test_apply_premium_with_floor_margin_less() { + // chain_fee = 100_000_000 (1 native) + // native_price_usd = 1 USD = 100_000_000_000_000_000_000 + // floor_margin_usd = 0.02 USD = 2_000_000_000_000_000_000 + // floor_margin_in_native = + // 100_000_000 (chain_fee) + 100_000_000_000_000_000_000 (floor_margin_usd) * 100_000_000 (native_decimals_rate) / 100_000_000_000_000_000_000 (native_price_usd) + // = 300_000_000 + let fee = apply_premium( + CHAIN_FEE, + 100_000_000_000_000_000_000, + 12000, + 2_000_000_000_000_000_000 /* 2usd */, + 10500, + APTOS_NATIVE_DECIMAL_RATE, + ); + let expected_fee = 120_000_000; + assert!(fee == expected_fee, 0); + } + + #[test] + fun test_get_calldata_size_for_fee() { + let worker = @1234; + initialize_native_token_for_test(); + worker_common::worker_config::initialize_for_worker_test_only( + worker, + WORKER_ID_DVN(), + worker, + @0x501ead, + vector[@111], + vector[@222], + @0xfee11b, + ); + worker_common::multisig::initialize_for_worker_test_only(worker, 1, vector[ + x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a", + ]); + + get_calldata_size_for_fee(worker); + } +} diff --git a/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_libs/executor_fee_lib_0/Move.toml b/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_libs/executor_fee_lib_0/Move.toml new file mode 100644 index 00000000..b4503e57 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_libs/executor_fee_lib_0/Move.toml @@ -0,0 +1,34 @@ +[package] +name = "executor_fee_lib_0" +version = "1.0.0" + +[addresses] +executor_fee_lib_0 = "_" +endpoint_v2_common = "_" +endpoint_v2 = "_" +layerzero_admin = "_" +layerzero_treasury_admin = "_" +price_feed_router_0 = "_" +price_feed_router_1 = "_" +price_feed_module_0 = "_" + +worker_common = "_" +msglib_types = "_" + +[dev-addresses] +executor_fee_lib_0 = "0x3000a" +endpoint_v2_common = "0x9098" +endpoint_v2 = "0x12345678" +layerzero_admin = "0x18943124" +layerzero_treasury_admin = "0x1894312499" +worker_common = "0x3999" +msglib_types = "0x13242342" +price_feed_router_0 = "0x65DD71A" +price_feed_router_1 = "0x65DD71AB" +price_feed_module_0 = "0x65DD71" + +[dependencies] +endpoint_v2_common = { local = "../../../endpoint_v2_common" } +worker_common = { local = "../../worker_common" } +price_feed_router_0 = { local = "../../price_feed_routers/price_feed_router_0" } +msglib_types = { local = "../../../msglib/msglib_types" } \ No newline at end of file diff --git a/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_libs/executor_fee_lib_0/sources/executor_fee_lib.move b/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_libs/executor_fee_lib_0/sources/executor_fee_lib.move new file mode 100644 index 00000000..d5d985ee --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_libs/executor_fee_lib_0/sources/executor_fee_lib.move @@ -0,0 +1,199 @@ +module executor_fee_lib_0::executor_fee_lib { + use std::vector; + + use executor_fee_lib_0::executor_option::{Self, ExecutorOptions}; + use msglib_types::worker_options::EXECUTOR_WORKER_ID; + use price_feed_router_0::router as price_feed_router; + use worker_common::worker_config; + + #[test_only] + friend executor_fee_lib_0::executor_fee_lib_tests; + + #[view] + /// Get the total executor fee, including premiums for an Executor worker to send a message + /// This checks that the Message Library is supported by the worker, the sender is allowed, the worker is + /// unpaused, and that the worker is an executor + public fun get_executor_fee( + msglib: address, + worker: address, + dst_eid: u32, + sender: address, + message_size: u64, + options: vector, + ): (u64, address) { + worker_config::assert_fee_lib_supports_transaction(worker, EXECUTOR_WORKER_ID(), sender, msglib); + + let executor_options = executor_option::extract_executor_options(&options, &mut 0); + let fee = get_executor_fee_internal( + worker, + dst_eid, + executor_options, + // Price Feed Estimate Fee on Send - partially applying parameters available in scope + |price_feed, feed_address, total_gas| price_feed_router::estimate_fee_on_send( + price_feed, + feed_address, + dst_eid, + message_size, + total_gas, + ) + ); + let deposit_address = worker_config::get_deposit_address(worker); + (fee, deposit_address) + } + + /// Get the total executor fee, using a provided price feed fee estimation function + /// + /// @param worker: The worker address + /// @param dst_eid: The destination EID + /// @param options: The executor options + /// @param estimate_fee_on_send: fee estimator + /// |price_feed, feed_address, total_remote_gas| -> (local_chain_fee, price_ratio, denominator, native_price_usd) + /// @return The total fee + public(friend) inline fun get_executor_fee_internal( + worker: address, + dst_eid: u32, + options: ExecutorOptions, + estimate_fee_on_send: |address, address, u128| (u128, u128, u128, u128) + ): u64 { + let ( + lz_receive_base_gas, multiplier_bps, floor_margin_usd, native_cap, lz_compose_base_gas, + ) = worker_config::get_executor_dst_config_values(worker, dst_eid); + assert!(lz_receive_base_gas != 0, err_EEXECUTOR_EID_NOT_SUPPORTED()); + + let (total_dst_amount, total_gas) = calculate_executor_dst_amount_and_total_gas( + is_v1_eid(dst_eid), + lz_receive_base_gas, + lz_compose_base_gas, + native_cap, + options, + ); + let (price_feed, feed_address) = worker_config::get_effective_price_feed(worker); + let (chain_fee, price_ratio, denominator, native_price_usd) = estimate_fee_on_send( + price_feed, feed_address, total_gas, + ); + + let default_multiplier_bps = worker_config::get_default_multiplier_bps(worker); + let multiplier_bps = if (multiplier_bps == 0) default_multiplier_bps else multiplier_bps; + let native_decimals_rate = worker_common::worker_config::get_native_decimals_rate(); + let fee = apply_premium_to_gas( + chain_fee, + multiplier_bps, + floor_margin_usd, + native_price_usd, + native_decimals_rate, + ); + fee = fee + convert_and_apply_premium_to_value( + total_dst_amount, + price_ratio, + denominator, + multiplier_bps, + ); + (fee as u64) + } + + // ================================================ Internal Functions ================================================ + + /// Apply the premium to the fee, this will take the higher of the multiplier applied to the fee or the floor margin + /// added to the fee + public(friend) fun apply_premium_to_gas( + fee: u128, + multiplier_bps: u16, + margin_usd: u128, + native_price_usd: u128, + native_decimals_rate: u128, + ): u128 { + let fee_with_multiplier = (fee * (multiplier_bps as u128)) / 10000; + if (native_price_usd == 0 || margin_usd == 0) { + return fee_with_multiplier + }; + let fee_with_margin = (margin_usd * native_decimals_rate) / native_price_usd + fee; + if (fee_with_margin > fee_with_multiplier) { fee_with_margin } else { fee_with_multiplier } + } + + /// Convert the destination value to the local chain native token and apply a multiplier to the value + public(friend) fun convert_and_apply_premium_to_value( + value: u128, + ratio: u128, + denominator: u128, + multiplier_bps: u16, + ): u128 { + if (value > 0) { (((value * ratio) / denominator) * (multiplier_bps as u128)) / 10000 } else 0 + } + + /// Check whether the EID is a V1 EID + public(friend) fun is_v1_eid(eid: u32): bool { eid < 30000 } + + /// Calculate the Destination Amount and Total Gas for the Executor + /// @return (destination amount, total gas) + public(friend) fun calculate_executor_dst_amount_and_total_gas( + is_v1_eid: bool, + lz_receive_base_gas: u64, + lz_compose_base_gas: u64, + native_cap: u128, + options: ExecutorOptions, + ): (u128, u128) { + let ( + lz_receive_options, + native_drop_options, + lz_compose_options, + ordered_execution_option, + ) = executor_option::unpack_options(options); + + // The total value to to be sent to the destination + let dst_amount: u128 = 0; + // The total gas to be used for the transaction + let lz_receive_gas: u128 = 0; + + // Loop through LZ Receive options + for (i in 0..vector::length(&lz_receive_options)) { + let option = *vector::borrow(&lz_receive_options, i); + let (gas, value) = executor_option::unpack_lz_receive_option(option); + + assert!(!is_v1_eid || value == 0, EEV1_DOES_NOT_SUPPORT_LZ_RECEIVE_WITH_VALUE); + dst_amount = dst_amount + value; + lz_receive_gas = lz_receive_gas + gas; + }; + assert!(lz_receive_gas > 0, EEXECUTOR_ZERO_LZRECEIVE_GAS_PROVIDED); + let total_gas = (lz_receive_base_gas as u128) + lz_receive_gas; + + // Loop through LZ Compose options + for (i in 0..vector::length(&lz_compose_options)) { + let option = *vector::borrow(&lz_compose_options, i); + let (_index, gas, value) = executor_option::unpack_lz_compose_option(option); + // Endpoint V1 doesnot support LZ Compose + assert!(!is_v1_eid, EEV1_DOES_NOT_SUPPORT_LZ_COMPOSE_WITH_VALUE); + assert!(gas > 0, EEXECUTOR_ZERO_LZCOMPOSE_GAS_PROVIDED); + dst_amount = dst_amount + value; + // The LZ Compose base gas is required for each LZ Compose, which is represented by the count of indexes. + // However, this calculation is simplified to match the EVM calculation, which does not deduplicate based on + // the Lz Compose index. Therefore, if there are multiple LZ Compose Options for a specific index, the + // Lz Compose base gas will also be duplicated by the number of options on that index + total_gas = total_gas + gas + (lz_compose_base_gas as u128); + }; + + // Loop through Native Drop options + for (i in 0..vector::length(&native_drop_options)) { + let option = *vector::borrow(&native_drop_options, i); + let (amount, _receiver) = executor_option::unpack_native_drop_option(option); + dst_amount = dst_amount + amount; + }; + assert!(dst_amount <= native_cap, EEXECUTOR_NATIVE_AMOUNT_EXCEEDS_CAP); + + // If ordered execution is enabled, increase the gas by 2% + if (ordered_execution_option) { + total_gas = (total_gas * 102) / 100; + }; + (dst_amount, total_gas) + } + + // ================================================== Error Codes ================================================= + + const EEV1_DOES_NOT_SUPPORT_LZ_COMPOSE_WITH_VALUE: u64 = 1; + const EEV1_DOES_NOT_SUPPORT_LZ_RECEIVE_WITH_VALUE: u64 = 2; + const EEXECUTOR_EID_NOT_SUPPORTED: u64 = 3; + const EEXECUTOR_NATIVE_AMOUNT_EXCEEDS_CAP: u64 = 4; + const EEXECUTOR_ZERO_LZCOMPOSE_GAS_PROVIDED: u64 = 5; + const EEXECUTOR_ZERO_LZRECEIVE_GAS_PROVIDED: u64 = 6; + + public(friend) fun err_EEXECUTOR_EID_NOT_SUPPORTED(): u64 { EEXECUTOR_EID_NOT_SUPPORTED } +} \ No newline at end of file diff --git a/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_libs/executor_fee_lib_0/sources/types/executor_option.move b/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_libs/executor_fee_lib_0/sources/types/executor_option.move new file mode 100644 index 00000000..60b34a51 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_libs/executor_fee_lib_0/sources/types/executor_option.move @@ -0,0 +1,243 @@ +module executor_fee_lib_0::executor_option { + use std::vector; + + use endpoint_v2_common::bytes32::Bytes32; + use endpoint_v2_common::serde; + use msglib_types::worker_options::EXECUTOR_WORKER_ID; + + const OPTION_TYPE_LZ_RECEIVE: u8 = 1; + const OPTION_TYPE_NATIVE_DROP: u8 = 2; + const OPTION_TYPE_LZ_COMPOSE: u8 = 3; + const OPTION_TYPE_ORDERED_EXECUTION: u8 = 4; + + + /// ExecutorOptions is used to specify the options for an executor + struct ExecutorOptions has drop, copy, store { + // The gas and value delivered via the LZ Receive operation + lz_receive_options: vector, + // The amount and receiver for the Native Drop operation + native_drop_options: vector, + // The gas and value for each LZ Compose operation + lz_compose_options: vector, + // Whether or not the execution will require ordered execution + ordered_execution_option: bool, + } + + /// The gas and value for the LZ Receive operation + struct LzReceiveOption has drop, copy, store { + gas: u128, + value: u128, + } + + /// The amount and receiver for the Native Drop operation + struct NativeDropOption has drop, copy, store { + amount: u128, + receiver: Bytes32, + } + + /// The gas, value, and index of a specific LZ Compose operation + struct LzComposeOption has drop, copy, store { + index: u16, + gas: u128, + value: u128, + } + + /// Unpacks ExecutorOptions into its components + public fun unpack_options( + options: ExecutorOptions, + ): (vector, vector, vector, bool) { + let ExecutorOptions { + lz_receive_options, + native_drop_options, + lz_compose_options, + ordered_execution_option, + } = options; + (lz_receive_options, native_drop_options, lz_compose_options, ordered_execution_option) + } + + /// Unpacks LzReceiveOption into its components + /// @return (gas, value) + public fun unpack_lz_receive_option(option: LzReceiveOption): (u128, u128) { + let LzReceiveOption { gas, value } = option; + (gas, value) + } + + /// Unpacks NativeDropOption into its components + /// @return (amount, receiver) + public fun unpack_native_drop_option(option: NativeDropOption): (u128, Bytes32) { + let NativeDropOption { amount, receiver } = option; + (amount, receiver) + } + + /// Unpacks LzComposeOption into its components + /// @return (compose index, gas, value) + public fun unpack_lz_compose_option(option: LzComposeOption): (u16, u128, u128) { + let LzComposeOption { index, gas, value } = option; + (index, gas, value) + } + + /// Creates a new ExecutorOptions from its components + public fun new_executor_options( + lz_receive_options: vector, + native_drop_options: vector, + lz_compose_options: vector, + ordered_execution_option: bool, + ): ExecutorOptions { + ExecutorOptions { lz_receive_options, native_drop_options, lz_compose_options, ordered_execution_option } + } + + /// Creates a new LzReceiveOption from its components + public fun new_lz_receive_option(gas: u128, value: u128): LzReceiveOption { + LzReceiveOption { gas, value } + } + + /// Creates a new NativeDropOption from its components + public fun new_native_drop_option(amount: u128, receiver: Bytes32): NativeDropOption { + NativeDropOption { amount, receiver } + } + + /// Creates a new LzComposeOption from its components + public fun new_lz_compose_option(index: u16, gas: u128, value: u128): LzComposeOption { + LzComposeOption { index, gas, value } + } + + /// Extracts an ExecutorOptions from a byte buffer + public fun extract_executor_options(buf: &vector, pos: &mut u64): ExecutorOptions { + let options = ExecutorOptions { + lz_receive_options: vector[], + native_drop_options: vector[], + lz_compose_options: vector[], + ordered_execution_option: false, + }; + let len = vector::length(buf); + while (*pos < len) { + let _worker_id = serde::extract_u8(buf, pos); + // The serialized option_size includes 1 byte for the option_type. Subtracting 1 byte is the number of bytes + // that should be read after reading the option type + let option_size = serde::extract_u16(buf, pos) - 1; + let option_type = serde::extract_u8(buf, pos); + + if (option_type == OPTION_TYPE_LZ_RECEIVE) { + // LZ Receive + let option = extract_lz_receive_option(buf, option_size, pos); + vector::push_back(&mut options.lz_receive_options, option) + } else if (option_type == OPTION_TYPE_NATIVE_DROP) { + // Native Drop + let option = extract_native_drop_option(buf, option_size, pos); + vector::push_back(&mut options.native_drop_options, option) + } else if (option_type == OPTION_TYPE_LZ_COMPOSE) { + // LZ Compose + let option = extract_lz_compose_option(buf, option_size, pos); + vector::push_back(&mut options.lz_compose_options, option) + } else if (option_type == OPTION_TYPE_ORDERED_EXECUTION) { + // Ordered Execution + assert!(option_size == 0, EINVALID_ORDERED_EXECUTION_OPTION_LENGTH); + options.ordered_execution_option = true; + // Nothing else to read - continue to next + } else { + abort EUNSUPPORTED_OPTION + } + }; + options + } + + /// Appends an ExecutorOptions to a byte buffer + public fun append_executor_options(buf: &mut vector, options: &ExecutorOptions) { + vector::for_each_ref(&options.lz_receive_options, |option| { + serde::append_u8(buf, EXECUTOR_WORKER_ID()); + append_lz_receive_option(buf, option) + }); + vector::for_each_ref(&options.native_drop_options, |option| { + serde::append_u8(buf, EXECUTOR_WORKER_ID()); + append_native_drop_option(buf, option) + }); + vector::for_each_ref(&options.lz_compose_options, |option| { + serde::append_u8(buf, EXECUTOR_WORKER_ID()); + append_lz_compose_option(buf, option) + }); + if (options.ordered_execution_option) { + serde::append_u8(buf, EXECUTOR_WORKER_ID()); + append_ordered_execution_option(buf); + } + } + + /// Extracts a LzReceiveOption from a buffer and updates position to the end of the read + fun extract_lz_receive_option(option: &vector, size: u16, pos: &mut u64): LzReceiveOption { + let gas = serde::extract_u128(option, pos); + let value = if (size == 32) { + serde::extract_u128(option, pos) + } else if (size == 16) { + 0 + } else { + abort EINVALID_LZ_RECEIVE_OPTION_LENGTH + }; + LzReceiveOption { gas, value } + } + + /// Serializes a LzReceiveOption to the end of a buffer + fun append_lz_receive_option(output: &mut vector, lz_receive_option: &LzReceiveOption) { + let size = if (lz_receive_option.value == 0) { 17 } else { 33 }; + serde::append_u16(output, size); + serde::append_u8(output, OPTION_TYPE_LZ_RECEIVE); + serde::append_u128(output, lz_receive_option.gas); + if (lz_receive_option.value != 0) { + serde::append_u128(output, lz_receive_option.value); + } + } + + /// Extracts a NativeDropOption from a buffer and updates position to the end of the read + fun extract_native_drop_option(option: &vector, size: u16, pos: &mut u64): NativeDropOption { + assert!(size == 48, EINVALID_NATIVE_DROP_OPTION_LENGTH); + let amount = serde::extract_u128(option, pos); + let receiver = serde::extract_bytes32(option, pos); + NativeDropOption { amount, receiver } + } + + /// Serializes a NativeDropOption to the end of a buffer + fun append_native_drop_option(output: &mut vector, native_drop_option: &NativeDropOption) { + serde::append_u16(output, 49); + serde::append_u8(output, OPTION_TYPE_NATIVE_DROP); + serde::append_u128(output, native_drop_option.amount); + serde::append_bytes32(output, native_drop_option.receiver); + } + + /// Extracts a LzComposeOption from a buffer + fun extract_lz_compose_option(option: &vector, size: u16, pos: &mut u64): LzComposeOption { + let index = serde::extract_u16(option, pos); + let gas = serde::extract_u128(option, pos); + let value = if (size == 34) { + serde::extract_u128(option, pos) + } else if (size == 18) { + 0 + } else { + abort EINVALID_LZ_COMPOSE_OPTION_LENGTH + }; + LzComposeOption { index, gas, value } + } + + /// Serializes a LzComposeOption to the end of a buffer + fun append_lz_compose_option(output: &mut vector, lz_compose_option: &LzComposeOption) { + let size = if (lz_compose_option.value == 0) { 19 } else { 35 }; + serde::append_u16(output, size); + serde::append_u8(output, OPTION_TYPE_LZ_COMPOSE); + serde::append_u16(output, lz_compose_option.index); + serde::append_u128(output, lz_compose_option.gas); + if (lz_compose_option.value != 0) { + serde::append_u128(output, lz_compose_option.value); + } + } + + /// Serializes an ordered execution option into a buffer + fun append_ordered_execution_option(output: &mut vector) { + serde::append_u16(output, 1); // size = 1 + serde::append_u8(output, OPTION_TYPE_ORDERED_EXECUTION); + } + + // ================================================== Error Codes ================================================= + + const EINVALID_LZ_COMPOSE_OPTION_LENGTH: u64 = 1; + const EINVALID_LZ_RECEIVE_OPTION_LENGTH: u64 = 2; + const EINVALID_NATIVE_DROP_OPTION_LENGTH: u64 = 3; + const EINVALID_ORDERED_EXECUTION_OPTION_LENGTH: u64 = 4; + const EUNSUPPORTED_OPTION: u64 = 5; +} diff --git a/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_libs/executor_fee_lib_0/tests/executor_fee_lib_tests.move b/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_libs/executor_fee_lib_0/tests/executor_fee_lib_tests.move new file mode 100644 index 00000000..a40504d4 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_libs/executor_fee_lib_0/tests/executor_fee_lib_tests.move @@ -0,0 +1,709 @@ +#[test_only] +module executor_fee_lib_0::executor_fee_lib_tests { + use std::account; + use std::account::create_signer_for_test; + + use endpoint_v2_common::bytes32; + use endpoint_v2_common::contract_identity::make_call_ref_for_test; + use endpoint_v2_common::native_token_test_helpers::initialize_native_token_for_test; + use endpoint_v2_common::serde; + use executor_fee_lib_0::executor_fee_lib::{ + apply_premium_to_gas, calculate_executor_dst_amount_and_total_gas, convert_and_apply_premium_to_value, + get_executor_fee, get_executor_fee_internal, is_v1_eid, + }; + use executor_fee_lib_0::executor_option::{ + append_executor_options, new_executor_options, new_lz_compose_option, new_lz_receive_option, + new_native_drop_option, + }; + use price_feed_module_0::eid_model_pair; + use price_feed_module_0::eid_model_pair::{ + ARBITRUM_MODEL_TYPE, DEFAULT_MODEL_TYPE, EidModelPair, new_eid_model_pair, OPTIMISM_MODEL_TYPE, + }; + use price_feed_module_0::price; + use price_feed_module_0::price::{EidTaggedPrice, tag_price_with_eid}; + use worker_common::worker_config::{Self, set_executor_dst_config}; + + #[test] + fun test_get_fee() { + // 1. Set up the price feed (@price_feed_module_0, @1111) + use price_feed_module_0::feeds; + let feed = &create_signer_for_test(@1111); + let updater = &create_signer_for_test(@9999); + feeds::initialize(feed); + feeds::enable_feed_updater(feed, @9999); + + initialize_native_token_for_test(); + + // These prices are the same as used in the individual model tests + // We are testing whether we get the expected model response + // Using different price ratios for goerli, sepolia to see that arbitrum calcs are using the correct L2 price + let eth_price = price::new_price(4000, 51, 33); + let eth_goerli_price = price::new_price(40000, 51, 33); + let eth_sepolia_price = price::new_price(400000, 51, 33); + let arb_price = price::new_price(1222, 12, 3); + let opt_price = price::new_price(200, 43, 5); + + feeds::set_denominator(feed, 100); + feeds::set_arbitrum_compression_percent(feed, 47); + feeds::set_arbitrum_traits(updater, @1111, 5432, 11); + feeds::set_native_token_price_usd(updater, @1111, 6); + + // Test some non-hardcoded model types + let eid_model_pairs = vector[ + new_eid_model_pair( + 110, + DEFAULT_MODEL_TYPE() + ), // cannot override hardcoded type - this will still be "ARBITRUM" + new_eid_model_pair(11000, OPTIMISM_MODEL_TYPE()), // optimism using L1 sepolia + new_eid_model_pair(25555, ARBITRUM_MODEL_TYPE()), + new_eid_model_pair(26666, OPTIMISM_MODEL_TYPE()), + ]; + + let pairs_serialized = eid_model_pair::serialize_eid_model_pair_list(&eid_model_pairs); + feeds::set_eid_models(feed, pairs_serialized); + + let list = vector[ + tag_price_with_eid(101, eth_price), // First 6 EIDs are all of hardcoded types + tag_price_with_eid(110, arb_price), + tag_price_with_eid(111, opt_price), + tag_price_with_eid(10101, eth_price), + tag_price_with_eid(10143, arb_price), + tag_price_with_eid(10132, opt_price), + tag_price_with_eid(11000, opt_price), // optimism using L1 sepolia + tag_price_with_eid(10121, eth_goerli_price), // eth-goerli - used for arbitrum estimate + tag_price_with_eid(10161, eth_sepolia_price), // eth-sepolia - used for arbitrum estimate + + tag_price_with_eid(24444, eth_price), // not hardcoded and not set - should default to "DEFAULT" + tag_price_with_eid(25555, arb_price), // configured to "ARBITRUM" + tag_price_with_eid(26666, opt_price), // configured to "OPTIMISM" + tag_price_with_eid(20121, eth_goerli_price), // eth-goerli - used for arbitrum estimate + ]; + let prices_serialized = price::serialize_eid_tagged_price_list(&list); + feeds::set_price(updater, @1111, prices_serialized); + + let (fee, price_ratio, denominator, native_token_price) = feeds::estimate_fee_on_send( + @1111, + 10101, + 50, + 100, + ); + assert!(fee == 3570000, 0); + assert!(price_ratio == 4000, 1); + assert!(denominator == 100, 2); + assert!(native_token_price == 6, 3); + + // 2. Set up the worker (@1234) + let worker = @1234; + initialize_native_token_for_test(); + worker_config::initialize_for_worker_test_only( + worker, + 1, + worker, + @0x501ead, + vector[@111], + vector[@222], + @0xfee11b, + ); + worker_config::set_executor_dst_config( + &make_call_ref_for_test(worker), + 10101, + 1000, + 50, + 100, + 100000000000, + 100002132, + ); + worker_config::set_price_feed( + &make_call_ref_for_test(worker), + @price_feed_module_0, + @1111, + ); + + let options = serde::bytes_of(|buf| append_executor_options(buf, &new_executor_options( + vector[ + new_lz_receive_option(100, 0), + ], + vector[], + vector[], + false, + ))); + let (fee, deposit) = get_executor_fee( + @222, + worker, + 10101, + @5555, + 1000, + options, + ); + + assert!(fee != 0, 0); + // worker address + assert!(deposit == @1234, 1); + + // test after updating deposit address + account::create_account_for_test(@4321); + worker_config::set_deposit_address(&make_call_ref_for_test(worker), @4321); + + let options = serde::bytes_of(|buf| append_executor_options(buf, &new_executor_options( + vector[ + new_lz_receive_option(100, 0), + ], + vector[], + vector[], + false, + ))); + + let (_fee, deposit) = get_executor_fee( + @222, + worker, + 10101, + @5555, + 1000, + options, + ); + + assert!(deposit == @4321, 1); + } + + #[test] + fun test_get_fee_internal() { + initialize_native_token_for_test(); + // Set up the worker (@1234) + let worker = @1234; + worker_config::initialize_for_worker_test_only( + worker, + 1, + worker, + @0x501ead, + vector[@111], + vector[@222], + @0xfee11b, + ); + worker_config::set_executor_dst_config( + &make_call_ref_for_test(worker), + 10101, + 1000, + 50, + 100, + 100000000000, + 100002132, + ); + worker_config::set_price_feed( + &make_call_ref_for_test(worker), + @0x501ead, + @111, + ); + + let called = false; + assert!(!called, 0); + + let fee = get_executor_fee_internal( + worker, + 10101, + new_executor_options( + vector[ + new_lz_receive_option(100, 0), + ], + vector[], + vector[], + false, + ), + |price_feed_module, feed_address, total_gas| { + called = true; + assert!(price_feed_module == @0x501ead, 0); + assert!(feed_address == @111, 1); + + // [Calculate Executor DST Amount and Gas] 1000 (lz_receive base gas) + 100 (lz_receive gas) = 200 + assert!(total_gas == 1100, 1); + (20000 /*chain fee*/, 40 /*price_ratio*/, 1_000_000 /*denominator*/, 0 /*native_price_usd*/) + } + ); + assert!(called, 2); + + // [Apply Premium to gas] 20000 (fee) * 50 (multiplier) / 10000 = 100 + assert!(fee == 100, 0); + } + + #[test] + fun test_get_fee_works_with_delegated_price_feed() { + initialize_native_token_for_test(); + // other worker + worker_config::initialize_for_worker_test_only( + @5555, + 1, + @5555, + @0x501ead, + vector[@111], + vector[@222], + @0xfee11b, + ); + worker_config::set_price_feed( + &make_call_ref_for_test(@5555), + @0xabcd, + @1234, + ); + + // Set up the worker (@1234) + let worker = @1234; + worker_config::initialize_for_worker_test_only( + worker, + 1, + worker, + @0x501ead, + vector[@111], + vector[@222], + @0xfee11b, + ); + worker_config::set_executor_dst_config( + &make_call_ref_for_test(worker), + 10101, + 1000, + 50, + 100, + 100000000000, + 100002132, + ); + worker_config::set_price_feed_delegate( + &make_call_ref_for_test(worker), + @5555, + ); + + let called = false; + assert!(!called, 0); + + let fee = get_executor_fee_internal( + worker, + 10101, + new_executor_options( + vector[ + new_lz_receive_option(100, 0), + ], + vector[], + vector[], + false, + ), + |price_feed_module, feed_address, total_gas| { + called = true; + assert!(price_feed_module == @0xabcd, 0); + assert!(feed_address == @1234, 1); + + // [Calculate Executor DST Amount and Gas] 1000 (lz_receive base gas) + 100 (lz_receive gas) = 200 + assert!(total_gas == 1100, 1); + (20000 /*chain fee*/, 40 /*price_ratio*/, 1_000_000 /*denominator*/, 0 /*native_price_usd*/) + } + ); + assert!(called, 2); + + // [Apply Premium to gas] 20000 (fee) * 50 (multiplier) / 10000 = 100 + assert!(fee == 100, 0); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::EWORKER_PAUSED)] + fun test_get_fee_will_fail_if_worker_paused() { + let worker = @1234; + initialize_native_token_for_test(); + worker_common::worker_config::initialize_for_worker_test_only( + worker, + 1, + worker, + @0x501ead, + vector[@111], + vector[@222], + @0xfee11b, + ); + worker_config::set_worker_pause(&make_call_ref_for_test(worker), true); + + let options = serde::bytes_of(|buf| append_executor_options(buf, &new_executor_options( + vector[], + vector[], + vector[], + false, + ))); + + get_executor_fee( + @0xfee11b, + worker, + 12, + @555, + 1000, + options, + ); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::ESENDER_DENIED)] + fun test_get_fee_will_fail_if_sender_not_allowed() { + let worker = @1234; + initialize_native_token_for_test(); + worker_common::worker_config::initialize_for_worker_test_only( + worker, + 1, + worker, + @0x501ead, + vector[@111], + vector[@222], + @0xfee11b, + ); + // create an allowlist without the sender + worker_config::set_allowlist(&make_call_ref_for_test(worker), @55555555, true); + + worker_config::set_worker_fee_lib(&make_call_ref_for_test(worker), @0xfee11b); + set_executor_dst_config( + &make_call_ref_for_test(worker), + 12, + 1000, + 50, + 100, + 100000000000, + 100002132, + ); + + let options = serde::bytes_of(|buf| append_executor_options(buf, &new_executor_options( + vector[], + vector[], + vector[], + false, + ))); + + get_executor_fee( + @222, + worker, + 12, + @555, + 1000, + options, + ); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::EWORKER_AUTH_UNSUPPORTED_MSGLIB)] + fun test_get_fee_will_fail_if_msglib_not_supported() { + let worker = @1234; + initialize_native_token_for_test(); + worker_common::worker_config::initialize_for_worker_test_only( + worker, + 1, + worker, + @0x501ead, + vector[@111], + vector[@222], + @0xfee11b, + ); + + worker_config::set_worker_fee_lib(&make_call_ref_for_test(worker), @0xfee11b); + set_executor_dst_config( + &make_call_ref_for_test(worker), + 12, + 1000, + 50, + 100, + 100000000000, + 100002132, + ); + + let options = serde::bytes_of(|buf| append_executor_options(buf, &new_executor_options( + vector[], + vector[], + vector[], + false, + ))); + + get_executor_fee( + @1234, // not @222 + worker, + 12, + @555, + 1000, + options, + ); + } + + #[test] + fun test_apply_premium_to_gas_uses_multiplier_if_gt_fee_with_margin() { + let fee = apply_premium_to_gas( + 20000, + 10500, + 1, + 1, + 1, + ); + assert!(fee == 21000, 0); // 20000 * 10500 / 10000 + } + + #[test] + fun test_apply_premium_to_gas_uses_margin_if_gt_fee_with_multiplier() { + let fee = apply_premium_to_gas( + 20000, + 10500, + 6000, + 2000, + 1000, + ); + assert!(fee == 23000, 0); // 20000 + (6000 * 1000) / 2000 + } + + #[test] + fun test_apply_premium_to_gas_uses_margin_if_native_price_used_is_0() { + let fee = apply_premium_to_gas( + 20000, + 10500, + 6000, + 0, + 1000, + ); + assert!(fee == 21000, 0); // 20000 * 10500 / 10000; + } + + #[test] + fun test_apply_premium_to_gas_uses_multiplier_if_margin_usd_is_0() { + let fee = apply_premium_to_gas( + 20000, + 10500, + 0, + 1, + 1, + ); + assert!(fee == 21000, 0); // 20000 * 10500 / 10000 + } + + #[test] + fun test_convert_and_apply_premium_to_value() { + let fee = convert_and_apply_premium_to_value( + 9512000, + 123, + 1_000, + 600, // 6% + ); + assert!(fee == 70198, 0); // (((9512000*123)/1000) * 600) / 10000; + } + + #[test] + fun test_convert_and_apply_premium_to_value_returns_0_if_value_is_0() { + let fee = convert_and_apply_premium_to_value( + 0, + 112312323, + 1, + 1000, // 10% + ); + assert!(fee == 0, 0); + } + + #[test] + fun test_is_v1_eid() { + assert!(is_v1_eid(1), 0); + assert!(is_v1_eid(29999), 1); + assert!(!is_v1_eid(30000), 2); + assert!(!is_v1_eid(130000), 3); + } + + #[test] + fun test_calculate_executor_dst_amount_and_gas() { + let lz_receive_options = vector[ + new_lz_receive_option(100, 200), + new_lz_receive_option(300, 0), + ]; + let native_drop_options = vector[ + new_native_drop_option(100, bytes32::from_address(@123)), + new_native_drop_option(200, bytes32::from_address(@456)), + ]; + let lz_compose_options = vector[ + new_lz_compose_option(0, 400, 500), + new_lz_compose_option(1, 400, 500), + ]; + let options = new_executor_options( + lz_receive_options, + native_drop_options, + lz_compose_options, + false, + ); + + let (dst_amount, total_gas) = calculate_executor_dst_amount_and_total_gas( + false, // is_v1_eid + 100, // lz_receive_base_gas + 200, // lz_compose_base_gas + 100000000000, // native_cap + options, + ); + + let expected_total_gas = 0 + + 100 // lz_receive_base_gas + + 100 // lz_receive gas + + 300 // lz_receive gas + + 200 // lz_compose_base_gas + + 400 // lz_compose gas + + 200 // lz_compose_base_gas + + 400; // lz_compose gas + + let expected_dst_amount = 0 + + 200 // lz_receive value + + 0 // lz_receive value + + 100 // native_drop amount + + 200 // native_drop amount + + 500 // lz_compose value + + 500; // lz_compose value + + assert!(dst_amount == expected_dst_amount, 0); + assert!(total_gas == expected_total_gas, 1); + + // do again but use ordered execution option + let options = new_executor_options( + lz_receive_options, + native_drop_options, + lz_compose_options, + true, + ); + + let (dst_amount, total_gas) = calculate_executor_dst_amount_and_total_gas( + false, // is_v1_eid + 100, // lz_receive_base_gas + 200, // lz_compose_base_gas + 100000000000, // native_cap + options, + ); + + // 2% addtional fee, no change in dst_amount + let expected_total_gas = expected_total_gas * 102 / 100; + assert!(dst_amount == expected_dst_amount, 1); + assert!(total_gas == expected_total_gas, 2); + } + + #[test] + #[expected_failure(abort_code = executor_fee_lib_0::executor_fee_lib::EEXECUTOR_ZERO_LZRECEIVE_GAS_PROVIDED)] + fun test_calculate_executor_dst_amount_and_gas_should_fail_if_no_lz_receive_gas_provided() { + let lz_receive_options = vector[ + new_lz_receive_option(0, 200), + new_lz_receive_option(0, 0), + ]; + let native_drop_options = vector[]; + let lz_compose_options = vector[]; + + let options = new_executor_options( + lz_receive_options, + native_drop_options, + lz_compose_options, + false, + ); + + calculate_executor_dst_amount_and_total_gas( + false, // is_v1_eid + 100, // lz_receive_base_gas + 200, // lz_compose_base_gas + 100000000000, // native_cap + options, + ); + } + + #[test] + #[expected_failure(abort_code = executor_fee_lib_0::executor_fee_lib::EEXECUTOR_ZERO_LZCOMPOSE_GAS_PROVIDED)] + fun test_calculate_executor_dst_amount_and_gas_should_fail_if_lz_compose_gas_not_provided_on_any_of_the_options() { + let lz_receive_options = vector[ + new_lz_receive_option(100, 200), + new_lz_receive_option(300, 0), + ]; + let native_drop_options = vector[]; + let lz_compose_options = vector[ + new_lz_compose_option(0, 400, 500), + new_lz_compose_option(1, 0, 500), + ]; + + let options = new_executor_options( + lz_receive_options, + native_drop_options, + lz_compose_options, + false, + ); + + calculate_executor_dst_amount_and_total_gas( + false, // is_v1_eid + 100, // lz_receive_base_gas + 200, // lz_compose_base_gas + 100000000000, // native_cap + options, + ); + } + + #[test] + #[expected_failure(abort_code = executor_fee_lib_0::executor_fee_lib::EEXECUTOR_NATIVE_AMOUNT_EXCEEDS_CAP)] + fun test_calculate_executor_dst_amount_and_gas_should_fail_if_native_amount_exceeds_cap() { + let lz_receive_options = vector[ + new_lz_receive_option(100, 200), + new_lz_receive_option(300, 0), + ]; + let native_drop_options = vector[ + new_native_drop_option(100, bytes32::from_address(@123)), + new_native_drop_option(200, bytes32::from_address(@456)), + ]; + let lz_compose_options = vector[]; + + let options = new_executor_options( + lz_receive_options, + native_drop_options, + lz_compose_options, + false, + ); + + calculate_executor_dst_amount_and_total_gas( + false, // is_v1_eid + 100, // lz_receive_base_gas + 200, // lz_compose_base_gas + 100, // native_cap (less than the 500 total native amount) + options, + ); + } + + #[test] + #[expected_failure(abort_code = executor_fee_lib_0::executor_fee_lib::EEV1_DOES_NOT_SUPPORT_LZ_RECEIVE_WITH_VALUE)] + fun test_calculate_executor_dst_amount_and_gas_should_fail_if_v1_eid_and_lz_receive_value_provided() { + let lz_receive_options = vector[ + new_lz_receive_option(100, 200), + new_lz_receive_option(300, 0), + ]; + let native_drop_options = vector[]; + let lz_compose_options = vector[]; + + let options = new_executor_options( + lz_receive_options, + native_drop_options, + lz_compose_options, + false, + ); + + calculate_executor_dst_amount_and_total_gas( + true, // is_v1_eid + 100, // lz_receive_base_gas + 200, // lz_compose_base_gas + 100000000000, // native_cap + options, + ); + } + + #[test] + #[expected_failure(abort_code = executor_fee_lib_0::executor_fee_lib::EEV1_DOES_NOT_SUPPORT_LZ_COMPOSE_WITH_VALUE)] + fun test_calculate_executor_dst_amount_and_gas_should_fail_if_v1_eid_and_lz_compose_value_provided() { + let lz_receive_options = vector[ + new_lz_receive_option(100, 0), + new_lz_receive_option(300, 0), + ]; + let native_drop_options = vector[]; + let lz_compose_options = vector[ + new_lz_compose_option(0, 400, 500), + new_lz_compose_option(1, 400, 0), + ]; + + let options = new_executor_options( + lz_receive_options, + native_drop_options, + lz_compose_options, + true, + ); + + calculate_executor_dst_amount_and_total_gas( + true, // is_v1_eid + 100, // lz_receive_base_gas + 200, // lz_compose_base_gas + 100000000000, // native_cap + options, + ); + } +} diff --git a/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_libs/executor_fee_lib_0/tests/types/executor_option_tests.move b/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_libs/executor_fee_lib_0/tests/types/executor_option_tests.move new file mode 100644 index 00000000..d98fa970 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/worker_peripherals/fee_libs/executor_fee_lib_0/tests/types/executor_option_tests.move @@ -0,0 +1,156 @@ +#[test_only] +module executor_fee_lib_0::executor_option_tests { + use endpoint_v2_common::bytes32; + use endpoint_v2_common::serde::{Self, flatten}; + use executor_fee_lib_0::executor_option::{ + append_executor_options, extract_executor_options, new_executor_options, new_lz_compose_option, + new_lz_receive_option, new_native_drop_option, unpack_options, + }; + + #[test] + fun serializes_and_deserializes_to_the_same_input() { + let options = new_executor_options( + vector[ + new_lz_receive_option(333, 200), // serializes to a 32 length option + new_lz_receive_option(222, 0), // serializes to a 16 length option + ], + vector[ + new_native_drop_option(111, bytes32::from_address(@0x123456)), + new_native_drop_option(222, bytes32::from_address(@0x654321)), + ], + vector[ + new_lz_compose_option(1, 444, 1234), // serializes to a 34 length option + new_lz_compose_option(1, 555, 0), // serializes to a 18 length option + ], + true, + ); + + let serialized = serde::bytes_of(|buf| append_executor_options(buf, &options)); + + let deserialized = extract_executor_options(&serialized, &mut 0); + let ( + lz_receive_options, + native_drop_options, + lz_compose_options, + ordered_execution_option, + ) = unpack_options(deserialized); + + assert!(lz_receive_options == vector[ + new_lz_receive_option(333, 200), + new_lz_receive_option(222, 0), + ], 1); + + assert!(native_drop_options == vector[ + new_native_drop_option(111, bytes32::from_address(@0x123456)), + new_native_drop_option(222, bytes32::from_address(@0x654321)), + ], 2); + + assert!(lz_compose_options == vector[ + new_lz_compose_option(1, 444, 1234), + new_lz_compose_option(1, 555, 0), + ], 3); + + assert!(ordered_execution_option == true, 4); + + + // test having ordered_execution_option as false + let options = new_executor_options( + vector[], + vector[], + vector[], + false, + ); + + let serialized = serde::bytes_of(|buf| append_executor_options(buf, &options)); + let deserialized = extract_executor_options(&serialized, &mut 0); + let ( + _lz_receive_options, + _native_drop_options, + _lz_compose_options, + ordered_execution_option, + ) = unpack_options(deserialized); + + assert!(ordered_execution_option == false, 5); + } + + #[test] + #[expected_failure(abort_code = executor_fee_lib_0::executor_option::EUNSUPPORTED_OPTION)] + fun test_deserialize_executor_options_will_fail_if_provided_unsupported_option() { + let options = new_executor_options( + vector[ + new_lz_receive_option(333, 200), + new_lz_receive_option(222, 0), + ], + vector[ + new_native_drop_option(111, bytes32::from_address(@0x123456)), + new_native_drop_option(222, bytes32::from_address(@0x654321)), + ], + vector[ + new_lz_compose_option(1, 444, 1234), + new_lz_compose_option(1, 555, 0), + ], + true, + ); + let serialized = serde::bytes_of(|buf| append_executor_options(buf, &options)); + serialized = flatten(vector[ + serialized, + x"01", // worker_id = 1 + x"0001", // option_size = 1 + x"05", // option_type = 5 (invalid option type) + ]); + + extract_executor_options(&serialized, &mut 0); + } + + #[test] + #[expected_failure(abort_code = executor_fee_lib_0::executor_option::EINVALID_ORDERED_EXECUTION_OPTION_LENGTH)] + fun test_deserialize_executor_options_will_fail_if_provided_invalid_ordered_execution_option() { + let serialized = flatten(vector[ + x"04", // worker_id = 1 + x"0002", // option_size = 2 (should be 1) + x"04", // option_type = 4 (ordered execution option) + x"12", // option body + ]); + + extract_executor_options(&serialized, &mut 0); + } + + #[test] + #[expected_failure(abort_code = executor_fee_lib_0::executor_option::EINVALID_LZ_RECEIVE_OPTION_LENGTH)] + fun test_deserialize_executor_options_will_fail_if_provided_invalid_lz_receive_option() { + let serialized = flatten(vector[ + x"01", // worker_id = 1 + x"0015", // option_size = 21 (should be 17 or 33) + x"01", // option_type = 1 (lz receive option) + x"0101010101010101010101010101010101010101", // lz receive option + ]); + + extract_executor_options(&serialized, &mut 0); + } + + #[test] + #[expected_failure(abort_code = executor_fee_lib_0::executor_option::EINVALID_NATIVE_DROP_OPTION_LENGTH)] + fun test_deserialize_executor_options_will_fail_if_provided_invalid_native_drop_option() { + let serialized = flatten(vector[ + x"01", // worker_id = 1 + x"0011", // option_size = 17 (should be 49) + x"02", // option_type = 2 (native drop option) + x"01010101010101010101010101010101", // native drop option + ]); + + extract_executor_options(&serialized, &mut 0); + } + + #[test] + #[expected_failure(abort_code = executor_fee_lib_0::executor_option::EINVALID_LZ_COMPOSE_OPTION_LENGTH)] + fun test_deserialize_executor_options_will_fail_if_provided_invalid_lz_compose_option() { + let serialized = flatten(vector[ + x"01", // worker_id = 1 + x"0015", // option_size = 21 (should be 19 or 35) + x"03", // option_type = 3 (lz compose option) + x"0101010101010101010101010101010101010101", // lz compose option + ]); + + extract_executor_options(&serialized, &mut 0); + } +} diff --git a/packages/layerzero-v2/aptos/contracts/worker_peripherals/price_feed_modules/price_feed_module_0/Move.toml b/packages/layerzero-v2/aptos/contracts/worker_peripherals/price_feed_modules/price_feed_module_0/Move.toml new file mode 100644 index 00000000..c25dc12b --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/worker_peripherals/price_feed_modules/price_feed_module_0/Move.toml @@ -0,0 +1,26 @@ +[package] +name = "price_feed_module_0" +version = "1.0.0" +authors = [] + +[addresses] +endpoint_v2 = "_" +price_feed_module_0 = "_" +endpoint_v2_common = "_" +layerzero_admin = "_" +layerzero_treasury_admin = "_" + +[dev-addresses] +endpoint_v2 = "0x12345678" +price_feed_module_0 = "0x65DD71" +endpoint_v2_common = "0x9098" +layerzero_admin = "0x18943124" +layerzero_treasury_admin = "0x1894312499" + +[dependencies.AptosFramework] +git = "https://github.com/aptos-labs/aptos-framework.git" +rev = "mainnet" +subdir = "aptos-framework" + +[dependencies] +endpoint_v2_common = { local = "../../../endpoint_v2_common" } diff --git a/packages/layerzero-v2/aptos/contracts/worker_peripherals/price_feed_modules/price_feed_module_0/sources/feeds.move b/packages/layerzero-v2/aptos/contracts/worker_peripherals/price_feed_modules/price_feed_module_0/sources/feeds.move new file mode 100644 index 00000000..6217c8be --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/worker_peripherals/price_feed_modules/price_feed_module_0/sources/feeds.move @@ -0,0 +1,392 @@ +module price_feed_module_0::feeds { + use std::event::emit; + use std::signer::address_of; + use std::table::{Self, Table}; + use std::vector; + + use price_feed_module_0::eid_model_pair::{Self, ARBITRUM_MODEL_TYPE, DEFAULT_MODEL_TYPE, OPTIMISM_MODEL_TYPE}; + use price_feed_module_0::price::{Self, Price, split_eid_tagged_price}; + + #[test_only] + friend price_feed_module_0::feeds_tests; + + /// The data for a single price feed on this price feed module + struct Feed has key { + // The denominator for the price ratio remote price * price ratio / denominator = local price + denominator: u128, + // The compression percent for arbitrum (base 100) + arbitrum_compression_percent: u64, + // The model to use (which corresponds to chain type) for each destination EID + model_type: Table, + // The price ratio, gas price, and gas per byte for each destination EID + prices: Table, + // The base gas price for an Arbitrum L2 transaction + arbitrum_gas_per_l2_tx: u128, + // The gas price for an Arbitrum L1 calldata byte + arbitrum_gas_per_l1_calldata_byte: u128, + // The price, with `denominator` precision, of the native token in USD + native_token_price_usd: u128, + // The set of approved feed updaters (presence indicates approved, the value is always `true`) + updaters: Table, + } + + // =============================================== Only Feed Admins =============================================== + + /// Initializes a new feed under the signer's account address + public entry fun initialize(account: &signer) { + move_to(account, Feed { + denominator: 100_000_000_000_000_000_000, // 1e20 + arbitrum_compression_percent: 47, + model_type: table::new(), + prices: table::new(), + arbitrum_gas_per_l2_tx: 0, + arbitrum_gas_per_l1_calldata_byte: 0, + native_token_price_usd: 0, + updaters: table::new(), + }); + } + + /// Gives a feed updater permission to write to the signer's feed + public entry fun enable_feed_updater(account: &signer, updater: address) acquires Feed { + let feed_address = address_of(move account); + table::upsert(feed_updaters_mut(feed_address), updater, true); + emit(FeedUpdaterSet { feed_address, updater, enabled: true }); + } + + /// Revokes a feed updater's permission to write to the signer's feed + public entry fun disable_feed_updater(account: &signer, updater: address) acquires Feed { + let feed_address = address_of(move account); + table::remove(feed_updaters_mut(feed_address), updater); + emit(FeedUpdaterSet { feed_address, updater, enabled: false }); + } + + /// Sets the denominator for the feed + public entry fun set_denominator(account: &signer, denominator: u128) acquires Feed { + assert!(denominator > 0, EINVALID_DENOMNIATOR); + let feed_address = address_of(move account); + feed_data_mut(feed_address).denominator = denominator; + } + + /// Sets the arbitrum compression percent (base 100) for the feed + public entry fun set_arbitrum_compression_percent(account: &signer, percent: u64) acquires Feed { + let feed_address = address_of(move account); + feed_data_mut(feed_address).arbitrum_compression_percent = percent; + } + + /// Sets the model type for multiple given destination EIDs + /// The params are a serialized list of EidModelPair + public entry fun set_eid_models(account: &signer, params: vector) acquires Feed { + let feed_address = address_of(move account); + + let eid_to_model_list = eid_model_pair::deserialize_eid_model_pair_list(¶ms); + let model_type_mut = &mut feed_data_mut(feed_address).model_type; + for (i in 0..vector::length(&eid_to_model_list)) { + let eid_model = vector::borrow(&eid_to_model_list, i); + let dst_eid = eid_model_pair::get_dst_eid(eid_model); + let model_type = eid_model_pair::get_model_type(eid_model); + assert!(eid_model_pair::is_valid_model_type(model_type), EINVALID_MODEL_TYPE); + table::upsert(model_type_mut, dst_eid, model_type); + } + } + + #[view] + /// Gets the model type for a given destination EID + /// @dev the model type can be default (0), arbitrum (1), or optimism (2) + public fun get_model_type(feed_address: address, dst_eid: u32): u16 acquires Feed { + let feed = feed_data(feed_address); + *table::borrow_with_default(&feed.model_type, dst_eid, &DEFAULT_MODEL_TYPE()) + } + + // ============================================== Only Feed Updaters ============================================== + + /// Asserts that a feed updater is approved to write to a specific feed + fun assert_valid_fee_updater(updater: address, feed: address) acquires Feed { + assert!(is_price_updater(updater, feed), EUNAUTHORIZED_UPDATER); + } + + /// Sets the price (serialized EidTagged) for a given destination EID + public entry fun set_price(updater: &signer, feed: address, prices: vector) acquires Feed { + let updater = address_of(move updater); + assert_valid_fee_updater(updater, feed); + + let price_list = price::deserialize_eid_tagged_price_list(&prices); + let prices_mut = &mut feed_data_mut(feed).prices; + for (i in 0..vector::length(&price_list)) { + let eid_tagged_price = vector::borrow(&price_list, i); + let (eid, price) = split_eid_tagged_price(eid_tagged_price); + table::upsert(prices_mut, eid, price); + } + } + + /// Sets the arbitrum traits for the feed + public entry fun set_arbitrum_traits( + updater: &signer, + feed_address: address, + gas_per_l2_tx: u128, + gas_per_l1_calldata_byte: u128, + ) acquires Feed { + let updater = address_of(move updater); + assert_valid_fee_updater(updater, feed_address); + + let feed_data_mut = feed_data_mut(feed_address); + feed_data_mut.arbitrum_gas_per_l2_tx = gas_per_l2_tx; + feed_data_mut.arbitrum_gas_per_l1_calldata_byte = gas_per_l1_calldata_byte; + } + + /// Sets the native token price in USD for the feed denominated in `denominator` precision + public entry fun set_native_token_price_usd( + updater: &signer, + feed_address: address, + native_token_price_usd: u128, + ) acquires Feed { + let updater = address_of(move updater); + assert_valid_fee_updater(updater, feed_address); + + feed_data_mut(feed_address).native_token_price_usd = native_token_price_usd; + } + + // ===================================================== View ===================================================== + + #[view] + /// Gets the denominator used for price ratios for the feed + public fun get_price_ratio_denominator(feed_address: address): u128 acquires Feed { + feed_data(feed_address).denominator + } + + #[view] + /// Checks if a feed updater has the permission to write to a feed + public fun is_price_updater(updater: address, feed: address): bool acquires Feed { + table::contains(feed_updaters(feed), updater) + } + + #[view] + /// Gets the native token price in USD for the feed (denominated in `denominator` precision) + public fun get_native_token_price_usd(feed_address: address): u128 acquires Feed { + feed_data(feed_address).native_token_price_usd + } + + #[view] + /// Gets the arbitrum compression percent (base 100) for the feed + public fun get_arbitrum_compression_percent(feed_address: address): u64 acquires Feed { + feed_data(feed_address).arbitrum_compression_percent + } + + #[view] + /// Gets the arbitrum traits for the feed + /// @return (gas per L2 transaction, gas per L1 calldata byte) + public fun get_arbitrum_price_traits(feed_address: address): (u128, u128) acquires Feed { + let feed = feed_data(feed_address); + (feed.arbitrum_gas_per_l2_tx, feed.arbitrum_gas_per_l1_calldata_byte) + } + + #[view] + /// Gets the price data for a given destination EID + /// @return (price ratio, gas price in unit, gas price per byte) + public fun get_price(feed_address: address, _dst_eid: u32): (u128, u64, u32) acquires Feed { + let prices = &feed_data(feed_address).prices; + + assert!(table::contains(prices, _dst_eid), EEID_DOES_NOT_EXIST); + let eid_price = table::borrow(prices, _dst_eid); + (price::get_price_ratio(eid_price), price::get_gas_price_in_unit(eid_price), price::get_gas_per_byte(eid_price)) + } + + // ================================================ Fee Estimation ================================================ + + #[view] + /// Estimates the fee for a send transaction, considering the eid, gas, and call data size + /// This selects the appropriate model and prices inputs based on the destination EID + /// @return (fee, price ratio, denominator, native token price in USD) + public fun estimate_fee_on_send( + feed_address: address, + dst_eid: u32, + call_data_size: u64, + gas: u128, + ): (u128, u128, u128, u128) acquires Feed { + // v2 EIDs are the v1 EIDs + 30,000 + // We anticipate that each subsequent eid will be 30,000 more than the prior (but on the same chain) + let dst_eid_mod = dst_eid % 30_000; + + let feed = feed_data(feed_address); + let denominator = feed.denominator; + let native_token_price_usd = feed.native_token_price_usd; + + let type = table::borrow_with_default(&feed.model_type, dst_eid_mod, &DEFAULT_MODEL_TYPE()); + assert!(table::contains(&feed.prices, dst_eid_mod), EPRICE_FEED_NOT_CONFIGURED_FOR_EID); + let dst_pricing = table::borrow(&feed.prices, dst_eid_mod); + + let fee = if (dst_eid_mod == 110 || dst_eid_mod == 10143 || dst_eid_mod == 20143 || type == &ARBITRUM_MODEL_TYPE( + )) { + // Arbitrum Type + estimate_fee_with_arbitrum_model( + call_data_size, + gas, + dst_pricing, + feed.denominator, + feed.arbitrum_compression_percent, + feed.arbitrum_gas_per_l1_calldata_byte, + feed.arbitrum_gas_per_l2_tx, + ) + } else if (dst_eid_mod == 111 || dst_eid_mod == 10132 || dst_eid_mod == 20132 || type == &OPTIMISM_MODEL_TYPE( + )) { + // Optimism Type + let ethereum_id = get_l1_lookup_id_for_optimism_model(dst_eid_mod); + assert!(table::contains(&feed.prices, ethereum_id), EPRICE_FEED_NOT_CONFIGURED_FOR_EID_ETH_L1); + let ethereum_pricing = table::borrow(&feed.prices, ethereum_id); + estimate_fee_with_optimism_model( + call_data_size, + gas, + ethereum_pricing, + dst_pricing, + feed.denominator, + ) + } else { + // Default + estimate_fee_with_default_model(call_data_size, gas, dst_pricing, feed.denominator) + }; + + let price_ratio = price::get_price_ratio(dst_pricing); + (fee, price_ratio, denominator, native_token_price_usd) + } + + /// Estimates the fee for a send transaction using the default model + public(friend) fun estimate_fee_with_default_model( + call_data_size: u64, + gas: u128, + dst_pricing: &Price, + denominator: u128, + ): u128 { + let gas_per_byte = price::get_gas_per_byte_u128(dst_pricing); + let gas_price_in_unit = price::get_gas_price_in_unit_u128(dst_pricing); + let gas_for_call_data = (call_data_size as u128) * gas_per_byte; + let remote_fee = (gas_for_call_data + gas) * gas_price_in_unit; + + let fee = (remote_fee * price::get_price_ratio(dst_pricing)) / denominator; + fee + } + + /// Estimates the fee for a send transaction using the arbitrum model + public(friend) fun estimate_fee_with_arbitrum_model( + call_data_size: u64, + gas: u128, + dst_pricing: &Price, + denominator: u128, + arbitrum_compression_percent: u64, + arbitrum_gas_per_l1_call_data_byte: u128, + arbitrum_gas_per_l2_tx: u128, + ): u128 { + let arbitrum_gas_per_byte = price::get_gas_per_byte_u128(dst_pricing); + let gas_price_in_unit = price::get_gas_price_in_unit_u128(dst_pricing); + let price_ratio = price::get_price_ratio(dst_pricing); + + let gas_for_l1_call_data = ((call_data_size as u128) * (arbitrum_compression_percent as u128) / 100) + * arbitrum_gas_per_l1_call_data_byte; + let gas_for_l2_call_data = (call_data_size as u128) * arbitrum_gas_per_byte; + let gas_fee = (gas + + arbitrum_gas_per_l2_tx + + gas_for_l1_call_data + gas_for_l2_call_data) + * gas_price_in_unit; + + gas_fee * price_ratio / denominator + } + + /// Estimates the fee for a send transaction using the optimism model + public(friend) fun estimate_fee_with_optimism_model( + call_data_size: u64, + gas: u128, + ethereum_pricing: &Price, + optimism_pricing: &Price, + denominator: u128, + ): u128 { + // L1 Fee + let gas_per_byte_eth = price::get_gas_per_byte_u128(ethereum_pricing); + let gas_price_in_unit_eth = price::get_gas_price_in_unit_u128(ethereum_pricing); + let gas_for_l1_call_data = (call_data_size as u128) * gas_per_byte_eth + 3188; + let l1_fee = gas_for_l1_call_data * gas_price_in_unit_eth; + + // L2 Fee + let gas_per_byte_opt = price::get_gas_per_byte_u128(optimism_pricing); + let gas_price_in_unit_opt = price::get_gas_price_in_unit_u128(optimism_pricing); + let gas_for_l2_call_data = (call_data_size as u128) * gas_per_byte_opt; + let l2_fee = (gas_for_l2_call_data + gas) * gas_price_in_unit_opt; + + let gas_price_ratio_eth = price::get_price_ratio(ethereum_pricing); + let gas_price_ratio_opt = price::get_price_ratio(optimism_pricing); + let l1_fee_in_src_price = (l1_fee * gas_price_ratio_eth) / denominator; + let l2_fee_in_src_price = (l2_fee * gas_price_ratio_opt) / denominator; + + l1_fee_in_src_price + l2_fee_in_src_price + } + + /// Gets the L1 lookup ID for the optimism model + /// This is a hardcoded lookup for the L1 chain for the optimism model and it differs based on network + public(friend) fun get_l1_lookup_id_for_optimism_model(l2_eid: u32): u32 { + if (l2_eid < 10_000) { + 101 + } else if (l2_eid < 20_000) { + if (l2_eid == 10132) { + 10121 // ethereum-goerli + } else { + 10161 // ethereum-sepolia + } + } else { + 20121 // ethereum-goerli + } + } + + // ==================================================== Helpers =================================================== + + /// Asserts that a feed exists + inline fun assert_feed_exists(feed: address) { assert!(exists(feed), EFEED_DOES_NOT_EXIST); } + + /// Borrow the feed data for a single feed + inline fun feed_data(feed: address): &Feed { + assert_feed_exists(feed); + borrow_global(feed) + } + + /// Borrow the feed data for a single feed mutably + inline fun feed_data_mut(feed: address): &mut Feed { + assert_feed_exists(feed); + borrow_global_mut(feed) + } + + /// Borrow the updaters for a single feed + inline fun feed_updaters(feed: address): &Table { + assert_feed_exists(feed); + &feed_data(feed).updaters + } + + /// Borrow the updaters for a single feed mutably + inline fun feed_updaters_mut(feed: address): &mut Table { + assert_feed_exists(feed); + &mut feed_data_mut(feed).updaters + } + + // ==================================================== Events ==================================================== + + #[event] + struct FeedUpdaterSet has drop, store { + feed_address: address, + updater: address, + enabled: bool, + } + + #[test_only] + public fun feed_updater_set_event(feed_address: address, updater: address, enabled: bool): FeedUpdaterSet { + FeedUpdaterSet { + feed_address, + updater, + enabled, + } + } + + // ================================================== Error Codes ================================================= + + const EEID_DOES_NOT_EXIST: u64 = 1; + const EFEED_DOES_NOT_EXIST: u64 = 2; + const EINVALID_DENOMNIATOR: u64 = 3; + const EINVALID_MODEL_TYPE: u64 = 4; + const EPRICE_FEED_NOT_CONFIGURED_FOR_EID: u64 = 5; + const EPRICE_FEED_NOT_CONFIGURED_FOR_EID_ETH_L1: u64 = 6; + const EUNAUTHORIZED_UPDATER: u64 = 7; +} \ No newline at end of file diff --git a/packages/layerzero-v2/aptos/contracts/worker_peripherals/price_feed_modules/price_feed_module_0/sources/types/eid_model_pair.move b/packages/layerzero-v2/aptos/contracts/worker_peripherals/price_feed_modules/price_feed_module_0/sources/types/eid_model_pair.move new file mode 100644 index 00000000..8da951fe --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/worker_peripherals/price_feed_modules/price_feed_module_0/sources/types/eid_model_pair.move @@ -0,0 +1,77 @@ +module price_feed_module_0::eid_model_pair { + use std::vector; + + use endpoint_v2_common::serde::{append_u16, append_u32, extract_u16, extract_u32}; + + public inline fun DEFAULT_MODEL_TYPE(): u16 { 0 } + + public inline fun ARBITRUM_MODEL_TYPE(): u16 { 1 } + + public inline fun OPTIMISM_MODEL_TYPE(): u16 { 2 } + + /// A pair containing the destination EID and pricing model type (default, arbitrum, or optimism) + struct EidModelPair has store, copy, drop { + dst_eid: u32, + model_type: u16, + } + + // Constructor for EidModelPair + public fun new_eid_model_pair(dst_eid: u32, model_type: u16): EidModelPair { + EidModelPair { + dst_eid, + model_type, + } + } + + /// Get the destination EID from the EidModelPair + public fun get_dst_eid(pair: &EidModelPair): u32 { pair.dst_eid } + + /// Get the model type from the EidModelPair + public fun get_model_type(pair: &EidModelPair): u16 { pair.model_type } + + /// Check if the model type is valid + /// @dev The model type must be one of the following: default 0, arbitrum 1, or optimism 2 + public fun is_valid_model_type(model_type: u16): bool { + model_type == DEFAULT_MODEL_TYPE() || + model_type == ARBITRUM_MODEL_TYPE() || + model_type == OPTIMISM_MODEL_TYPE() + } + + /// Serialize EidModelPair to the end of a byte buffer + public fun append_eid_model_pair(buf: &mut vector, obj: &EidModelPair) { + append_u32(buf, obj.dst_eid); + append_u16(buf, obj.model_type); + } + + /// Serialize a list of EidModelPair + /// This is a series of EidModelPairs serialized one after the other + public fun serialize_eid_model_pair_list(objs: &vector): vector { + let buf = vector[]; + for (i in 0..vector::length(objs)) { + append_eid_model_pair(&mut buf, vector::borrow(objs, i)); + }; + buf + } + + /// Deserialize EidModelPair from a byte buffer at a given position + /// The position to be updated to the next position after the deserialized EidModelPair + public fun extract_eid_model_pair(buf: &vector, position: &mut u64): EidModelPair { + let dst_eid = extract_u32(buf, position); + let model_type = extract_u16(buf, position); + EidModelPair { + dst_eid, + model_type, + } + } + + /// Deserialize a list of EidModelPair + /// This accepts a series of EidModelPairs serialized one after the other + public fun deserialize_eid_model_pair_list(buf: &vector): vector { + let result = vector[]; + let position = 0; + while (position < vector::length(buf)) { + vector::push_back(&mut result, extract_eid_model_pair(buf, &mut position)); + }; + result + } +} diff --git a/packages/layerzero-v2/aptos/contracts/worker_peripherals/price_feed_modules/price_feed_module_0/sources/types/price.move b/packages/layerzero-v2/aptos/contracts/worker_peripherals/price_feed_modules/price_feed_module_0/sources/types/price.move new file mode 100644 index 00000000..83f5ecb1 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/worker_peripherals/price_feed_modules/price_feed_module_0/sources/types/price.move @@ -0,0 +1,108 @@ +module price_feed_module_0::price { + use std::vector; + + use endpoint_v2_common::serde; + + /// This struct carries the EID specific price and gas information required to calculate gas for a given chain and + /// convert gas, value, and fees to the current chain's native token. + /// The price_ratio is relative to a DENOMINATOR that is defined in the price feed module + struct Price has copy, drop, store { + price_ratio: u128, + gas_price_in_unit: u64, + gas_per_byte: u32, + } + + struct EidTaggedPrice has copy, drop, store { + eid: u32, + price: Price, + } + + // Creates a new price struct + public fun new_price(price_ratio: u128, gas_price_in_unit: u64, gas_per_byte: u32): Price { + Price { + price_ratio, + gas_price_in_unit, + gas_per_byte, + } + } + + public fun tag_price_with_eid(eid: u32, price: Price): EidTaggedPrice { + EidTaggedPrice { eid, price } + } + + public fun split_eid_tagged_price(eid_price: &EidTaggedPrice): (u32, Price) { + (eid_price.eid, eid_price.price) + } + + // Gets the price ratio + public fun get_price_ratio(price: &Price): u128 { price.price_ratio } + + // Gets the gas price in unit + public fun get_gas_price_in_unit(price: &Price): u64 { price.gas_price_in_unit } + + // Gets the gas price in unit as a u128 (for use in arithmetic) + public fun get_gas_price_in_unit_u128(price: &Price): u128 { (price.gas_price_in_unit as u128) } + + // Gets the gas price per byte in the native token + public fun get_gas_per_byte(price: &Price): u32 { price.gas_per_byte } + + /// Gets the gas price per byte in the native token as a u128 (for use in arithmetic) + public fun get_gas_per_byte_u128(price: &Price): u128 { (price.gas_per_byte as u128) } + + // Append Price to the end of a byte buffer + public fun append_price(buf: &mut vector, price: &Price) { + serde::append_u128(buf, price.price_ratio); + serde::append_u64(buf, price.gas_price_in_unit); + serde::append_u32(buf, price.gas_per_byte); + } + + /// Append an Eid-tagged Price to the end of a byte buffer + public fun append_eid_tagged_price(buf: &mut vector, eid_price: &EidTaggedPrice) { + serde::append_u32(buf, eid_price.eid); + append_price(buf, &eid_price.price); + } + + /// Serialize a list of Eid-tagged Prices into a byte vector + /// This will be a series of Eid-tagged Prices serialized one after the other + public fun serialize_eid_tagged_price_list(eid_prices: &vector): vector { + let buf = vector[]; + for (i in 0..vector::length(eid_prices)) { + append_eid_tagged_price(&mut buf, vector::borrow(eid_prices, i)); + }; + buf + } + + /// Extract a Price from a byte buffer at a given position + /// The position to be updated to the next position after the deserialized Price + public fun extract_price(buf: &vector, position: &mut u64): Price { + let price_ratio = serde::extract_u128(buf, position); + let gas_price_in_unit = serde::extract_u64(buf, position); + let gas_per_byte = serde::extract_u32(buf, position); + Price { + price_ratio, + gas_price_in_unit, + gas_per_byte, + } + } + + /// Extract an Eid-tagged Price from a byte buffer at a given position + /// The position to be updated to the next position after the deserialized Eid-tagged Price + public fun extract_eid_tagged_price(buf: &vector, position: &mut u64): EidTaggedPrice { + let eid = serde::extract_u32(buf, position); + EidTaggedPrice { + eid, + price: extract_price(buf, position), + } + } + + /// Deserialize a list of Eid-tagged Prices from a byte buffer + /// This will extract a series of one-after-another Eid-tagged Prices from the buffer + public fun deserialize_eid_tagged_price_list(buf: &vector): vector { + let result = vector[]; + let position = 0; + while (position < vector::length(buf)) { + vector::push_back(&mut result, extract_eid_tagged_price(buf, &mut position)); + }; + result + } +} diff --git a/packages/layerzero-v2/aptos/contracts/worker_peripherals/price_feed_modules/price_feed_module_0/tests/feeds_tests.move b/packages/layerzero-v2/aptos/contracts/worker_peripherals/price_feed_modules/price_feed_module_0/tests/feeds_tests.move new file mode 100644 index 00000000..0b08151d --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/worker_peripherals/price_feed_modules/price_feed_module_0/tests/feeds_tests.move @@ -0,0 +1,301 @@ +#[test_only] +module price_feed_module_0::feeds_tests { + use std::event::was_event_emitted; + use std::signer::address_of; + + use price_feed_module_0::eid_model_pair; + use price_feed_module_0::eid_model_pair::{ + ARBITRUM_MODEL_TYPE, DEFAULT_MODEL_TYPE, EidModelPair, new_eid_model_pair, OPTIMISM_MODEL_TYPE, + }; + use price_feed_module_0::feeds::{ + disable_feed_updater, enable_feed_updater, estimate_fee_on_send, estimate_fee_with_arbitrum_model, + estimate_fee_with_default_model, estimate_fee_with_optimism_model, feed_updater_set_event, + get_arbitrum_compression_percent, get_arbitrum_price_traits, get_l1_lookup_id_for_optimism_model, + get_model_type, get_native_token_price_usd, get_price, get_price_ratio_denominator, initialize, + is_price_updater, set_arbitrum_compression_percent, set_arbitrum_traits, set_denominator, set_eid_models, + set_native_token_price_usd, set_price, + }; + use price_feed_module_0::price::{Self, EidTaggedPrice, tag_price_with_eid}; + + #[test(feed = @1111)] + fun test_enable_disable_feed_updater(feed: &signer) { + initialize(feed); + + let feed_address = address_of(feed); + assert!(!is_price_updater(@3333, feed_address), 1); + assert!(!is_price_updater(@4444, feed_address), 2); + + enable_feed_updater(feed, @3333); + assert!(was_event_emitted(&feed_updater_set_event(@1111, @3333, true)), 0); + assert!(is_price_updater(@3333, feed_address), 1); + assert!(!is_price_updater(@4444, feed_address), 2); + + enable_feed_updater(feed, @4444); + assert!(was_event_emitted(&feed_updater_set_event(@1111, @4444, true)), 0); + assert!(is_price_updater(@3333, feed_address), 1); + assert!(is_price_updater(@4444, feed_address), 2); + + disable_feed_updater(feed, @3333); + assert!(was_event_emitted(&feed_updater_set_event(@1111, @3333, false)), 0); + assert!(!is_price_updater(@3333, feed_address), 1); + assert!(is_price_updater(@4444, feed_address), 2); + + disable_feed_updater(feed, @4444); + assert!(was_event_emitted(&feed_updater_set_event(@1111, @4444, false)), 0); + assert!(!is_price_updater(@3333, feed_address), 1); + assert!(!is_price_updater(@4444, feed_address), 2); + } + + #[test(feed = @1111)] + fun test_set_denominator(feed: &signer) { + initialize(feed); + let feed_address = address_of(feed); + assert!(get_price_ratio_denominator(feed_address) == 100_000_000_000_000_000_000, 1); // default + set_denominator(feed, 1_0000_0000); + assert!(get_price_ratio_denominator(feed_address) == 1_0000_0000, 2); + } + + #[test(feed = @1111)] + fun test_set_arbitrum_compression_percent(feed: &signer) { + initialize(feed); + let feed_address = address_of(feed); + + // check default value + assert!(get_arbitrum_compression_percent(feed_address) == 47, 1); + + // set value and check + set_arbitrum_compression_percent(feed, 50); + assert!(get_arbitrum_compression_percent(feed_address) == 50, 2); + } + + #[test(feed = @1111)] + fun test_set_eid_to_model_type(feed: &signer) { + initialize(feed); + let feed_address = address_of(feed); + + let list = vector[ + new_eid_model_pair(101, DEFAULT_MODEL_TYPE()), + new_eid_model_pair(102, OPTIMISM_MODEL_TYPE()), + new_eid_model_pair(103, ARBITRUM_MODEL_TYPE()), + ]; + + let params = eid_model_pair::serialize_eid_model_pair_list(&list); + set_eid_models(feed, params); + + assert!(get_model_type(feed_address, 101) == DEFAULT_MODEL_TYPE(), 1); + assert!(get_model_type(feed_address, 102) == OPTIMISM_MODEL_TYPE(), 2); + assert!(get_model_type(feed_address, 103) == ARBITRUM_MODEL_TYPE(), 3); + } + + #[test(feed = @1111, updater = @9999, feed_2 = @2222)] + fun test_set_price(feed: &signer, updater: &signer, feed_2: &signer) { + initialize(feed); + let feed_address = address_of(feed); + enable_feed_updater(feed, @9999); + + // unrelated feed that with the same updater enabled + initialize(feed_2); + let feed_address_2 = address_of(feed_2); + enable_feed_updater(feed_2, @9999); + + // Serialize and set prices + let list = vector[ + tag_price_with_eid(101, price::new_price(1, 2, 3)), + tag_price_with_eid(102, price::new_price(4, 5, 6)), + tag_price_with_eid(103, price::new_price(7, 8, 9)), + ]; + let prices = price::serialize_eid_tagged_price_list(&list); + set_price(updater, feed_address, prices); + + // Set price on another feed to make sure there isn't interference + let list = vector[ + tag_price_with_eid(102, price::new_price(400, 500, 600)), + ]; + let prices = price::serialize_eid_tagged_price_list(&list); + set_price(updater, feed_address_2, prices); + + // Feed should be updated + let (price_ratio, _gas_price_in_unit, _gas_per_byte) = get_price(feed_address, 101); + assert!(price_ratio == 1, 1); + let (_price_ratio, gas_price_in_unit, _gas_per_byte) = get_price(feed_address, 102); + assert!(gas_price_in_unit == 5, 2); + let (_price_ratio, _gas_price_in_unit, gas_per_byte) = get_price(feed_address, 103); + assert!(gas_per_byte == 9, 3); + } + + #[test(feed = @1111, updater = @9999)] + fun test_set_arbitrum_traits(feed: &signer, updater: &signer) { + initialize(feed); + let feed_address = address_of(feed); + enable_feed_updater(feed, @9999); + + set_arbitrum_traits(updater, feed_address, 100, 200); + + let (gas_per_l2_tx, gas_per_l1_calldata_byte) = get_arbitrum_price_traits(feed_address); + assert!(gas_per_l2_tx == 100, 1); + assert!(gas_per_l1_calldata_byte == 200, 2); + } + + #[test(feed = @1111, updater = @9999)] + fun test_set_native_token_price_usd(feed: &signer, updater: &signer) { + initialize(feed); + let feed_address = address_of(feed); + enable_feed_updater(feed, @9999); + + set_native_token_price_usd(updater, feed_address, 100); + assert!(get_native_token_price_usd(feed_address) == 100, 1); + } + + #[test(feed = @1111, updater = @9999)] + fun test_estimate_fee_on_send(feed: &signer, updater: &signer) { + initialize(feed); + enable_feed_updater(feed, @9999); + + // These prices are the same as used in the individual model tests + // We are testing whether we get the expected model response + // Using different price ratios for goerli, sepolia to see that arbitrum calcs are using the correct L2 price + let eth_price = price::new_price(4000, 51, 33); + let eth_goerli_price = price::new_price(40000, 51, 33); + let eth_sepolia_price = price::new_price(400000, 51, 33); + let arb_price = price::new_price(1222, 12, 3); + let opt_price = price::new_price(200, 43, 5); + + set_denominator(feed, 100); + set_arbitrum_compression_percent(feed, 47); + set_arbitrum_traits(updater, @1111, 5432, 11); + set_native_token_price_usd(updater, @1111, 6); + + // Test some non-hardcoded model types + let eid_model_pairs = vector[ + new_eid_model_pair( + 110, + DEFAULT_MODEL_TYPE() + ), // cannot override hardcoded type - this will still be "ARBITRUM" + new_eid_model_pair(11000, OPTIMISM_MODEL_TYPE()), // optimism using L1 sepolia + new_eid_model_pair(25555, ARBITRUM_MODEL_TYPE()), + new_eid_model_pair(26666, OPTIMISM_MODEL_TYPE()), + ]; + + let pairs_serialized = eid_model_pair::serialize_eid_model_pair_list(&eid_model_pairs); + set_eid_models(feed, pairs_serialized); + + let list = vector[ + tag_price_with_eid(101, eth_price), // First 6 EIDs are all of hardcoded types + tag_price_with_eid(110, arb_price), + tag_price_with_eid(111, opt_price), + tag_price_with_eid(10101, eth_price), + tag_price_with_eid(10143, arb_price), + tag_price_with_eid(10132, opt_price), + tag_price_with_eid(11000, opt_price), // optimism using L1 sepolia + tag_price_with_eid(10121, eth_goerli_price), // eth-goerli - used for arbitrum estimate + tag_price_with_eid(10161, eth_sepolia_price), // eth-sepolia - used for arbitrum estimate + + tag_price_with_eid(24444, eth_price), // not hardcoded and not set - should default to "DEFAULT" + tag_price_with_eid(25555, arb_price), // configured to "ARBITRUM" + tag_price_with_eid(26666, opt_price), // configured to "OPTIMISM" + tag_price_with_eid(20121, eth_goerli_price), // eth-goerli - used for arbitrum estimate + ]; + let prices_serialized = price::serialize_eid_tagged_price_list(&list); + set_price(updater, @1111, prices_serialized); + + // Variety of tests to make sure that the correct model is used and that the parameters are correctly provided + // to each model. Testing for different networks (10000 intervals) and also for different versions (30000 + // intervals). + // For the arbitrum calculations, we need to make sure that it is able to pull from the right l1 chain + // Also, testing if the % 30000 is working by adding multiples of 30000 to EIDs + + // Default (101 + 30000) + let (fee, price_ratio, denominator, native_token_price_usd) = estimate_fee_on_send(@1111, 30101, 50, 100); + assert!(fee == 3570000, 1); + assert!(price_ratio == 4000, 2); + assert!(denominator == 100, 3); + assert!(native_token_price_usd == 6, 4); + + // Default (10101) + let (fee, _pr, _d, _ntp) = estimate_fee_on_send(@1111, 10101, 50, 100); + assert!(fee == 3570000, 5); + + // Default (24444 + 60000) + let (fee, _pr, _d, _ntp) = estimate_fee_on_send(@1111, 84444, 50, 100); + assert!(fee == 3570000, 6); + + // Arbitrum (110 + 60000) + let (fee, _pr, _d, _ntp) = estimate_fee_on_send(@1111, 60110, 50, 232); + assert!(fee == 889664, 7); + + // Arbitrum (10143) + let (fee, _pr, _d, _ntp) = estimate_fee_on_send(@1111, 10143, 50, 232); + assert!(fee == 889664, 8); + + // Arbitrum (25555) + let (fee, _pr, _d, _ntp) = estimate_fee_on_send(@1111, 25555, 50, 232); + assert!(fee == 889664, 9); + + // Optimism (111 + 90000) + let (fee, _pr, _d, _ntp) = estimate_fee_on_send(@1111, 90111, 2100, 232); + assert!(fee == 148798472, 10); + + // Optimism (10132 + 30000) + let (fee, _pr, _d, _ntp) = estimate_fee_on_send(@1111, 40132, 2100, 232); + assert!(fee == 1479678152, 11); // goreli 10x + + // Optimism (11000 + 30000) + let (fee, _pr, _d, _ntp) = estimate_fee_on_send(@1111, 41000, 2100, 232); + assert!(fee == 14788474952, 11); // sepolia 100x + + // Optimism (26666) + let (fee, _pr, _d, _ntp) = estimate_fee_on_send(@1111, 26666, 2100, 232); + assert!(fee == 1479678152, 12); // goerli 10x + } + + #[test] + fun test_estimate_fee_with_default_model() { + let price = price::new_price(100000000000000000000, 1000000000, 16); + let fee = estimate_fee_with_default_model(1000, 500, &price, 100000000000000000000); + // gas: ((call data: 1000) * 16) + (gas: 500) = 16500 + // fee: (gas: 16500) * (gas price: 1000000000) * (price ratio: 100000000000000000000) / (denom: 100000000000000000000) = 16500000000000 + assert!(fee == 16500000000000, 0); + } + + #[test] + fun test_estimate_fee_with_arbitrum_model() { + let price = price::new_price(100000000000000000000, 10000000, 16); + let fee = estimate_fee_with_arbitrum_model( + 1000, + 500, + &price, + 100000000000000000000, + 47, + 29, + 4176, + ); + assert!(fee == 343060000000, 0); + // compressed calldata size = floor((calldata_size: 1000) * (47: compression_percent) / 100) = 470 + // l1 calldata gas = (compressed_size: 470) * (arb_gas_per_l1_calldata_byte: 29) = 13630 + // l2 calldata gas = (calldata_size: 1000) * (arb_gas_per_byte: 16) = 16000 + // total gas = (gas: 500) + (arb_gas_per_l2_tx: 4176) + (l1: 13630) + (l2: 16000) = 34306 + // total fee = (total gas: 34306) * (gas_price: 10000000) * (price_ratio: 100000000000000000000) / (denominator: 100000000000000000000) = 343060000000 + } + + #[test] + fun test_estimate_fee_with_optimism_model() { + let ethereum_price = price::new_price(100000000000000000000, 646718991, 8); + let optimism_price = price::new_price(100000000000000000000, 2231118, 16); + let fee = estimate_fee_with_optimism_model( + 1000, + 500, + ðereum_price, + &optimism_price, + 100000000000000000000, + ); + assert!(fee == 7272305518308, 0); + } + + #[test] + fun test_get_l1_lookup_id_for_optimism_model() { + assert!(get_l1_lookup_id_for_optimism_model(111) == 101, 1); // eth + assert!(get_l1_lookup_id_for_optimism_model(10132) == 10121, 2); // eth-goerli + assert!(get_l1_lookup_id_for_optimism_model(10500) == 10161, 2); // eth-sepolia + assert!(get_l1_lookup_id_for_optimism_model(20132) == 20121, 3); // eth-goerli + } +} diff --git a/packages/layerzero-v2/aptos/contracts/worker_peripherals/price_feed_modules/price_feed_module_0/tests/types/eid_model_pair_tests.move b/packages/layerzero-v2/aptos/contracts/worker_peripherals/price_feed_modules/price_feed_module_0/tests/types/eid_model_pair_tests.move new file mode 100644 index 00000000..438d7a31 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/worker_peripherals/price_feed_modules/price_feed_module_0/tests/types/eid_model_pair_tests.move @@ -0,0 +1,38 @@ +#[test_only] +module price_feed_module_0::eid_model_pair_tests { + use std::vector; + + use price_feed_module_0::eid_model_pair::{ + append_eid_model_pair, deserialize_eid_model_pair_list, EidModelPair, extract_eid_model_pair, + get_dst_eid, get_model_type, new_eid_model_pair, serialize_eid_model_pair_list, + }; + + #[test] + fun test_eid_to_model() { + let obj = new_eid_model_pair(123, 456); + let buf = vector[]; + append_eid_model_pair(&mut buf, &obj); + let pos = 0; + let obj2 = extract_eid_model_pair(&buf, &mut pos); + + // test getters + assert!(get_dst_eid(&obj) == get_dst_eid(&obj2), 0); + assert!(get_model_type(&obj) == get_model_type(&obj2), 1); + + // test object + assert!(obj == obj2, 3); + } + + #[test] + fun test_eid_to_model_list() { + let objs = vector[]; + vector::push_back(&mut objs, new_eid_model_pair(123, 456)); + vector::push_back(&mut objs, new_eid_model_pair(789, 1)); + + let buf = serialize_eid_model_pair_list(&objs); + let objs2 = deserialize_eid_model_pair_list(&buf); + + // test object + assert!(objs == objs2, 1); + } +} diff --git a/packages/layerzero-v2/aptos/contracts/worker_peripherals/price_feed_modules/price_feed_module_0/tests/types/price_tests.move b/packages/layerzero-v2/aptos/contracts/worker_peripherals/price_feed_modules/price_feed_module_0/tests/types/price_tests.move new file mode 100644 index 00000000..fa8510aa --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/worker_peripherals/price_feed_modules/price_feed_module_0/tests/types/price_tests.move @@ -0,0 +1,40 @@ +#[test_only] +module price_feed_module_0::price_tests { + use std::vector; + + use price_feed_module_0::price::{ + append_eid_tagged_price, + deserialize_eid_tagged_price_list, EidTaggedPrice, extract_eid_tagged_price, new_price, + serialize_eid_tagged_price_list, tag_price_with_eid, + }; + + #[test] + fun test_append_extract_eid_tagged_price() { + let obj = tag_price_with_eid( + 123, + new_price(456, 789, 101112), + ); + let buf = vector[]; + append_eid_tagged_price(&mut buf, &obj); + let pos = 0; + let obj2 = extract_eid_tagged_price(&buf, &mut pos); + assert!(obj == obj2, 1); + } + + #[test] + fun test_append_extract_eid_tagged_price_list() { + let objs = vector[]; + vector::push_back(&mut objs, tag_price_with_eid( + 123, + new_price(456, 789, 101112), + )); + vector::push_back(&mut objs, tag_price_with_eid( + 321, + new_price(654, 987, 111210), + )); + + let serialized = serialize_eid_tagged_price_list(&objs); + let objs2 = deserialize_eid_tagged_price_list(&serialized); + assert!(objs == objs2, 1); + } +} diff --git a/packages/layerzero-v2/aptos/contracts/worker_peripherals/price_feed_routers/price_feed_router_0/Move.toml b/packages/layerzero-v2/aptos/contracts/worker_peripherals/price_feed_routers/price_feed_router_0/Move.toml new file mode 100644 index 00000000..dd6272e8 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/worker_peripherals/price_feed_routers/price_feed_router_0/Move.toml @@ -0,0 +1,29 @@ +[package] +name = "price_feed_router_0" +version = "1.0.0" + +[addresses] +price_feed_router_0 = "_" +price_feed_router_1 = "_" +price_feed_module_0 = "_" +endpoint_v2_common = "_" +endpoint_v2 = "_" +layerzero_admin = "_" +layerzero_treasury_admin = "_" +worker_common = "_" + +[dev-addresses] +price_feed_router_0 = "0x65DD71A" +price_feed_router_1 = "0x65DD71AB" +price_feed_module_0 = "0x65DD71" +endpoint_v2_common = "0x9098" +endpoint_v2 = "0x12345678" +layerzero_admin = "0x18943124" +layerzero_treasury_admin = "0x1894312499" +worker_common = "0x3999" + +[dependencies] +endpoint_v2_common = { local = "../../../endpoint_v2_common" } +worker_common = { local = "../../worker_common" } +price_feed_module_0 = { local = "../../price_feed_modules/price_feed_module_0" } +price_feed_router_1 = { local = "../../price_feed_routers/price_feed_router_1_placeholder" } \ No newline at end of file diff --git a/packages/layerzero-v2/aptos/contracts/worker_peripherals/price_feed_routers/price_feed_router_0/sources/router.move b/packages/layerzero-v2/aptos/contracts/worker_peripherals/price_feed_routers/price_feed_router_0/sources/router.move new file mode 100644 index 00000000..386bfe96 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/worker_peripherals/price_feed_routers/price_feed_router_0/sources/router.move @@ -0,0 +1,21 @@ +module price_feed_router_0::router { + public fun estimate_fee_on_send( + price_feed: address, + feed_address: address, + dst_eid: u32, + call_data_size: u64, + gas: u128, + ): (u128, u128, u128, u128) { + if (price_feed == @price_feed_module_0) { + price_feed_module_0::feeds::estimate_fee_on_send(feed_address, dst_eid, call_data_size, gas) + } else { + price_feed_router_1::router::estimate_fee_on_send( + price_feed, + feed_address, + dst_eid, + call_data_size, + gas, + ) + } + } +} \ No newline at end of file diff --git a/packages/layerzero-v2/aptos/contracts/worker_peripherals/price_feed_routers/price_feed_router_1_placeholder/Move.toml b/packages/layerzero-v2/aptos/contracts/worker_peripherals/price_feed_routers/price_feed_router_1_placeholder/Move.toml new file mode 100644 index 00000000..68e82516 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/worker_peripherals/price_feed_routers/price_feed_router_1_placeholder/Move.toml @@ -0,0 +1,14 @@ +[package] +name = "price_feed_router_1" +version = "1.0.0" + +[addresses] +price_feed_router_1 = "_" + +[dev-addresses] +price_feed_router_1 = "0x2142134" + +[dependencies.AptosFramework] +git = "https://github.com/aptos-labs/aptos-framework.git" +rev = "mainnet" +subdir = "aptos-framework" diff --git a/packages/layerzero-v2/aptos/contracts/worker_peripherals/price_feed_routers/price_feed_router_1_placeholder/sources/router.move b/packages/layerzero-v2/aptos/contracts/worker_peripherals/price_feed_routers/price_feed_router_1_placeholder/sources/router.move new file mode 100644 index 00000000..59dca70b --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/worker_peripherals/price_feed_routers/price_feed_router_1_placeholder/sources/router.move @@ -0,0 +1,15 @@ +module price_feed_router_1::router { + public fun estimate_fee_on_send( + _price_feed: address, + _feed_address: address, + _dst_eid: u32, + _call_data_size: u64, + _gas: u128, + ): (u128, u128, u128, u128) { + abort EUNKNOWN_PRICE_FEED + } + + // ================================================== Error Codes ================================================= + + const EUNKNOWN_PRICE_FEED: u64 = 1; +} \ No newline at end of file diff --git a/packages/layerzero-v2/aptos/contracts/worker_peripherals/worker_common/Move.toml b/packages/layerzero-v2/aptos/contracts/worker_peripherals/worker_common/Move.toml new file mode 100644 index 00000000..e1d0e5c4 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/worker_peripherals/worker_common/Move.toml @@ -0,0 +1,27 @@ +[package] +name = "worker_common" +version = "1.0.0" + +[addresses] +endpoint_v2 = "_" +endpoint_v2_common = "_" +layerzero_admin = "_" +layerzero_treasury_admin = "_" +worker_common = "_" +native_token_metadata_address = "0xa" +# For Initia: "0x8e4733bdabcf7d4afc3d14f0dd46c9bf52fb0fce9e4b996c939e195b8bc891d9" + +[dev-addresses] +endpoint_v2 = "0x13241234" +endpoint_v2_common = "0x9098" +layerzero_admin = "0x18943124" +layerzero_treasury_admin = "0x1894312499" +worker_common = "0x3999" + +[dependencies.AptosFramework] +git = "https://github.com/aptos-labs/aptos-framework.git" +rev = "mainnet" +subdir = "aptos-framework" + +[dependencies] +endpoint_v2_common = { local = "../../endpoint_v2_common" } \ No newline at end of file diff --git a/packages/layerzero-v2/aptos/contracts/worker_peripherals/worker_common/sources/internal/multisig_store.move b/packages/layerzero-v2/aptos/contracts/worker_peripherals/worker_common/sources/internal/multisig_store.move new file mode 100644 index 00000000..79e2e0fc --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/worker_peripherals/worker_common/sources/internal/multisig_store.move @@ -0,0 +1,198 @@ +module worker_common::multisig_store { + use std::option; + use std::signer::address_of; + use std::table::{Self, Table}; + use std::vector; + + use endpoint_v2_common::assert_no_duplicates::assert_no_duplicates; + use endpoint_v2_common::bytes32::{Bytes32, from_bytes32}; + + friend worker_common::multisig; + + #[test_only] + friend worker_common::signing_store_tests; + + const PUBKEY_SIZE: u64 = 64; + const SIGNATURE_SIZE: u64 = 65; // 64 bytes signature + 1 byte recovery id + + struct SigningStore has key { + quorum: u64, + signers: vector>, + used_hashes: Table, + } + + public(friend) fun initialize_for_worker(worker_account: &signer) { + let worker_address = address_of(worker_account); + assert!(!exists(worker_address), EWORKER_ALREADY_INITIALIZED); + move_to(move worker_account, SigningStore { + quorum: 0, + signers: vector[], + used_hashes: table::new(), + }); + } + + /// Mark the hash as used after asserting that the hash is not expired, signatures are verified, and the hash has + /// not been previously used + public(friend) fun assert_all_and_add_to_history( + worker: address, + signatures: &vector, + expiry: u64, + hash: Bytes32, + ) acquires SigningStore { + assert_not_expired(expiry); + assert_signatures_verified(worker, signatures, hash); + assert!(!was_hash_used(worker, hash), EHASH_ALREADY_USED); + add_hash_to_used(worker, hash); + } + + /// Asserts that multiple signatures match the provided pub keys at the provided quorum threshold + fun assert_signatures_verified( + worker: address, + signatures_joined: &vector, + hash: Bytes32, + ) acquires SigningStore { + let signatures = &split_signatures(signatures_joined); + let quorum = get_quorum(worker); + let signers = get_multisig_signers(worker); + assert_signatures_verified_internal(signatures, hash, &signers, quorum); + } + + /// Internal - asserts that multiple signatures match the provided pub keys at the provided quorum threshold + public(friend) fun assert_signatures_verified_internal( + signatures: &vector>, + hash: Bytes32, + multisig_signers: &vector>, + quorum: u64, + ) { + let signatures_count = vector::length(signatures); + assert!(signatures_count >= quorum, EDVN_LESS_THAN_QUORUM); + + let pub_keys_verified = &mut vector[]; + for (i in 0..signatures_count) { + let pubkey_bytes = get_pubkey(vector::borrow(signatures, i), hash); + assert!(vector::contains(multisig_signers, &pubkey_bytes), EDVN_INCORRECT_SIGNATURE); + assert!(!vector::contains(pub_keys_verified, &pubkey_bytes), EDVN_DUPLICATE_PK); + vector::push_back(pub_keys_verified, pubkey_bytes); + } + } + + /// Internal - gets the pubkey given a signature + public(friend) fun get_pubkey(signature_with_recovery: &vector, hash: Bytes32): vector { + let signature = vector::slice(signature_with_recovery, 0, 64); + let recovery_id = *vector::borrow(signature_with_recovery, 64); + + let ecdsa_signature = std::secp256k1::ecdsa_signature_from_bytes(signature); + let pubkey = std::secp256k1::ecdsa_recover( + from_bytes32(hash), + recovery_id, + &ecdsa_signature, + ); + assert!(std::option::is_some(&pubkey), EDVN_INCORRECT_SIGNATURE); + std::secp256k1::ecdsa_raw_public_key_to_bytes(option::borrow(&pubkey)) + } + + /// Internal - splits a vector of signatures into a vector of vectors of signatures + public(friend) fun split_signatures(signatures: &vector): vector> { + let bytes_length = vector::length(signatures); + assert!(bytes_length % SIGNATURE_SIZE == 0, EINVALID_SIGNATURE_LENGTH); + + let signatures_vector = vector[]; + let i = 0; + while (i < bytes_length) { + let signature = vector::slice(signatures, i, i + SIGNATURE_SIZE); + vector::push_back(&mut signatures_vector, signature); + i = i + SIGNATURE_SIZE; + }; + signatures_vector + } + + public(friend) fun assert_not_expired(expiration: u64) { + let current_time = std::timestamp::now_seconds(); + assert!(expiration > current_time, EEXPIRED_SIGNATURE); + } + + public(friend) fun set_quorum(worker: address, quorum: u64) acquires SigningStore { + let store = signing_store_mut(worker); + let signer_count = vector::length(&store.signers); + assert!(quorum > 0, EZERO_QUORUM); + assert!(quorum <= signer_count, ESIGNERS_LESS_THAN_QUORUM); + store.quorum = quorum; + } + + public(friend) fun get_quorum(worker: address): u64 acquires SigningStore { + signing_store(worker).quorum + } + + public(friend) fun set_multisig_signers( + worker: address, + multisig_signers: vector>, + ) acquires SigningStore { + assert_no_duplicates(&multisig_signers); + vector::for_each_ref(&multisig_signers, |signer| { + assert!(vector::length(signer) == PUBKEY_SIZE, EINVALID_SIGNER_LENGTH); + }); + let store = signing_store_mut(worker); + assert!(store.quorum <= vector::length(&multisig_signers), ESIGNERS_LESS_THAN_QUORUM); + store.signers = multisig_signers; + } + + public(friend) fun add_multisig_signer(worker: address, multisig_signer: vector) acquires SigningStore { + let multisig_signers = &mut signing_store_mut(worker).signers; + assert!(!vector::contains(multisig_signers, &multisig_signer), ESIGNER_ALREADY_EXISTS); + assert!(vector::length(&multisig_signer) == PUBKEY_SIZE, EINVALID_SIGNER_LENGTH); + vector::push_back(multisig_signers, multisig_signer); + } + + public(friend) fun remove_multisig_signer(worker: address, multisig_signer: vector) acquires SigningStore { + let (found, index) = vector::index_of(&signing_store(worker).signers, &multisig_signer); + assert!(found, ESIGNER_NOT_FOUND); + vector::swap_remove(&mut signing_store_mut(worker).signers, index); + let store = signing_store(worker); + assert!(vector::length(&store.signers) >= store.quorum, ESIGNERS_LESS_THAN_QUORUM); + } + + public(friend) fun is_multisig_signer(worker: address, multisig_signer: vector): bool acquires SigningStore { + let multisig_signers = &signing_store(worker).signers; + vector::contains(multisig_signers, &multisig_signer) + } + + public(friend) fun get_multisig_signers(worker: address): vector> acquires SigningStore { + signing_store(worker).signers + } + + public(friend) fun was_hash_used(worker: address, hash: Bytes32): bool acquires SigningStore { + let dvn_used_hashes = &signing_store(worker).used_hashes; + table::contains(dvn_used_hashes, hash) + } + + public(friend) fun add_hash_to_used(worker: address, hash: Bytes32) acquires SigningStore { + let dvn_used_hashes = &mut signing_store_mut(worker).used_hashes; + table::add(dvn_used_hashes, hash, true); + } + + public(friend) fun assert_initialized(worker: address) { + assert!(exists(worker), EWORKER_MULTISIG_NOT_REGISTERED); + } + + // ==================================================== Helpers =================================================== + + inline fun signing_store(worker: address): &SigningStore { borrow_global(worker) } + + inline fun signing_store_mut(worker: address): &mut SigningStore { borrow_global_mut(worker) } + + // ================================================== Error Codes ================================================= + + const EWORKER_ALREADY_INITIALIZED: u64 = 1; + const EDVN_INCORRECT_SIGNATURE: u64 = 2; + const EWORKER_MULTISIG_NOT_REGISTERED: u64 = 3; + const EDVN_DUPLICATE_PK: u64 = 4; + const EDVN_LESS_THAN_QUORUM: u64 = 5; + const EINVALID_SIGNATURE_LENGTH: u64 = 6; + const EEXPIRED_SIGNATURE: u64 = 7; + const EHASH_ALREADY_USED: u64 = 8; + const ESIGNERS_LESS_THAN_QUORUM: u64 = 9; + const ESIGNER_NOT_FOUND: u64 = 10; + const ESIGNER_ALREADY_EXISTS: u64 = 11; + const EINVALID_SIGNER_LENGTH: u64 = 12; + const EZERO_QUORUM: u64 = 13; +} diff --git a/packages/layerzero-v2/aptos/contracts/worker_peripherals/worker_common/sources/internal/worker_config_store.move b/packages/layerzero-v2/aptos/contracts/worker_peripherals/worker_common/sources/internal/worker_config_store.move new file mode 100644 index 00000000..99bbef7e --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/worker_peripherals/worker_common/sources/internal/worker_config_store.move @@ -0,0 +1,406 @@ +module worker_common::worker_config_store { + use std::option::{Self, Option}; + use std::signer::address_of; + use std::table::{Self, Table}; + use std::vector; + + friend worker_common::worker_config; + + #[test_only] + friend worker_common::worker_config_store_tests; + + struct WorkerStore has key { + // The worker ID (either Executor: 1 or DVN: 2) + worker_id: u8, + // The feelib that should be used for this worker + fee_lib: address, + // The admins of this worker + admins: vector
, + // The role admins of this worker - role admins can add/remove admins and other role admins + role_admins: vector
, + // The supported message libraries for this worker + supported_msglibs: vector
, + // The allowlist of senders - if not empty, no senders not on allowlist will be allowed + allowlist: Table, + // The denylist of senders - senders on the denylist will not be allowed + denylist: Table, + // The number of items on the allowlist. This is needed to check whether the allowlist is empty + allowlist_count: u64, + // Whether the worker has been paused. If the worker is paused, any send transaction that involves this worker + // will fail + paused: bool, + // The optional price feed module and feed selection for this worker + price_feed_selection: Option, + // The optional price feed delegate selection for this worker + price_feed_delegate_selection: Option
, + // The number of workers delegating to this worker for price feed selection + count_price_feed_delegating_workers: u64, + // The deposit address for this worker: worker payments will be sent to this address + deposit_address: address, + // The default multiplier bps that will be used to calculate premiums for this worker + default_multiplier_bps: u16, + // The serialized supported option types for this worker + supported_option_types: vector, + // Per destination EID configuration (for executors only) + executor_dst_config: Table, + // Per destination EID configuration (for DVNs only) + dvn_dst_config: Table, + } + + struct PriceFeedSelection has drop, copy, store { + price_feed_module: address, + price_feed: address, + } + + struct ExecutorDstConfig has store, copy, drop { + lz_receive_base_gas: u64, + multiplier_bps: u16, + floor_margin_usd: u128, + native_cap: u128, + lz_compose_base_gas: u64, + } + + struct DvnDstConfig has store, copy, drop { + gas: u64, + multiplier_bps: u16, + floor_margin_usd: u128, + } + + public(friend) fun initialize_store_for_worker( + worker_account: &signer, + worker_id: u8, + deposit_address: address, + role_admin: address, + admins: vector
, + supported_msglibs: vector
, + fee_lib: address, + ) { + let worker_address = address_of(worker_account); + assert!(!exists(worker_address), EWORKER_ALREADY_INITIALIZED); + assert!(vector::length(&admins) > 0, ENO_ADMINS_PROVIDED); + move_to(move worker_account, WorkerStore { + worker_id, + fee_lib, + role_admins: vector[role_admin], + admins, + supported_msglibs, + allowlist: table::new(), + denylist: table::new(), + allowlist_count: 0, + paused: false, + price_feed_selection: option::none(), + price_feed_delegate_selection: option::none(), + count_price_feed_delegating_workers: 0, + deposit_address, + default_multiplier_bps: 0, + supported_option_types: vector[], + executor_dst_config: table::new(), + dvn_dst_config: table::new(), + }) + } + + public(friend) fun assert_initialized(worker: address) { + assert!(exists(worker), EWORKER_NOT_REGISTERED); + } + + public(friend) fun is_worker_initialized(worker: address): bool { + exists(worker) + } + + public(friend) fun get_worker_id(worker: address): u8 acquires WorkerStore { + worker_store(worker).worker_id + } + + // ================================================ Worker General ================================================ + + public(friend) fun set_supported_msglibs(worker: address, msglibs: vector
) acquires WorkerStore { + worker_store_mut(worker).supported_msglibs = msglibs; + } + + public(friend) fun get_supported_msglibs(worker: address): vector
acquires WorkerStore { + worker_store(worker).supported_msglibs + } + + public(friend) fun is_supported_msglib(worker: address, msglib: address): bool acquires WorkerStore { + vector::contains(&worker_store(worker).supported_msglibs, &msglib) + } + + public(friend) fun set_fee_lib(worker: address, fee_lib: address) acquires WorkerStore { + worker_store_mut(worker).fee_lib = fee_lib; + } + + public(friend) fun get_fee_lib(worker: address): address acquires WorkerStore { + worker_store(worker).fee_lib + } + + public(friend) fun set_pause_status(worker: address, paused: bool) acquires WorkerStore { + worker_store_mut(worker).paused = paused; + } + + public(friend) fun is_paused(worker: address): bool acquires WorkerStore { + worker_store(worker).paused + } + + public(friend) fun set_deposit_address(worker: address, deposit_address: address) acquires WorkerStore { + worker_store_mut(worker).deposit_address = deposit_address; + } + + public(friend) fun get_deposit_address(worker: address): address acquires WorkerStore { + worker_store(worker).deposit_address + } + + public(friend) fun set_default_multiplier_bps(worker: address, default_multiplier_bps: u16) acquires WorkerStore { + worker_store_mut(worker).default_multiplier_bps = default_multiplier_bps; + } + + public(friend) fun get_default_multiplier_bps(worker: address): u16 acquires WorkerStore { + worker_store(worker).default_multiplier_bps + } + + public(friend) fun set_supported_option_types(worker: address, option_types: vector) acquires WorkerStore { + worker_store_mut(worker).supported_option_types = option_types; + } + + public(friend) fun get_supported_option_types(worker: address): vector acquires WorkerStore { + worker_store(worker).supported_option_types + } + + // =================================================== Executor =================================================== + + public(friend) fun set_executor_dst_config( + worker: address, + dst_eid: u32, + lz_receive_base_gas: u64, + multiplier_bps: u16, + floor_margin_usd: u128, + native_cap: u128, + lz_compose_base_gas: u64, + ) acquires WorkerStore { + let executor_dst_config = &mut worker_store_mut(worker).executor_dst_config; + table::upsert(executor_dst_config, dst_eid, ExecutorDstConfig { + lz_receive_base_gas, + multiplier_bps, + floor_margin_usd, + native_cap, + lz_compose_base_gas, + }); + } + + public(friend) fun get_executor_dst_config_values( + worker: address, + dst_eid: u32, + ): (u64, u16, u128, u128, u64) acquires WorkerStore { + let config_store = &worker_store(worker).executor_dst_config; + assert!(table::contains(config_store, dst_eid), EEXECUTOR_DST_EID_NOT_CONFIGURED); + let executor_dst_config = table::borrow(config_store, dst_eid); + ( + executor_dst_config.lz_receive_base_gas, + executor_dst_config.multiplier_bps, + executor_dst_config.floor_margin_usd, + executor_dst_config.native_cap, + executor_dst_config.lz_compose_base_gas, + ) + } + + // ====================================================== DVN ===================================================== + + public(friend) fun set_dvn_dst_config( + worker: address, + dst_eid: u32, + gas: u64, + multiplier_bps: u16, + floor_margin_usd: u128, + ) acquires WorkerStore { + let dvn_dst_config = &mut worker_store_mut(worker).dvn_dst_config; + table::upsert(dvn_dst_config, dst_eid, DvnDstConfig { gas, multiplier_bps, floor_margin_usd }); + } + + public(friend) fun get_dvn_dst_config_values( + worker: address, + dst_eid: u32, + ): (u64, u16, u128) acquires WorkerStore { + let config_store = &worker_store(worker).dvn_dst_config; + assert!(table::contains(config_store, dst_eid), EDVN_DST_EID_NOT_CONFIGURED); + let dvn_dst_config = table::borrow(&worker_store(worker).dvn_dst_config, dst_eid); + (dvn_dst_config.gas, dvn_dst_config.multiplier_bps, dvn_dst_config.floor_margin_usd) + } + + // ================================================== Price Feed ================================================== + + public(friend) fun set_price_feed( + worker: address, + price_feed_module: address, + price_feed: address, + ) acquires WorkerStore { + worker_store_mut(worker).price_feed_selection = option::some(PriceFeedSelection { + price_feed_module, + price_feed, + }); + } + + public(friend) fun has_price_feed(worker: address): bool acquires WorkerStore { + option::is_some(&worker_store(worker).price_feed_selection) + } + + public(friend) fun get_price_feed(worker: address): (address, address) acquires WorkerStore { + let price_feed_selection = &worker_store(worker).price_feed_selection; + assert!(option::is_some(price_feed_selection), ENO_PRICE_FEED_CONFIGURED); + let selection = option::borrow(price_feed_selection); + (selection.price_feed_module, selection.price_feed) + } + + public(friend) fun set_price_feed_delegate(worker: address, delegate: address) acquires WorkerStore { + let price_feed_delegate_selection = &mut worker_store_mut(worker).price_feed_delegate_selection; + let prior_delegate = *price_feed_delegate_selection; + + assert!(option::is_none(&prior_delegate) || *option::borrow(&prior_delegate) != delegate, EUNCHANGED); + *price_feed_delegate_selection = option::some(delegate); + + // subtract from prior delegate's count if it exists + if (option::is_some(&prior_delegate)) { + let prior = *option::borrow(&prior_delegate); + let count_delegating = &mut worker_store_mut(prior).count_price_feed_delegating_workers; + *count_delegating = *count_delegating - 1; + }; + + // add to new delegate's count + let count_delegating = &mut worker_store_mut(delegate).count_price_feed_delegating_workers; + *count_delegating = *count_delegating + 1; + } + + public(friend) fun unset_price_feed_delegate(worker: address) acquires WorkerStore { + let price_feed_delegate_selection = &mut worker_store_mut(worker).price_feed_delegate_selection; + assert!(option::is_some(price_feed_delegate_selection), ENOT_DELEGATING); + + let prior_delegate = *option::borrow(price_feed_delegate_selection); + *price_feed_delegate_selection = option::none(); + + // subtract from prior delegate's count + let count_delegating = &mut worker_store_mut(prior_delegate).count_price_feed_delegating_workers; + *count_delegating = *count_delegating - 1; + } + + public(friend) fun has_price_feed_delegate(worker: address): bool acquires WorkerStore { + option::is_some(&worker_store(worker).price_feed_delegate_selection) + } + + public(friend) fun get_price_feed_delegate(worker: address): address acquires WorkerStore { + *option::borrow(&worker_store(worker).price_feed_delegate_selection) + } + + public(friend) fun get_count_price_feed_delegate_dependents(worker: address): u64 acquires WorkerStore { + worker_store(worker).count_price_feed_delegating_workers + } + + // ==================================================== Admins ==================================================== + + public(friend) fun add_role_admin(worker: address, role_admin: address) acquires WorkerStore { + let role_admins = &mut worker_store_mut(worker).role_admins; + assert!(!vector::contains(role_admins, &role_admin), EROLE_ADMIN_ALREADY_EXISTS); + vector::push_back(role_admins, role_admin); + } + + public(friend) fun remove_role_admin(worker: address, role_admin: address) acquires WorkerStore { + let (found, index) = vector::index_of(&worker_store(worker).role_admins, &role_admin); + assert!(found, EROLE_ADMIN_NOT_FOUND); + vector::swap_remove(&mut worker_store_mut(worker).role_admins, index); + } + + public(friend) fun get_role_admins(worker: address): vector
acquires WorkerStore { + worker_store(worker).role_admins + } + + public(friend) fun is_role_admin(worker: address, role_admin: address): bool acquires WorkerStore { + vector::contains(&worker_store(worker).role_admins, &role_admin) + } + + public(friend) fun add_admin(worker: address, admin: address) acquires WorkerStore { + let admins = &mut worker_store_mut(worker).admins; + assert!(!vector::contains(admins, &admin), EADMIN_ALREADY_EXISTS); + vector::push_back(admins, admin); + } + + public(friend) fun remove_admin(worker: address, admin: address) acquires WorkerStore { + let (found, index) = vector::index_of(&worker_store(worker).admins, &admin); + assert!(found, EADMIN_NOT_FOUND); + let admins = &mut worker_store_mut(worker).admins; + vector::swap_remove(admins, index); + assert!(vector::length(admins) > 0, EATTEMPING_TO_REMOVE_ONLY_ADMIN); + } + + public(friend) fun get_admins(worker: address): vector
acquires WorkerStore { + worker_store(worker).admins + } + + public(friend) fun is_admin(worker: address, admin: address): bool acquires WorkerStore { + vector::contains(&worker_store(worker).admins, &admin) + } + + // ====================================================== ACL ===================================================== + + public(friend) fun add_to_allowlist(worker: address, sender: address) acquires WorkerStore { + let allowlist = &mut worker_store_mut(worker).allowlist; + assert!(!table::contains(allowlist, sender), EWORKER_ALREADY_ON_ALLOWLIST); + table::add(allowlist, sender, true); + let count_allowed = &mut worker_store_mut(worker).allowlist_count; + *count_allowed = *count_allowed + 1; + } + + public(friend) fun remove_from_allowlist(worker: address, sender: address) acquires WorkerStore { + let allowlist = &mut worker_store_mut(worker).allowlist; + assert!(table::contains(allowlist, sender), EWORKER_NOT_ON_ALLOWLIST); + table::remove(allowlist, sender); + let count_allowed = &mut worker_store_mut(worker).allowlist_count; + *count_allowed = *count_allowed - 1; + } + + public(friend) fun add_to_denylist(worker: address, sender: address) acquires WorkerStore { + let denylist = &mut worker_store_mut(worker).denylist; + assert!(!table::contains(denylist, sender), EWORKER_ALREADY_ON_DENYLIST); + table::add(denylist, sender, true); + } + + public(friend) fun remove_from_denylist(worker: address, sender: address) acquires WorkerStore { + let denylist = &mut worker_store_mut(worker).denylist; + assert!(table::contains(denylist, sender), EWORKER_NOT_ON_DENYLIST); + table::remove(denylist, sender); + } + + public(friend) fun is_on_allowlist(worker: address, sender: address): bool acquires WorkerStore { + table::contains(&worker_store(worker).allowlist, sender) + } + + public(friend) fun is_on_denylist(worker: address, sender: address): bool acquires WorkerStore { + table::contains(&worker_store(worker).denylist, sender) + } + + public(friend) fun has_allowlist(worker: address): bool acquires WorkerStore { + worker_store(worker).allowlist_count > 0 + } + + // ==================================================== Helpers =================================================== + + inline fun worker_store(worker: address): &WorkerStore { borrow_global(worker) } + + inline fun worker_store_mut(worker: address): &mut WorkerStore { borrow_global_mut(worker) } + + // ==================================================Error Codes ================================================== + + const EADMIN_ALREADY_EXISTS: u64 = 1; + const EADMIN_NOT_FOUND: u64 = 2; + const EATTEMPING_TO_REMOVE_ONLY_ADMIN: u64 = 3; + const EDVN_DST_EID_NOT_CONFIGURED: u64 = 4; + const EEXECUTOR_DST_EID_NOT_CONFIGURED: u64 = 5; + const ENOT_DELEGATING: u64 = 6; + const ENO_ADMINS_PROVIDED: u64 = 7; + const ENO_PRICE_FEED_CONFIGURED: u64 = 8; + const EROLE_ADMIN_ALREADY_EXISTS: u64 = 9; + const EROLE_ADMIN_NOT_FOUND: u64 = 10; + const EUNCHANGED: u64 = 11; + const EWORKER_ALREADY_INITIALIZED: u64 = 12; + const EWORKER_ALREADY_ON_ALLOWLIST: u64 = 13; + const EWORKER_ALREADY_ON_DENYLIST: u64 = 14; + const EWORKER_NOT_ON_ALLOWLIST: u64 = 15; + const EWORKER_NOT_ON_DENYLIST: u64 = 16; + const EWORKER_NOT_REGISTERED: u64 = 17; +} diff --git a/packages/layerzero-v2/aptos/contracts/worker_peripherals/worker_common/sources/multisig.move b/packages/layerzero-v2/aptos/contracts/worker_peripherals/worker_common/sources/multisig.move new file mode 100644 index 00000000..bdebbeb3 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/worker_peripherals/worker_common/sources/multisig.move @@ -0,0 +1,147 @@ +// This multisig module was developed for use with DVN workers. It is used to manage the quorum and signers for +// a worker and to verify that the required number of signatures are present and valid when a change is made. It +// keeps track of used hashes to prevent the same command being replayed (all hashes should be hashed with an expiration +// time, which allows two of the otherwise same command to be called at different times). +// The important place where the configuration is used beyond direct multisig activity is that the fee_lib_0 depends on +// the quorum number set here to compute the send fee for the DVN, which scale with the quorum of signers required. +module worker_common::multisig { + use std::event::emit; + use std::signer::address_of; + + use endpoint_v2_common::bytes32::{Bytes32, to_bytes32}; + use endpoint_v2_common::contract_identity::{ + CallRef, + get_call_ref_caller, + }; + use worker_common::multisig_store; + + struct WorkerMultisigTarget {} + + /// Initialize the worker signing / multisig store + /// The signers provided are the public keys of the signers that are allowed to sign for the worker + /// A quorum of signatures is required to sign for a transaction to succeed + public fun initialize_for_worker(account: &signer, quorum: u64, signers: vector>) { + multisig_store::initialize_for_worker(account); + let worker = address_of(move account); + multisig_store::set_multisig_signers(worker, signers); + emit(SetSigners { worker, multisig_signers: signers }); + multisig_store::set_quorum(worker, quorum); + emit(SetQuorum { worker, quorum }); + } + + #[test_only] + /// Initialize the worker signing / multisig store for testing purposes + public fun initialize_for_worker_test_only(worker: address, quorum: u64, signers: vector>) { + let account = &std::account::create_signer_for_test(worker); + initialize_for_worker(account, quorum, signers); + } + + /// Assert that all signatures are valid, it is not expired, and add the hash to the history or abort if it is + /// present. This will abort if the hash has already been used, expired, or the signatures are invalid + /// *Important*: The `hash` must be the result of a hash operation to be secure. The caller should ensure that the + /// expiry time provided is a component of the hash provided + public fun assert_all_and_add_to_history( + call_ref: &CallRef, + signatures: &vector, + expiration: u64, + hash: Bytes32, + ) { + let worker = get_call_ref_caller(call_ref); + multisig_store::assert_initialized(worker); + multisig_store::assert_all_and_add_to_history(worker, signatures, expiration, hash); + } + + /// Set the quorum required for a worker + public fun set_quorum(call_ref: &CallRef, quorum: u64) { + let worker = get_call_ref_caller(call_ref); + multisig_store::assert_initialized(worker); + multisig_store::set_quorum(worker, quorum); + emit(SetQuorum { worker, quorum }); + } + + /// Set the signers (public keys) for a worker + public fun set_signers(call_ref: &CallRef, multisig_signers: vector>) { + let worker = get_call_ref_caller(call_ref); + multisig_store::assert_initialized(worker); + multisig_store::set_multisig_signers(worker, multisig_signers); + emit(SetSigners { worker, multisig_signers }); + } + + /// Set a signer (public key) for a worker + /// `active` param refers to whether the signer should be added (true) or deleted (false) + public fun set_signer(call_ref: &CallRef, multisig_signer: vector, active: bool) { + let worker = get_call_ref_caller(call_ref); + multisig_store::assert_initialized(worker); + if (active) { + multisig_store::add_multisig_signer(worker, multisig_signer); + } else { + multisig_store::remove_multisig_signer(worker, multisig_signer); + }; + emit(UpdateSigner { worker, multisig_signer, active }); + } + + #[view] + /// Get the quorum required for a worker + public fun get_quorum(worker: address): u64 { + multisig_store::assert_initialized(worker); + multisig_store::get_quorum(worker) + } + + #[view] + /// Get the signers (public keys) for a worker + public fun get_signers(worker: address): vector> { + multisig_store::assert_initialized(worker); + multisig_store::get_multisig_signers(worker) + } + + #[view] + /// Check if a signer (public key) is a signer for a worker + public fun is_signer(worker: address, multisig_signer: vector): bool { + multisig_store::assert_initialized(worker); + multisig_store::is_multisig_signer(worker, multisig_signer) + } + + #[view] + /// Check if a specific hash has already been used to submit a transaction + public fun was_hash_used(worker: address, hash: vector): bool { + multisig_store::assert_initialized(worker); + multisig_store::was_hash_used(worker, to_bytes32(hash)) + } + + + // ==================================================== Events ==================================================== + + #[event] + /// Emitted when the quorum is set for a worker + struct SetQuorum has store, drop { worker: address, quorum: u64 } + + #[event] + /// Emitted when the signers are set for a worker + struct SetSigners has store, drop { worker: address, multisig_signers: vector> } + + #[event] + /// Emitted when a signer is added or removed for a worker + struct UpdateSigner has store, drop { + worker: address, + multisig_signer: vector, + active: bool, + } + + #[test_only] + /// Generates a SetQuorum event for testing + public fun set_quorum_event(worker: address, quorum: u64): SetQuorum { + SetQuorum { worker, quorum } + } + + #[test_only] + /// Generates a SetSigners event for testing + public fun set_signers_event(worker: address, multisig_signers: vector>): SetSigners { + SetSigners { worker, multisig_signers } + } + + #[test_only] + /// Generates an UpdateSigner event for testing + public fun update_signer_event(worker: address, multisig_signer: vector, active: bool): UpdateSigner { + UpdateSigner { worker, multisig_signer, active } + } +} diff --git a/packages/layerzero-v2/aptos/contracts/worker_peripherals/worker_common/sources/worker_config.move b/packages/layerzero-v2/aptos/contracts/worker_peripherals/worker_common/sources/worker_config.move new file mode 100644 index 00000000..e797e0d2 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/worker_peripherals/worker_common/sources/worker_config.move @@ -0,0 +1,753 @@ +/// Worker Config is a shared module that provides configuration for workers +module worker_common::worker_config { + use std::account; + use std::event::emit; + use std::fungible_asset::{Self, Metadata}; + use std::math128::pow; + use std::object::address_to_object; + + use endpoint_v2_common::contract_identity::{ + CallRef, + get_call_ref_caller, + }; + use worker_common::worker_config_store; + + #[test_only] + use std::account::create_signer_for_test; + + #[test_only] + friend worker_common::worker_config_tests; + + // Worker Ids + public inline fun WORKER_ID_EXECUTOR(): u8 { 1 } + + public inline fun WORKER_ID_DVN(): u8 { 2 } + + // ================================================ Initialization ================================================ + + #[test_only] + /// Initialize the worker for testing purposes (does not require a signer, and accepts fewer params) + public fun initialize_for_worker_test_only( + worker: address, + worker_id: u8, + deposit_address: address, + role_admin: address, + admins: vector
, + supported_msglibs: vector
, + fee_lib: address, + ) { + let account = &create_signer_for_test(worker); + worker_config_store::initialize_store_for_worker( + account, + worker_id, + deposit_address, + role_admin, + admins, + supported_msglibs, + fee_lib, + ); + } + + /// Initialize the worker - this must be called an can be called most once + public fun initialize_for_worker( + account: &signer, + worker_id: u8, + deposit_address: address, + role_admin: address, + admins: vector
, + supported_msglibs: vector
, + fee_lib: address, + ) { + assert!(account::exists_at(deposit_address), ENOT_AN_ACCOUNT); + worker_config_store::initialize_store_for_worker( + account, + worker_id, + deposit_address, + role_admin, + admins, + supported_msglibs, + fee_lib, + ); + } + + #[view] + // Check whether the worker is initialized + public fun is_worker_initialized(worker: address): bool { + worker_config_store::is_worker_initialized(worker) + } + + /// Assert that the fee lib supports a transaction + /// This checks that the worker is initialized, unpaused, the sender is allowed, that the msglib is supported, and + /// that the worker id matches the expected worker id + public fun assert_fee_lib_supports_transaction(worker: address, worker_id: u8, sender: address, msglib: address) { + worker_config_store::assert_initialized(worker); + assert_worker_unpaused(worker); + assert_allowed(worker, sender); + assert_supported_msglib(worker, msglib); + assert!(worker_config_store::get_worker_id(worker) == worker_id, EUNEXPECTED_WORKER_ID); + } + + #[view] + /// Get the worker id for the given worker address + public fun get_worker_id_from_worker_address(worker: address): u8 { + worker_config_store::assert_initialized(worker); + worker_config_store::get_worker_id(worker) + } + + #[view] + /// Get the worker's price feed and feed address + public fun get_worker_price_feed_config(worker: address): (address, address) { + worker_config_store::assert_initialized(worker); + worker_config_store::get_price_feed(worker) + } + + // ==================================================== Admins ==================================================== + + struct WorkerAdminsTarget {} + + /// Set the admin status for the given address using the worker's CallRef + public fun set_worker_admin( + call_ref: &CallRef, + admin: address, + active: bool, + ) { + let worker = get_call_ref_caller(call_ref); + worker_config_store::assert_initialized(worker); + if (active) { + worker_config_store::add_admin(worker, admin); + } else { + worker_config_store::remove_admin(worker, admin); + }; + emit(SetWorkerAdmin { worker, admin, active }); + } + + #[view] + /// Check if the given address is an admin for the worker + public fun is_worker_admin(worker: address, admin: address): bool { + worker_config_store::assert_initialized(worker); + worker_config_store::is_admin(worker, admin) + } + + #[view] + /// Get a list of the worker's admins + public fun get_worker_admins(worker: address): vector
{ + worker_config_store::assert_initialized(worker); + worker_config_store::get_admins(worker) + } + + /// Assert that the given address is an admin for the worker + public fun assert_worker_admin(worker: address, admin: address) { + worker_config_store::assert_initialized(worker); + assert!(worker_config_store::is_admin(worker, admin), EUNAUTHORIZED); + } + + /// Set a role admin for the worker using the worker's CallRef + public fun set_worker_role_admin( + call_ref: &CallRef, + role_admin: address, + active: bool, + ) { + let worker = get_call_ref_caller(call_ref); + worker_config_store::assert_initialized(worker); + if (active) { + worker_config_store::add_role_admin(worker, role_admin); + } else { + worker_config_store::remove_role_admin(worker, role_admin); + }; + emit(SetWorkerRoleAdmin { worker, role_admin, active }); + } + + #[view] + /// Check if the given address is a role admin for the worker + public fun is_worker_role_admin(worker: address, role_admin: address): bool { + worker_config_store::assert_initialized(worker); + worker_config_store::is_role_admin(worker, role_admin) + } + + #[view] + /// Get a list of the role admins for the worker + public fun get_worker_role_admins(worker: address): vector
{ + worker_config_store::assert_initialized(worker); + worker_config_store::get_role_admins(worker) + } + + /// Assert that the given address is a role admin for the worker + public fun assert_worker_role_admin(worker: address, role_admin: address) { + worker_config_store::assert_initialized(worker); + assert!(worker_config_store::is_role_admin(worker, role_admin), EUNAUTHORIZED); + } + + // ==================================================== Pausing =================================================== + + struct WorkerPauseTarget {} + + /// Pauses the worker on the send side - will cause get_fee() functions to abort + public fun set_worker_pause( + call_ref: &CallRef, + paused: bool, + ) { + let worker = get_call_ref_caller(call_ref); + let previous_status = worker_config_store::is_paused(worker); + assert!(previous_status != paused, EPAUSE_STATUS_UNCHANGED); + worker_config_store::set_pause_status(worker, paused); + if (paused) { + emit(Paused { worker }); + } else { + emit(Unpaused { worker }); + } + } + + /// Assert that the worker is not paused + public fun assert_worker_unpaused(worker: address) { + assert!(!is_worker_paused(worker), EWORKER_PAUSED); + } + + #[view] + /// Check if the worker is paused + public fun is_worker_paused(worker: address): bool { + worker_config_store::assert_initialized(worker); + worker_config_store::is_paused(worker) + } + + + // =============================================== Message Libraries ============================================== + + struct WorkerMsgLibsTarget {} + + /// Set the supported message libraries for the worker + public fun set_supported_msglibs( + call_ref: &CallRef, + msglibs: vector
, + ) { + let worker = get_call_ref_caller(call_ref); + worker_config_store::set_supported_msglibs(worker, msglibs); + emit(SetSupportedMsglibs { worker, msglibs }); + } + + #[view] + /// Get the supported message libraries for the worker + public fun get_supported_msglibs(worker: address): vector
{ + worker_config_store::assert_initialized(worker); + worker_config_store::get_supported_msglibs(worker) + } + + /// Assert that the worker supports the given message library + public fun assert_supported_msglib(worker: address, msglib: address) { + assert!( + worker_config_store::is_supported_msglib(worker, msglib), + EWORKER_AUTH_UNSUPPORTED_MSGLIB, + ); + } + + // ================================================ Deposit Address =============================================== + + struct WorkerDepositAddressTarget {} + + /// Set the deposit address for the worker + public fun set_deposit_address(call_ref: &CallRef, deposit_address: address) { + let worker = get_call_ref_caller(call_ref); + assert!(account::exists_at(deposit_address), ENOT_AN_ACCOUNT); + worker_config_store::set_deposit_address(worker, deposit_address); + emit(SetDepositAddress { worker, deposit_address }); + } + + #[view] + /// Get the deposit address for the worker + public fun get_deposit_address(worker: address): address { + worker_config_store::assert_initialized(worker); + worker_config_store::get_deposit_address(worker) + } + + // ================================================== Price Feed ================================================== + + struct WorkerPriceFeedTarget {} + + /// Set the price feed module and price feed for the worker + public fun set_price_feed( + call_ref: &CallRef, + price_feed: address, + feed_address: address, + ) { + let worker = get_call_ref_caller(call_ref); + worker_config_store::set_price_feed(worker, price_feed, feed_address); + emit(SetPriceFeed { + worker, + price_feed, + feed_address, + }); + } + + #[view] + /// Get the effective price feed module and price feed for the worker, providing the delegated price feed details + /// if the worker has delegated the price feed; otherwise it provides what is directly configured for the worker + public fun get_effective_price_feed(worker: address): (address, address) { + worker_config_store::assert_initialized(worker); + if (worker_config_store::has_price_feed_delegate(worker)) { + let delegate = worker_config_store::get_price_feed_delegate(worker); + assert!(worker_config_store::has_price_feed(delegate), EWORKER_PRICE_FEED_DELEGATE_NOT_CONFIGURED); + worker_config_store::get_price_feed(delegate) + } else if (worker_config_store::has_price_feed(worker)) { + worker_config_store::get_price_feed(worker) + } else { + abort EWORKER_PRICE_FEED_NOT_CONFIGURED + } + } + + /// Sets a price feed delegate for the worker. This is another worker's address that has a price feed configured. + /// If the delegate is set to @0x0, the delegate is unset. When a worker has delegated to another worker, it will + /// use whatever is configured for the delegate worker when a fee is calculated + public fun set_price_feed_delegate(call_ref: &CallRef, delegate: address) { + let worker = get_call_ref_caller(call_ref); + if (delegate == @0x0) { + // Unset + assert!(worker_config_store::has_price_feed_delegate(worker), ENO_DELEGATE_TO_UNSET); + worker_config_store::unset_price_feed_delegate(worker); + } else { + // Set + worker_config_store::assert_initialized(delegate); + let (price_feed, feed_address) = worker_config_store::get_price_feed(delegate); + assert!(price_feed != @0x0, EDELEGATE_PRICE_FEED_NOT_CONFIGURED); + assert!(feed_address != @0x0, EDELEGATE_FEED_ADDRESS_NOT_CONFIGURED); + worker_config_store::set_price_feed_delegate(worker, delegate); + }; + emit(SetPriceFeedDelegate { worker, delegate }); + } + + #[view] + /// Get the price feed delegate for the worker + /// This will return @0x0 if the worker does not have a price feed delegate + public fun get_price_feed_delegate(worker: address): address { + worker_config_store::assert_initialized(worker); + if (!worker_config_store::has_price_feed_delegate(worker)) { + @0x0 + } else { + worker_config_store::get_price_feed_delegate(worker) + } + } + + #[view] + /// Get the count of other workers delegating to a worker for the price feed configuration + public fun get_count_price_feed_delegate_dependents(worker: address): u64 { + worker_config_store::get_count_price_feed_delegate_dependents(worker) + } + + // ============================================ Fee Libs Worker Config ============================================ + + struct WorkerFeeLibTarget {} + + /// Set the fee lib used for the worker + public fun set_worker_fee_lib(call_ref: &CallRef, fee_lib: address) { + let worker = get_call_ref_caller(call_ref); + worker_config_store::set_fee_lib(worker, fee_lib); + emit(WorkerFeeLibUpdated { worker, fee_lib }); + } + + #[view] + /// Get the fee lib for the worker + public fun get_worker_fee_lib(worker: address): address { + worker_config_store::assert_initialized(worker); + worker_config_store::get_fee_lib(worker) + } + + /// Set the default basis-points multiplier (for premium calculation) for the worker + public fun set_default_multiplier_bps(call_ref: &CallRef, default_multiplier_bps: u16) { + let worker = get_call_ref_caller(call_ref); + worker_config_store::assert_initialized(worker); + worker_config_store::set_default_multiplier_bps(worker, default_multiplier_bps); + emit(SetMultiplierBps { worker, default_multiplier_bps }); + } + + #[view] + /// Get the default basis-points multiplier for the worker + public fun get_default_multiplier_bps(worker: address): u16 { + worker_config_store::assert_initialized(worker); + worker_config_store::get_default_multiplier_bps(worker) + } + + /// Set the supported option types for the worker + public fun set_supported_option_types(call_ref: &CallRef, option_types: vector) { + let worker = get_call_ref_caller(call_ref); + worker_config_store::assert_initialized(worker); + worker_config_store::set_supported_option_types(worker, option_types); + emit(SetSupportedOptionTypes { worker, option_types }); + } + + #[view] + /// Get the supported option types for the worker + public fun get_supported_option_types(worker: address): vector { + worker_config_store::assert_initialized(worker); + worker_config_store::get_supported_option_types(worker) + } + + #[view] + /// Get the native decimals rate for the gas token on this chain + public fun get_native_decimals_rate(): u128 { + let decimals = fungible_asset::decimals(address_to_object(@native_token_metadata_address)); + pow(10, (decimals as u128)) + } + + // ================================================ Executor Config =============================================== + + struct WorkerExecutorTarget {} + + /// Set the executor destination config for the worker + /// @param call_ref The CallRef for the worker (should be addressed to @worker_common) + /// @param dst_eid The destination EID + /// @param lz_receive_base_gas The base gas for receiving a message + /// @param multiplier_bps The multiplier in basis points + /// @param floor_margin_usd The floor margin in USD + /// @param native_cap The native cap + /// @param lz_compose_base_gas The base gas for composing a message + public fun set_executor_dst_config( + call_ref: &CallRef, + dst_eid: u32, + lz_receive_base_gas: u64, + multiplier_bps: u16, + floor_margin_usd: u128, + native_cap: u128, + lz_compose_base_gas: u64, + ) { + let worker = get_call_ref_caller(call_ref); + worker_config_store::assert_initialized(worker); + worker_config_store::set_executor_dst_config( + worker, + dst_eid, + lz_receive_base_gas, + multiplier_bps, + floor_margin_usd, + native_cap, + lz_compose_base_gas, + ); + emit(SetExecutorDstConfig { + worker, + dst_eid, + lz_receive_base_gas, + multiplier_bps, + floor_margin_usd, + native_cap, + lz_compose_base_gas, + }); + } + + #[view] + /// Get the executor destination config for the worker + /// @return (lz_receive_base_gas, multiplier_bps, floor_margin_usd, native_cap, lz_compose_base_gas) + public fun get_executor_dst_config_values( + worker: address, + dst_eid: u32, + ): (u64, u16, u128, u128, u64) { + worker_config_store::assert_initialized(worker); + worker_config_store::get_executor_dst_config_values(worker, dst_eid) + } + + // ================================================== DVN Config ================================================== + + struct WorkerDvnTarget {} + + /// Set the DVN destination config for the worker + /// @param call_ref The CallRef for the worker (should be addressed to @worker_common) + /// @param dst_eid The destination EID + /// @param gas The gas + /// @param multiplier_bps The multiplier in basis points + /// @param floor_margin_usd The floor margin in USD + public fun set_dvn_dst_config( + call_ref: &CallRef, + dst_eid: u32, + gas: u64, + multiplier_bps: u16, + floor_margin_usd: u128, + ) { + let worker = get_call_ref_caller(call_ref); + worker_config_store::assert_initialized(worker); + worker_config_store::set_dvn_dst_config( + worker, + dst_eid, + gas, + multiplier_bps, + floor_margin_usd, + ); + emit(SetDvnDstConfig { + worker, + dst_eid, + gas, + multiplier_bps, + floor_margin_usd, + }); + } + + #[view] + /// Get the DVN destination config for the worker and destination EID + /// @return (gas, multiplier_bps, floor_margin_usd) + public fun get_dvn_dst_config_values( + worker: address, + dst_eid: u32, + ): (u64, u16, u128) { + worker_config_store::assert_initialized(worker); + worker_config_store::get_dvn_dst_config_values(worker, dst_eid) + } + + // ====================================================== ACL ===================================================== + + struct WorkerAclTarget {} + + /// Add or remove a sender from the worker allowlist + /// If the allowlist is empty, any sender, except those on the denylist, are allowed + /// Once there is at least one sender on the allowlist, only those on the allowlist are allowed, minus any that are + /// also on the denylist + public fun set_allowlist( + call_ref: &CallRef, + sender: address, + allowed: bool, + ) { + let worker = get_call_ref_caller(call_ref); + if (allowed) { + worker_config_store::add_to_allowlist(worker, sender); + } else { + worker_config_store::remove_from_allowlist(worker, sender); + }; + emit(SetAllowList { worker, sender, allowed }); + } + + /// Add or remove a sender from the worker denylist + /// Any sender on the denylist will not be allowed, regardless of whether they are also on the allowlist + public fun set_denylist(call_ref: &CallRef, sender: address, denied: bool) { + let worker = get_call_ref_caller(call_ref); + if (denied) { + worker_config_store::add_to_denylist(worker, sender); + } else { + worker_config_store::remove_from_denylist(worker, sender); + }; + emit(SetDenyList { worker, sender, denied }); + } + + #[view] + /// Check if a sender is allowed to use the worker based on the allowlist and denylist configuration + public fun is_allowed(worker: address, sender: address): bool { + if (worker_config_store::is_on_denylist(worker, sender)) { + false + } else if (worker_config_store::is_on_allowlist(worker, sender)) { + true + } else { + // if there is no allow list, an unlisted sender is allowed, otherwise they must be on the allow list + !worker_config_store::has_allowlist(worker) + } + } + + #[view] + /// Check if a sender is on the worker allowlist + public fun allowlist_contains(worker: address, sender: address): bool { + worker_config_store::is_on_allowlist(worker, sender) + } + + #[view] + /// Check if a sender is on the worker denylist + public fun denylist_contains(worker: address, sender: address): bool { + worker_config_store::is_on_denylist(worker, sender) + } + + /// Assert that the sender is allowed to use the worker + public fun assert_allowed(worker: address, sender: address) { + assert!(is_allowed(worker, sender), ESENDER_DENIED); + } + + // ==================================================== Events ==================================================== + + #[event] + /// Event emitted when the worker admin status is set + struct SetWorkerAdmin has store, drop { worker: address, admin: address, active: bool } + + #[event] + /// Event emitted when the worker role admin status is set + struct SetWorkerRoleAdmin has store, drop { worker: address, role_admin: address, active: bool } + + #[event] + /// Event emitted when the worker deposit address is set + struct SetDepositAddress has store, drop { worker: address, deposit_address: address } + + #[event] + /// Event emitted when the worker is paused + struct Paused has store, drop { worker: address } + + #[event] + /// Event emitted when the worker is unpaused + struct Unpaused has store, drop { worker: address } + + #[event] + /// Event emitted when the worker supported message libraries are set + struct SetSupportedMsglibs has store, drop { worker: address, msglibs: vector
} + + #[event] + /// Event emitted when the worker price feed is set + struct SetPriceFeed has store, drop { worker: address, price_feed: address, feed_address: address } + + #[event] + /// Event emitted when the worker price feed delegate is set + struct SetPriceFeedDelegate has store, drop { worker: address, delegate: address } + + #[event] + /// Event emitted when the worker default multiplier is set + struct SetMultiplierBps has store, drop { worker: address, default_multiplier_bps: u16 } + + #[event] + /// Event emitted when the worker supported option types are set + struct SetSupportedOptionTypes has store, drop { worker: address, option_types: vector } + + #[event] + /// Event emitted when the worker executor destination config is set + struct SetExecutorDstConfig has store, drop { + worker: address, + dst_eid: u32, + lz_receive_base_gas: u64, + multiplier_bps: u16, + floor_margin_usd: u128, + native_cap: u128, + lz_compose_base_gas: u64, + } + + #[event] + /// Event emitted when the worker DVN destination config is set + struct SetDvnDstConfig has store, drop { + worker: address, + dst_eid: u32, + gas: u64, + multiplier_bps: u16, + floor_margin_usd: u128, + } + + #[event] + /// Event emitted when worker adds/removes an oapp sender to allowlist + /// allowed = false means the sender is removed from the allowlist + struct SetAllowList has store, drop { worker: address, sender: address, allowed: bool } + + #[event] + /// Event emitted when the worker DVN destination config is set + /// denied = false means the sender is removed from the denylist + struct SetDenyList has store, drop { worker: address, sender: address, denied: bool } + + #[event] + /// Event emitted when the worker fee lib is set + struct WorkerFeeLibUpdated has store, drop { worker: address, fee_lib: address } + + // ============================================== Event Test Helpers ============================================== + + #[test_only] + /// Generate a SetWorkerAdmin event for testing + public fun set_worker_admin_event(worker: address, admin: address, active: bool): SetWorkerAdmin { + SetWorkerAdmin { worker, admin, active } + } + + #[test_only] + /// Generate a SetWorkerRoleAdmin event for testing + public fun set_worker_role_admin_event(worker: address, role_admin: address, active: bool): SetWorkerRoleAdmin { + SetWorkerRoleAdmin { worker, role_admin, active } + } + + #[test_only] + /// Generate a SetDepositAddress event for testing + public fun set_deposit_address_event(worker: address, deposit_address: address): SetDepositAddress { + SetDepositAddress { worker, deposit_address } + } + + #[test_only] + /// Generate a Paused event for testing + public fun paused_event(worker: address): Paused { + Paused { worker } + } + + #[test_only] + /// Generate a Unpaused event for testing + public fun unpaused_event(worker: address): Unpaused { + Unpaused { worker } + } + + #[test_only] + /// Generate a SetSupportedMsglibs event for testing + public fun set_supported_msglibs_event(worker: address, msglibs: vector
): SetSupportedMsglibs { + SetSupportedMsglibs { worker, msglibs } + } + + #[test_only] + /// Generate a SetPriceFeed event for testing + public fun set_price_feed_event(worker: address, price_feed: address, feed_address: address): SetPriceFeed { + SetPriceFeed { worker, price_feed, feed_address } + } + + #[test_only] + /// Generate a SetPriceFeedDelegate event for testing + public fun set_price_feed_delegate_event(worker: address, delegate: address): SetPriceFeedDelegate { + SetPriceFeedDelegate { worker, delegate } + } + + #[test_only] + /// Generate a SetMultiplierBps event for testing + public fun set_multiplier_bps_event(worker: address, default_multiplier_bps: u16): SetMultiplierBps { + SetMultiplierBps { worker, default_multiplier_bps } + } + + #[test_only] + /// Generate a SetSupportedOptionTypes event for testing + public fun set_supported_option_types_event(worker: address, option_types: vector): SetSupportedOptionTypes { + SetSupportedOptionTypes { worker, option_types } + } + + #[test_only] + /// Generate a SetExecutorDstConfig event for testing + public fun set_executor_dst_config_event( + worker: address, + dst_eid: u32, + lz_receive_base_gas: u64, + multiplier_bps: u16, + floor_margin_usd: u128, + native_cap: u128, + lz_compose_base_gas: u64, + ): SetExecutorDstConfig { + SetExecutorDstConfig { + worker, + dst_eid, + lz_receive_base_gas, + multiplier_bps, + floor_margin_usd, + native_cap, + lz_compose_base_gas, + } + } + + #[test_only] + /// Generate a SetDvnDstConfig event for testing + public fun set_dvn_dst_config_event( + worker: address, + dst_eid: u32, + gas: u64, + multiplier_bps: u16, + floor_margin_usd: u128, + ): SetDvnDstConfig { + SetDvnDstConfig { + worker, + dst_eid, + gas, + multiplier_bps, + floor_margin_usd, + } + } + + #[test_only] + /// Generate a WorkerFeeLibUpdated event for testing + public fun worker_fee_lib_updated_event(worker: address, fee_lib: address): WorkerFeeLibUpdated { + WorkerFeeLibUpdated { worker, fee_lib } + } + + // ================================================== Error Codes ================================================= + + const EDELEGATE_FEED_ADDRESS_NOT_CONFIGURED: u64 = 1; + const EDELEGATE_PRICE_FEED_NOT_CONFIGURED: u64 = 2; + const ENOT_AN_ACCOUNT: u64 = 3; + const ENO_DELEGATE_TO_UNSET: u64 = 4; + const EPAUSE_STATUS_UNCHANGED: u64 = 5; + const ESENDER_DENIED: u64 = 6; + const EUNAUTHORIZED: u64 = 7; + const EUNEXPECTED_WORKER_ID: u64 = 8; + const EWORKER_AUTH_UNSUPPORTED_MSGLIB: u64 = 9; + const EWORKER_PAUSED: u64 = 10; + const EWORKER_PRICE_FEED_DELEGATE_NOT_CONFIGURED: u64 = 11; + const EWORKER_PRICE_FEED_NOT_CONFIGURED: u64 = 12; +} diff --git a/packages/layerzero-v2/aptos/contracts/worker_peripherals/worker_common/tests/internal/signing_store_tests.move b/packages/layerzero-v2/aptos/contracts/worker_peripherals/worker_common/tests/internal/signing_store_tests.move new file mode 100644 index 00000000..6795fd8c --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/worker_peripherals/worker_common/tests/internal/signing_store_tests.move @@ -0,0 +1,142 @@ +#[test_only] +module worker_common::signing_store_tests { + use std::account::create_signer_for_test; + use std::vector; + + use endpoint_v2_common::bytes32::Bytes32; + use endpoint_v2_common::serde::{Self, flatten}; + use worker_common::multisig_store::{ + assert_not_expired, + assert_signatures_verified_internal, + get_pubkey, + split_signatures, + }; + + const DVN_WORKER_ID: u8 = 2; + + #[test] + public fun test_assert_not_expired() { + let native_framework = &create_signer_for_test(@std); + std::timestamp::set_time_has_started_for_testing(native_framework); + std::timestamp::update_global_time_for_test_secs(1000); + + assert_not_expired(1000000); + assert_not_expired(1500); + assert_not_expired(1001); + } + + #[test] + #[expected_failure(abort_code = worker_common::multisig_store::EEXPIRED_SIGNATURE)] + public fun test_assert_not_expired_fails_if_expired() { + let native_framework = &create_signer_for_test(@std); + std::timestamp::set_time_has_started_for_testing(native_framework); + std::timestamp::update_global_time_for_test_secs(1000); + + assert_not_expired(1000); + } + + #[test] + fun test_get_pubkey() { + let expiration = 1677465966; + let expected_pubkey = x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a"; + let signature_with_recovery = x"ee6c9646b2c55672734f06acb7347548f605046adcdf9ff080287ed0699779f6246c167cd30c6630cf5aa2cb157398b7b74237b18415cbd2e66bc0b2bff08f1a00"; + let quorum = 2; + let hash = create_set_quorum_hash(quorum, expiration); + let generated_pubkey = get_pubkey(&signature_with_recovery, hash); + assert!(generated_pubkey == expected_pubkey, 0); + } + + + #[test] + fun test_assert_signatures_verified_internal() { + let expiration = 1677465966; + let pubkey1 = x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a"; + let pubkey2 = x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d"; + let pubkey3 = x"37bdab42a45e9d6cc56f7d0cc7897e871a0357bce3f0f4c99c93c54291b259a29a92111167a25ae188ef49b2f3df880d8aae8522e29cb6c299745258a200cfff"; + let dvn_signers = &mut vector>[pubkey1, pubkey2, pubkey3]; + + let signature1 = x"ee6c9646b2c55672734f06acb7347548f605046adcdf9ff080287ed0699779f6246c167cd30c6630cf5aa2cb157398b7b74237b18415cbd2e66bc0b2bff08f1a00"; + let signature2 = x"b3d37d05832d808934f88e3f53ff2002e71125031543806964c0b1537f3abb694593f3b6b49f87caa1ca96e0cb955c24cb0865be8fc331b44a0afaf95480031f01"; + let signature3 = x"535e6f18117f1940ba3afef15d72dfc28cd0ab88ffa6d276c04c9d639f744352224f1c5a52046ef3962d444d15c6462dfe123665586e704b74704be22bcc8e1c00"; + let signatures = vector[signature1, signature2, signature3]; + + let quorum = 2; + let hash = create_set_quorum_hash(quorum, expiration); + assert_signatures_verified_internal(&signatures, hash, dvn_signers, quorum); + } + + #[test] + #[expected_failure(abort_code = worker_common::multisig_store::EDVN_LESS_THAN_QUORUM)] + fun test_assert_signatures_verified_internal_fails_if_quorum_not_met() { + let expiration = 1677465966; + let pubkey1 = x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a"; + let pubkey2 = x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d"; + let pubkey3 = x"37bdab42a45e9d6cc56f7d0cc7897e871a0357bce3f0f4c99c93c54291b259a29a92111167a25ae188ef49b2f3df880d8aae8522e29cb6c299745258a200cfff"; + let dvn_signers = &mut vector[pubkey1, pubkey2, pubkey3]; + + let signature1 = x"ee6c9646b2c55672734f06acb7347548f605046adcdf9ff080287ed0699779f6246c167cd30c6630cf5aa2cb157398b7b74237b18415cbd2e66bc0b2bff08f1a00"; + let signatures = vector[signature1]; + + let quorum = 2; + let hash = create_set_quorum_hash(quorum, expiration); + assert_signatures_verified_internal(&signatures, hash, dvn_signers, quorum); + } + + #[test] + #[expected_failure(abort_code = worker_common::multisig_store::EDVN_INCORRECT_SIGNATURE)] + fun test_assert_signatures_verified_internal_fails_if_any_signature_is_invalid() { + let expiration = 1677465966; + let pubkey1 = x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a"; + let pubkey2 = x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d"; + let pubkey3 = x"37bdab42a45e9d6cc56f7d0cc7897e871a0357bce3f0f4c99c93c54291b259a29a92111167a25ae188ef49b2f3df880d8aae8522e29cb6c299745258a200cfff"; + let dvn_signers = &mut vector[pubkey1, pubkey2, pubkey3]; + + let signature1 = x"ee6c9646b2c55672734f06acb7347548f605046adcdf9ff080287ed0699779f6246c167cd30c6630cf5aa2cb157398b7b74237b18415cbd2e66bc0b2bff08f1a00"; + let signature2 = x"aaaa7d05832d808934f88e3f53ff2002e71125031543806964c0b1537f3abb694593f3b6b49f87caa1ca96e0cb955c24cb0865be8fc331b44a0afaf95480031f01"; // invalid + let signature3 = x"535e6f18117f1940ba3afef15d72dfc28cd0ab88ffa6d276c04c9d639f744352224f1c5a52046ef3962d444d15c6462dfe123665586e704b74704be22bcc8e1c00"; + let signatures = vector[signature1, signature2, signature3]; + + let quorum = 2; + let hash = create_set_quorum_hash(quorum, expiration); + assert_signatures_verified_internal(&signatures, hash, dvn_signers, quorum); + } + + #[test] + fun test_split_and_join_signatures() { + let signature1 = x"ee6c9646b2c55672734f06acb7347548f605046adcdf9ff080287ed0699779f6246c167cd30c6630cf5aa2cb157398b7b74237b18415cbd2e66bc0b2bff08f1a00"; + let signature2 = x"b3d37d05832d808934f88e3f53ff2002e71125031543806964c0b1537f3abb694593f3b6b49f87caa1ca96e0cb955c24cb0865be8fc331b44a0afaf95480031f01"; + let signature3 = x"535e6f18117f1940ba3afef15d72dfc28cd0ab88ffa6d276c04c9d639f744352224f1c5a52046ef3962d444d15c6462dfe123665586e704b74704be22bcc8e1c00"; + let joined = flatten(vector[signature1, signature2, signature3]); + + let expected = vector[signature1, signature2, signature3]; + assert!(split_signatures(&joined) == expected, 0); + } + + + #[test] + #[expected_failure(abort_code = worker_common::multisig_store::EINVALID_SIGNATURE_LENGTH)] + fun test_split_signatures_fails_if_invalid_length() { + // 64 bytes instead of 65 + let signatures = x"ee6c9646b2c55672734f06acb7347548f605046adcdf9ff080287ed0699779f6246c167cd30c6630cf5aa2cb157398b7b74237b18415cbd2e66bc0b2bff08f1a"; + split_signatures(&signatures); + } + + + public fun create_set_quorum_hash(quorum: u64, expiration: u64): Bytes32 { + endpoint_v2_common::bytes32::keccak256( + build_set_quorum_payload(quorum, expiration) + ) + } + + fun build_set_quorum_payload(quorum: u64, expiration: u64): vector { + let payload = vector[]; + serde::append_bytes(&mut payload, get_function_signature(b"set_quorum")); + serde::append_u64(&mut payload, quorum); + serde::append_u64(&mut payload, expiration); + payload + } + + fun get_function_signature(function_name: vector): vector { + vector::slice(&std::aptos_hash::keccak256(std::bcs::to_bytes(&function_name)), 0, 4) + } +} diff --git a/packages/layerzero-v2/aptos/contracts/worker_peripherals/worker_common/tests/internal/worker_config_store_tests.move b/packages/layerzero-v2/aptos/contracts/worker_peripherals/worker_common/tests/internal/worker_config_store_tests.move new file mode 100644 index 00000000..b4416c87 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/worker_peripherals/worker_common/tests/internal/worker_config_store_tests.move @@ -0,0 +1,37 @@ +#[test_only] +module worker_common::worker_config_store_tests { + use std::account::create_signer_for_test; + + use worker_common::worker_config_store; + + #[test] + #[expected_failure(abort_code = worker_common::worker_config_store::ENO_ADMINS_PROVIDED)] + fun test_initialize_for_worker_fails_if_no_admins_provided() { + let account = &create_signer_for_test(@0x1111); + worker_config_store::initialize_store_for_worker(account, 1, @0x1111, @0xdefad, vector[], vector[], @0xfee11b); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config_store::EWORKER_ALREADY_INITIALIZED)] + fun test_initialize_for_worker_fails_if_already_initialized() { + let account = &create_signer_for_test(@0x1111); + worker_config_store::initialize_store_for_worker( + account, + 1, + @0xdefad, + @0x1111, + vector[@100], + vector[], + @0xfee11b, + ); + worker_config_store::initialize_store_for_worker( + account, + 1, + @0xdefad, + @0x1111, + vector[@200], + vector[], + @0xfee11b, + ); + } +} diff --git a/packages/layerzero-v2/aptos/contracts/worker_peripherals/worker_common/tests/multisig_tests.move b/packages/layerzero-v2/aptos/contracts/worker_peripherals/worker_common/tests/multisig_tests.move new file mode 100644 index 00000000..c2751750 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/worker_peripherals/worker_common/tests/multisig_tests.move @@ -0,0 +1,246 @@ +#[test_only] +module worker_common::multisig_tests { + use std::event::was_event_emitted; + + use endpoint_v2_common::contract_identity::make_call_ref_for_test; + use worker_common::multisig::{ + get_quorum, + is_signer, + set_quorum, + set_signer, + set_signers, + }; + use worker_common::multisig::{ + initialize_for_worker_test_only as initialize_multisig, set_quorum_event, set_signers_event, + update_signer_event, + }; + use worker_common::worker_config::{initialize_for_worker_test_only as initialize_worker, WORKER_ID_DVN}; + + const WORKER: address = @123456; + + #[test] + fun test_set_and_get_quorum() { + initialize_worker(WORKER, WORKER_ID_DVN(), WORKER, @0x501ead, vector[@123], vector[], @0xfee11b); + + let signers = vector[ + x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a", + x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d" + ]; + initialize_multisig(WORKER, 1, signers); + assert!(was_event_emitted(&set_signers_event(WORKER, signers)), 0); + assert!(was_event_emitted(&set_quorum_event(WORKER, 1)), 0); + assert!(get_quorum(WORKER) == 1, 0); + set_quorum(&make_call_ref_for_test(WORKER), 2); + assert!(was_event_emitted(&set_quorum_event(WORKER, 2)), 0); + assert!(get_quorum(WORKER) == 2, 0); + } + + #[test] + #[expected_failure(abort_code = worker_common::multisig_store::ESIGNERS_LESS_THAN_QUORUM)] + fun test_initialize_fails_if_signers_less_than_quorum() { + initialize_worker(WORKER, WORKER_ID_DVN(), WORKER, @0x501ead, vector[@123], vector[], @0xfee11b); + + let signers = vector[ + x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a", + x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d" + ]; + let quorum = 3; + initialize_multisig(WORKER, quorum, signers); + } + + #[test] + #[expected_failure(abort_code = worker_common::multisig_store::ESIGNERS_LESS_THAN_QUORUM)] + fun test_set_quorum_fails_if_signers_less_than_quorum() { + initialize_worker(WORKER, WORKER_ID_DVN(), WORKER, @0x501ead, vector[@123], vector[], @0xfee11b); + + let signers = vector[ + x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a", + x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d" + ]; + let quorum = 1; + initialize_multisig(WORKER, quorum, signers); + set_quorum(&make_call_ref_for_test(WORKER), 3); + } + + #[test] + #[expected_failure(abort_code = worker_common::multisig_store::EZERO_QUORUM)] + fun test_set_quorum_fails_if_zero() { + initialize_worker(WORKER, WORKER_ID_DVN(), WORKER, @0x501ead, vector[@123], vector[], @0xfee11b); + + let signers = vector[ + x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a", + x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d" + ]; + let quorum = 1; + initialize_multisig(WORKER, quorum, signers); + set_quorum(&make_call_ref_for_test(WORKER), 0); + } + + #[test] + fun test_set_signers() { + initialize_worker(WORKER, WORKER_ID_DVN(), WORKER, @0x501ead, vector[@123], vector[], @0xfee11b); + let signers = vector[ + x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a", + ]; + initialize_multisig(WORKER, 1, signers); + assert!(was_event_emitted(&set_signers_event(WORKER, signers)), 0); + assert!(was_event_emitted(&set_quorum_event(WORKER, 1)), 0); + let signers = vector[ + x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a", + x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d" + ]; + set_signers(&make_call_ref_for_test(WORKER), signers); + assert!(was_event_emitted(&set_signers_event(WORKER, signers)), 0); + assert!( + is_signer( + WORKER, + x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a" + ), + 0, + ); + assert!( + is_signer( + WORKER, + x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d" + ), + 1, + ); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2_common::assert_no_duplicates::EDUPLICATE_ITEM)] + fun test_initialize_multisig_fails_if_duplicate_item() { + initialize_worker(WORKER, WORKER_ID_DVN(), WORKER, @0x501ead, vector[@123], vector[], @0xfee11b); + let signers = vector[ + x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a", + x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d", + x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a", + ]; + initialize_multisig(WORKER, 1, signers); + } + + #[test] + #[expected_failure(abort_code = worker_common::multisig_store::EINVALID_SIGNER_LENGTH)] + fun test_initialize_fails_if_invalid_length() { + initialize_worker(WORKER, WORKER_ID_DVN(), WORKER, @0x501ead, vector[@123], vector[], @0xfee11b); + let signers = vector[ + x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a", + x"1234567890", // invalid + x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d", + ]; + initialize_multisig(WORKER, 1, signers); + } + + #[test] + fun test_set_signer() { + initialize_worker(WORKER, WORKER_ID_DVN(), WORKER, @0x501ead, vector[@123], vector[], @0xfee11b); + let signers = vector[ + x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a", + x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d" + ]; + initialize_multisig(WORKER, 1, signers); + + + // turn second signer on and off + assert!( + is_signer( + WORKER, + x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d" + ), + 0, + ); + set_signer( + &make_call_ref_for_test(WORKER), + x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d", + false, + ); + assert!( + was_event_emitted( + &update_signer_event( + WORKER, + x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d", + false, + ) + ), + 0, + ); + assert!( + !is_signer( + WORKER, + x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d" + ), + 0, + ); + set_signer( + &make_call_ref_for_test(WORKER), + x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d", + true, + ); + assert!( + was_event_emitted( + &update_signer_event( + WORKER, + x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d", + true, + ) + ), + 1, + ); + assert!( + is_signer( + WORKER, + x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d" + ), + 0, + ); + } + + #[test] + #[expected_failure(abort_code = worker_common::multisig_store::ESIGNERS_LESS_THAN_QUORUM)] + fun test_set_signer_fails_if_less_than_quorum() { + initialize_worker(WORKER, WORKER_ID_DVN(), WORKER, @0x501ead, vector[@123], vector[], @0xfee11b); + let signers = vector[ + x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a", + x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d" + ]; + initialize_multisig(WORKER, 2, signers); + // try to turn second signer off: fails + assert!( + is_signer( + WORKER, + x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d" + ), + 0, + ); + set_signer( + &make_call_ref_for_test(WORKER), + x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d", + false, + ); + } + + #[test] + #[expected_failure(abort_code = worker_common::multisig_store::EINVALID_SIGNER_LENGTH)] + fun test_set_signer_fails_if_incorrect_length() { + initialize_worker(WORKER, WORKER_ID_DVN(), WORKER, @0x501ead, vector[@123], vector[], @0xfee11b); + let signers = vector[ + x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a", + x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d" + ]; + initialize_multisig(WORKER, 2, signers); + set_signer(&make_call_ref_for_test(WORKER), x"1234567890", true); + } + + #[test] + #[expected_failure(abort_code = worker_common::multisig_store::ESIGNER_ALREADY_EXISTS)] + fun test_set_signer_fails_if_assigned_twice() { + initialize_worker(WORKER, WORKER_ID_DVN(), WORKER, @0x501ead, vector[@123], vector[], @0xfee11b); + let signers = vector[ + x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a", + ]; + initialize_multisig(WORKER, 1, signers); + let signer = x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d"; + set_signer(&make_call_ref_for_test(WORKER), signer, true); + set_signer(&make_call_ref_for_test(WORKER), signer, true); + } +} diff --git a/packages/layerzero-v2/aptos/contracts/worker_peripherals/worker_common/tests/worker_config_tests.move b/packages/layerzero-v2/aptos/contracts/worker_peripherals/worker_common/tests/worker_config_tests.move new file mode 100644 index 00000000..8261bbc5 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/worker_peripherals/worker_common/tests/worker_config_tests.move @@ -0,0 +1,807 @@ +#[test_only] +module worker_common::worker_config_tests { + use std::account; + use std::event::was_event_emitted; + use std::vector; + + use endpoint_v2_common::contract_identity::make_call_ref_for_test; + use worker_common::worker_config::{ + Self, + allowlist_contains, + assert_allowed, + assert_supported_msglib, + denylist_contains, + is_allowed, + set_allowlist, + set_denylist, + WORKER_ID_EXECUTOR, + }; + + const WORKER: address = @123456; + const DVN_WORKER_ID: u8 = 2; + + #[test] + fun test_set_worker_pause() { + let worker_address = @3001; + worker_common::worker_config::initialize_for_worker_test_only( + worker_address, + WORKER_ID_EXECUTOR(), + worker_address, + @0x501ead, + vector[@111], + vector[], + @0xfee11b1, + ); + // default state is unpaused + assert!(!worker_config::is_worker_paused(worker_address), 0); + + assert!(!worker_config::is_worker_paused(worker_address), 0); + worker_config::set_worker_pause(&make_call_ref_for_test(worker_address), true); + assert!(was_event_emitted(&worker_config::paused_event(worker_address)), 0); + assert!(!was_event_emitted(&worker_config::unpaused_event(worker_address)), 0); + assert!(worker_config::is_worker_paused(worker_address), 0); + + worker_config::set_worker_pause(&make_call_ref_for_test(worker_address), false); + assert!(was_event_emitted(&worker_config::unpaused_event(worker_address)), 0); + assert!(!worker_config::is_worker_paused(worker_address), 0); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::EPAUSE_STATUS_UNCHANGED)] + fun test_set_pause_fails_if_no_state_change() { + let worker_address = @3001; + worker_common::worker_config::initialize_for_worker_test_only( + worker_address, + WORKER_ID_EXECUTOR(), + worker_address, + @0x501ead, + vector[@111], + vector[], + @0xfee11b1, + ); + worker_config::set_worker_pause(&make_call_ref_for_test(worker_address), true); + // fails on state change to the prior value + worker_config::set_worker_pause(&make_call_ref_for_test(worker_address), true); + } + + #[test] + fun test_set_and_get_supported_msglibs() { + let worker_address = @3001; + let msglib1 = @1234; + let msglib2 = @2345; + let msglib3 = @3456; + worker_common::worker_config::initialize_for_worker_test_only( + worker_address, + WORKER_ID_EXECUTOR(), + worker_address, + @0x501ead, + vector[@111], + vector[msglib1, msglib2], + @0xfee11b1, + ); + + let supported_msglibs = worker_config::get_supported_msglibs(worker_address); + assert!(vector::contains(&supported_msglibs, &msglib1), 0); + assert!(vector::contains(&supported_msglibs, &msglib2), 0); + assert!(!vector::contains(&supported_msglibs, &msglib3), 0); + assert_supported_msglib(worker_address, msglib1); + assert_supported_msglib(worker_address, msglib2); + + worker_config::set_supported_msglibs(&make_call_ref_for_test(worker_address), vector[msglib1, msglib3]); + let supported_msglibs = worker_config::get_supported_msglibs(worker_address); + assert!(vector::contains(&supported_msglibs, &msglib1), 0); + assert!(!vector::contains(&supported_msglibs, &msglib2), 0); + assert!(vector::contains(&supported_msglibs, &msglib3), 0); + assert_supported_msglib(worker_address, msglib1); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::EWORKER_AUTH_UNSUPPORTED_MSGLIB)] + fun test_assert_supported_msglib_fails_if_not_supported() { + let worker_address = @3001; + let msglib1 = @1234; + let msglib2 = @2345; + worker_common::worker_config::initialize_for_worker_test_only( + worker_address, + WORKER_ID_EXECUTOR(), + worker_address, + @0x501ead, + vector[@111], + vector[msglib1], + @0xfee11b1, + ); + assert_supported_msglib(worker_address, msglib2); + } + + #[test] + fun test_get_and_set_deposit_address() { + let worker_address = @3001; + let deposit_address = @1234; + account::create_account_for_test(deposit_address); + + worker_common::worker_config::initialize_for_worker_test_only( + worker_address, + WORKER_ID_EXECUTOR(), + worker_address, + @0x501ead, + vector[@111], + vector[], + @0xfee11b1, + ); + // initializes to worker address + assert!(worker_config::get_deposit_address(worker_address) == worker_address, 0); + + worker_config::set_deposit_address(&make_call_ref_for_test(worker_address), deposit_address); + assert!(was_event_emitted(&worker_config::set_deposit_address_event(worker_address, deposit_address)), 0); + let deposit_address_result = worker_config::get_deposit_address(worker_address); + assert!(deposit_address == deposit_address_result, 0); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::ENOT_AN_ACCOUNT)] + fun set_deposit_address_fails_if_invalid_account() { + let worker_address = @3001; + let deposit_address = @1234; + worker_common::worker_config::initialize_for_worker_test_only( + worker_address, + WORKER_ID_EXECUTOR(), + worker_address, + @0x501ead, + vector[@111], + vector[], + @0xfee11b1, + ); + + // Attempt to set deposit address to an invalid account (expected failure) + worker_config::set_deposit_address(&make_call_ref_for_test(worker_address), deposit_address); + } + + #[test] + fun test_set_and_get_price_feed() { + let worker_address = @3001; + let price_feed = @1234; + let feed_address = @2345; + worker_common::worker_config::initialize_for_worker_test_only( + worker_address, + WORKER_ID_EXECUTOR(), + worker_address, + @0x501ead, + vector[@111], + vector[], + @0xfee11b1, + ); + + worker_config::set_price_feed(&make_call_ref_for_test(worker_address), price_feed, feed_address); + assert!(was_event_emitted(&worker_config::set_price_feed_event(worker_address, price_feed, feed_address)), 0); + let (price_feed_result, feed_address_result) = worker_config::get_effective_price_feed(worker_address); + assert!(price_feed == price_feed_result, 0); + assert!(feed_address == feed_address_result, 0); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::EWORKER_PRICE_FEED_NOT_CONFIGURED)] + fun test_get_effective_price_feed_fails_if_not_configured() { + let worker_address = @3001; + worker_common::worker_config::initialize_for_worker_test_only( + worker_address, + WORKER_ID_EXECUTOR(), + worker_address, + @0x501ead, + vector[@111], + vector[], + @0xfee11b1, + ); + + worker_config::get_effective_price_feed(worker_address); + } + + + #[test] + fun test_set_and_get_price_feed_delegate() { + // register worker + let first_worker_address = @3001; + worker_common::worker_config::initialize_for_worker_test_only( + first_worker_address, + WORKER_ID_EXECUTOR(), + first_worker_address, + @0x501ead, + vector[@111], + vector[], + @0xfee11b1, + ); + + // register delegate worker + let second_worker_address = @3002; + worker_common::worker_config::initialize_for_worker_test_only( + second_worker_address, + WORKER_ID_EXECUTOR(), + second_worker_address, + @0x501ead, + vector[@111], + vector[], + @0xfee11b1, + ); + + // register third worker + let third_worker_address = @3003; + worker_common::worker_config::initialize_for_worker_test_only( + third_worker_address, + WORKER_ID_EXECUTOR(), + third_worker_address, + @0x501ead, + vector[@111], + vector[], + @0xfee11b1, + ); + + // set price feed for second worker + worker_config::set_price_feed(&make_call_ref_for_test(second_worker_address), @10002, @20002); + assert!(was_event_emitted(&worker_config::set_price_feed_event(second_worker_address, @10002, @20002)), 0); + let (price_feed_result, feed_address_result) = worker_config::get_effective_price_feed(second_worker_address); + assert!(price_feed_result == @10002, 0); + assert!(feed_address_result == @20002, 0); + + // delegate for the first worker not yet set + let delegate_result = worker_config::get_price_feed_delegate(first_worker_address); + assert!(delegate_result == @0x0, 0); + let delegate_count = worker_config::get_count_price_feed_delegate_dependents(second_worker_address); + assert!(delegate_count == 0, 0); + + // set delegate for first worker + worker_config::set_price_feed_delegate(&make_call_ref_for_test(first_worker_address), second_worker_address); + assert!( + was_event_emitted( + &worker_config::set_price_feed_delegate_event(first_worker_address, second_worker_address) + ), + 0, + ); + let delegate_result = worker_config::get_price_feed_delegate(first_worker_address); + assert!(delegate_result == second_worker_address, 0); + let delegate_count = worker_config::get_count_price_feed_delegate_dependents(second_worker_address); + assert!(delegate_count == 1, 0); + + let (price_feed_result, feed_address_result) = worker_config::get_effective_price_feed(first_worker_address); + assert!(price_feed_result == @10002, 0); + assert!(feed_address_result == @20002, 0); + + // set the price feed for the first worker (should not override the delegate configuration) + worker_config::set_price_feed(&make_call_ref_for_test(first_worker_address), @10001, @20001); + assert!(was_event_emitted(&worker_config::set_price_feed_event(first_worker_address, @10001, @20001)), 0); + let (price_feed_result, feed_address_result) = worker_config::get_effective_price_feed(first_worker_address); + assert!(price_feed_result == @10002, 0); + assert!(feed_address_result == @20002, 0); + + // have the third worker delegate to the first worker + // the effective price feed should be the price feed of the first worker, not the price feed of the first + // worker's delegate (no delegate chaining) + worker_config::set_price_feed_delegate(&make_call_ref_for_test(third_worker_address), first_worker_address); + assert!( + was_event_emitted( + &worker_config::set_price_feed_delegate_event(third_worker_address, first_worker_address) + ), + 0, + ); + let delegate_result = worker_config::get_price_feed_delegate(third_worker_address); + assert!(delegate_result == first_worker_address, 0); + + let first_worker_delegate_count = worker_config::get_count_price_feed_delegate_dependents(first_worker_address); + assert!(first_worker_delegate_count == 1, 0); + let second_worker_delegate_count = worker_config::get_count_price_feed_delegate_dependents( + second_worker_address, + ); + assert!(second_worker_delegate_count == 1, 0); + let third_worker_delegate_count = worker_config::get_count_price_feed_delegate_dependents(third_worker_address); + assert!(third_worker_delegate_count == 0, 0); + + let (price_feed_result, feed_address_result) = worker_config::get_effective_price_feed(third_worker_address); + assert!(price_feed_result == @10001, 0); + assert!(feed_address_result == @20001, 0); + + // Set the third worker to delegate to the second worker (which will then have 2 delegates) + worker_config::set_price_feed_delegate(&make_call_ref_for_test(third_worker_address), second_worker_address); + assert!( + was_event_emitted( + &worker_config::set_price_feed_delegate_event(third_worker_address, second_worker_address) + ), + 0, + ); + let delegate_result = worker_config::get_price_feed_delegate(third_worker_address); + assert!(delegate_result == second_worker_address, 0); + + let first_worker_delegate_count = worker_config::get_count_price_feed_delegate_dependents(first_worker_address); + assert!(first_worker_delegate_count == 0, 0); + let second_worker_delegate_count = worker_config::get_count_price_feed_delegate_dependents( + second_worker_address, + ); + assert!(second_worker_delegate_count == 2, 0); + let third_worker_delegate_count = worker_config::get_count_price_feed_delegate_dependents(third_worker_address); + assert!(third_worker_delegate_count == 0, 0); + + worker_config::set_price_feed(&make_call_ref_for_test(third_worker_address), @10003, @20003); + + // swap delegate of first to point to third (instead of second) + worker_config::set_price_feed_delegate(&make_call_ref_for_test(first_worker_address), third_worker_address); + assert!( + was_event_emitted( + &worker_config::set_price_feed_delegate_event(first_worker_address, third_worker_address) + ), + 0, + ); + let first_worker_delegate_count = worker_config::get_count_price_feed_delegate_dependents(first_worker_address); + assert!(first_worker_delegate_count == 0, 0); + let second_worker_delegate_count = worker_config::get_count_price_feed_delegate_dependents( + second_worker_address, + ); + assert!(second_worker_delegate_count == 1, 0); + let third_worker_delegate_count = worker_config::get_count_price_feed_delegate_dependents(third_worker_address); + assert!(third_worker_delegate_count == 1, 0); + + // remove the delegate + worker_config::set_price_feed_delegate(&make_call_ref_for_test(first_worker_address), @0x0); + assert!(was_event_emitted(&worker_config::set_price_feed_delegate_event(first_worker_address, @0x0)), 0); + let delegate_result = worker_config::get_price_feed_delegate(first_worker_address); + assert!(delegate_result == @0x0, 0); + + let first_worker_delegate_count = worker_config::get_count_price_feed_delegate_dependents(first_worker_address); + assert!(first_worker_delegate_count == 0, 0); + let second_worker_delegate_count = worker_config::get_count_price_feed_delegate_dependents( + second_worker_address, + ); + assert!(second_worker_delegate_count == 1, 0); + let third_worker_delegate_count = worker_config::get_count_price_feed_delegate_dependents(third_worker_address); + assert!(third_worker_delegate_count == 0, 0); + + // the effective price feed should be the price feed of the delegate worker should be unaffected + let (price_feed_result, feed_address_result) = worker_config::get_effective_price_feed(second_worker_address); + assert!(price_feed_result == @10002, 0); + assert!(feed_address_result == @20002, 0); + + // the effective price feed should be the price feed of the worker should revert to its own + let (price_feed_result, feed_address_result) = worker_config::get_effective_price_feed(first_worker_address); + assert!(price_feed_result == @10001, 0); + assert!(feed_address_result == @20001, 0); + + // the effective price feed should be the price feed of the send (delegate) worker should be unaffected + let (price_feed_result, feed_address_result) = worker_config::get_effective_price_feed(third_worker_address); + assert!(price_feed_result == @10002, 0); + assert!(feed_address_result == @20002, 0); + } + + + #[test] + fun test_is_worker_admin() { + let admins = vector[@100, @200, @300]; + worker_config::initialize_for_worker_test_only(@0x1111, 1, @0x1111, @0x501ead, admins, vector[], @0xfee11b); + assert!(worker_config::is_worker_admin(@0x1111, @100), 0); + assert!(worker_config::is_worker_admin(@0x1111, @200), 1); + assert!(worker_config::is_worker_admin(@0x1111, @300), 2); + + // Does not approve non-admin + assert!(!worker_config::is_worker_admin(@0x1111, @11), 3); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config_store::EWORKER_NOT_REGISTERED)] + fun test_is_worker_admin_fails_if_worker_not_initialized() { + // Initialize one workerworker_config_store + let admins = vector[@100, @200, @300]; + worker_config::initialize_for_worker_test_only(@0x1111, 1, @0x1111, @0x501ead, admins, vector[], @0xfee11b); + + // Attempt to check admin status of a different worker (expected failure) + worker_config::is_worker_admin(@1112, @100); + } + + #[test] + fun test_assert_worker_admin_succeeds_if_is_admin() { + let admins = vector[@100, @200, @300]; + worker_config::initialize_for_worker_test_only(@0x1111, 1, @0x1111, @0x501ead, admins, vector[], @0xfee11b); + worker_config::assert_worker_admin(@0x1111, @100); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::EUNAUTHORIZED)] + fun test_assert_worker_admin_fails_if_not_admin() { + let admins = vector[@100, @200, @300]; + worker_config::initialize_for_worker_test_only(@0x1111, 1, @0x1111, @0x501ead, admins, vector[], @0xfee11b); + worker_config::assert_worker_admin(@0x1111, @150); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config_store::EWORKER_NOT_REGISTERED)] + fun test_asset_worker_admin_fails_if_worker_not_registered() { + let admins = vector[@100, @200, @300]; + worker_config::initialize_for_worker_test_only(@0x1111, 1, @0x1111, @0x501ead, admins, vector[], @0xfee11b); + + // different worker + worker_config::assert_worker_admin(@2222, @100); + } + + #[test] + fun test_set_worker_admin_internal() { + let admins = vector[@100, @200, @300]; + worker_config::initialize_for_worker_test_only(@0x1111, 1, @0x1111, @0x501ead, admins, vector[], @0xfee11b); + + // Add new admin + worker_config::set_worker_admin(&make_call_ref_for_test(@0x1111), @400, true); + assert!(was_event_emitted(&worker_config::set_worker_admin_event(@0x1111, @400, true)), 0); + assert!(worker_config::is_worker_admin(@0x1111, @100), 0); + assert!(worker_config::is_worker_admin(@0x1111, @400), 0); + assert!(!worker_config::is_worker_admin(@0x1111, @500), 1); + + // Remove admin + worker_config::set_worker_admin(&make_call_ref_for_test(@0x1111), @400, false); + assert!(was_event_emitted(&worker_config::set_worker_admin_event(@0x1111, @400, false)), 0); + assert!(worker_config::is_worker_admin(@0x1111, @100), 0); + assert!(!worker_config::is_worker_admin(@0x1111, @400), 1); + assert!(!worker_config::is_worker_admin(@0x1111, @500), 1); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config_store::EADMIN_ALREADY_EXISTS)] + fun test_set_worker_admin_internal_fails_if_admin_already_exists() { + let admins = vector[@100, @200, @300]; + worker_config::initialize_for_worker_test_only(@0x1111, 1, @0x1111, @0x501ead, admins, vector[], @0xfee11b); + + // Add new admin + worker_config::set_worker_admin(&make_call_ref_for_test(@0x1111), @400, true); + assert!(worker_config::is_worker_admin(@0x1111, @400), 0); + + // Attempt to add the same admin again (expected failure) + worker_config::set_worker_admin(&make_call_ref_for_test(@0x1111), @400, true); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config_store::EADMIN_NOT_FOUND)] + fun test_set_worker_admin_internal_fails_to_remove_an_admin_if_admin_not_found() { + let admins = vector[@100, @200, @300]; + worker_config::initialize_for_worker_test_only(@0x1111, 1, @0x1111, @0x501ead, admins, vector[], @0xfee11b); + + // Attempt to remove non-existent admin (expected failure) + worker_config::set_worker_admin(&make_call_ref_for_test(@0x1111), @400, false); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config_store::EATTEMPING_TO_REMOVE_ONLY_ADMIN)] + fun test_set_worker_admin_internal_fails_to_remove_last_admin() { + let admins = vector[@100]; + worker_config::initialize_for_worker_test_only(@0x1111, 1, @0x1111, @0x501ead, admins, vector[], @0xfee11b); + + // Attempt to remove last admin (expected failure) + worker_config::set_worker_admin(&make_call_ref_for_test(@0x1111), @100, false); + } + + #[test] + fun test_set_worker_admin() { + let admins = vector[@100, @200, @300]; + worker_config::initialize_for_worker_test_only(@0x1111, 1, @0x1111, @0x501ead, admins, vector[], @0xfee11b); + assert!(worker_config::is_worker_admin(@0x1111, @100), 0); + assert!(!worker_config::is_worker_admin(@0x1111, @400), 0); + + // Add new admin + worker_config::set_worker_admin(&make_call_ref_for_test(@0x1111), @400, true); + assert!(worker_config::is_worker_admin(@0x1111, @100), 0); + assert!(worker_config::is_worker_admin(@0x1111, @400), 0); + assert!(!worker_config::is_worker_admin(@0x1111, @500), 1); + + // Remove admins + worker_config::set_worker_admin(&make_call_ref_for_test(@0x1111), @400, false); + worker_config::set_worker_admin(&make_call_ref_for_test(@0x1111), @100, false); + assert!(!worker_config::is_worker_admin(@0x1111, @100), 0); + assert!(!worker_config::is_worker_admin(@0x1111, @400), 1); + assert!(!worker_config::is_worker_admin(@0x1111, @500), 1); + + // Re-add admin + worker_config::set_worker_admin(&make_call_ref_for_test(@0x1111), @400, true); + assert!(!worker_config::is_worker_admin(@0x1111, @100), 0); + assert!(worker_config::is_worker_admin(@0x1111, @400), 0); + assert!(!worker_config::is_worker_admin(@0x1111, @500), 1); + } + + #[test] + fun test_set_worker_admin_with_call_ref() { + let admins = vector[@100, @200, @300]; + worker_config::initialize_for_worker_test_only(@0x1111, 1, @0x1111, @0x501ead, admins, vector[], @0xfee11b); + assert!(worker_config::is_worker_admin(@0x1111, @100), 0); + assert!(!worker_config::is_worker_admin(@0x1111, @400), 0); + + // Add new admin + worker_config::set_worker_admin(&make_call_ref_for_test(@0x1111), @400, true); + assert!(worker_config::is_worker_admin(@0x1111, @100), 0); + assert!(worker_config::is_worker_admin(@0x1111, @400), 0); + assert!(!worker_config::is_worker_admin(@0x1111, @500), 1); + + // Remove admins + worker_config::set_worker_admin(&make_call_ref_for_test(@0x1111), @400, false); + worker_config::set_worker_admin(&make_call_ref_for_test(@0x1111), @100, false); + assert!(!worker_config::is_worker_admin(@0x1111, @100), 0); + assert!(!worker_config::is_worker_admin(@0x1111, @400), 1); + assert!(!worker_config::is_worker_admin(@0x1111, @500), 1); + + // Readd admin + worker_config::set_worker_admin(&make_call_ref_for_test(@0x1111), @400, true); + assert!(!worker_config::is_worker_admin(@0x1111, @100), 0); + assert!(worker_config::is_worker_admin(@0x1111, @400), 0); + assert!(!worker_config::is_worker_admin(@0x1111, @500), 1); + } + + #[test] + fun test_set_worker_role_admin() { + let admins = vector[@100, @200, @300]; + worker_config::initialize_for_worker_test_only(@0x1111, 1, @0x1111, @0x501ead, admins, vector[], @0xfee11b); + assert!(!worker_config::is_worker_role_admin(@0x1111, @100), 0); + assert!(worker_config::is_worker_role_admin(@0x1111, @0x501ead), 0); + + // Add new admin + worker_config::set_worker_role_admin(&make_call_ref_for_test(@0x1111), @400, true); + assert!(worker_config::is_worker_role_admin(@0x1111, @400), 0); + assert!(worker_config::is_worker_role_admin(@0x1111, @0x501ead), 0); + + // Remove admins + worker_config::set_worker_role_admin(&make_call_ref_for_test(@0x1111), @400, false); + worker_config::set_worker_role_admin(&make_call_ref_for_test(@0x1111), @0x501ead, false); + assert!(!worker_config::is_worker_role_admin(@0x1111, @400), 0); + assert!(!worker_config::is_worker_role_admin(@0x1111, @0x501ead), 0); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config_store::EROLE_ADMIN_ALREADY_EXISTS)] + fun test_set_worker_role_admin_fails_if_admin_already_exists() { + let admins = vector[@100, @200, @300]; + worker_config::initialize_for_worker_test_only(@0x1111, 1, @0x1111, @0x501ead, admins, vector[], @0xfee11b); + + // Add new admin + worker_config::set_worker_role_admin(&make_call_ref_for_test(@0x1111), @0x501ead, true); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config_store::EROLE_ADMIN_NOT_FOUND)] + fun test_set_worker_role_admin_fails_to_remove_an_admin_if_admin_not_found() { + let admins = vector[@100, @200, @300]; + worker_config::initialize_for_worker_test_only(@0x1111, 1, @0x1111, @0x501ead, admins, vector[], @0xfee11b); + + // Attempt to remove non-existent admin (expected failure) + worker_config::set_worker_role_admin(&make_call_ref_for_test(@0x1111), @400, false); + } + + #[test] + fun test_set_allowlist() { + let worker_address = @3001; + worker_common::worker_config::initialize_for_worker_test_only( + worker_address, + WORKER_ID_EXECUTOR(), + worker_address, + @0x501ead, + vector[@1234, @2345], + vector[], + @0xfee11b1, + ); + let alice = @1122; + let bob = @3344; + let carol = @5566; + + // add alice and bob to the allow list + set_allowlist(&make_call_ref_for_test(worker_address), alice, true); + set_allowlist(&make_call_ref_for_test(worker_address), bob, true); + assert!(allowlist_contains(worker_address, alice), 0); + assert!(allowlist_contains(worker_address, bob), 0); + assert!(!allowlist_contains(worker_address, carol), 0); + + // remove alice from the allow list + set_allowlist(&make_call_ref_for_test(worker_address), alice, false); + assert!(!allowlist_contains(worker_address, alice), 0); + assert!(allowlist_contains(worker_address, bob), 0); + assert!(!allowlist_contains(worker_address, carol), 0); + } + + #[test] + fun test_set_denylist() { + let worker_address = @3001; + worker_common::worker_config::initialize_for_worker_test_only( + worker_address, + WORKER_ID_EXECUTOR(), + worker_address, + @0x501ead, + vector[@1234, @2345], + vector[], + @0xfee11b1, + ); + let alice = @1122; + let bob = @3344; + let carol = @5566; + + // add alice and bob to the deny list + set_denylist(&make_call_ref_for_test(worker_address), alice, true); + set_denylist(&make_call_ref_for_test(worker_address), bob, true); + assert!(denylist_contains(worker_address, alice), 0); + assert!(denylist_contains(worker_address, bob), 0); + assert!(!denylist_contains(worker_address, carol), 0); + + // remove alice from the deny list + set_denylist(&make_call_ref_for_test(worker_address), alice, false); + assert!(!denylist_contains(worker_address, alice), 0); + assert!(denylist_contains(worker_address, bob), 0); + assert!(!denylist_contains(worker_address, carol), 0); + } + + #[test] + fun test_is_allowed() { + let worker_address = @3001; + worker_common::worker_config::initialize_for_worker_test_only( + worker_address, + WORKER_ID_EXECUTOR(), + worker_address, + @0x501ead, + vector[@1234, @2345], + vector[], + @0xfee11b1, + ); + let alice = @1122; + let bob = @3344; + let carol = @5566; + + + // add carol to the deny list, then assert that alice and bob are allowed + set_denylist(&make_call_ref_for_test(worker_address), carol, true); + assert_allowed(worker_address, alice); + assert_allowed(worker_address, bob); + assert!(!is_allowed(worker_address, carol), 0); + + // add alice to the allow list, then assert that alice is allowed and bob is not + set_allowlist(&make_call_ref_for_test(worker_address), alice, true); + assert_allowed(worker_address, alice); + assert!(!is_allowed(worker_address, bob), 0); + + // add bob to the allow list, then assert that alice and bob are allowed + set_allowlist(&make_call_ref_for_test(worker_address), bob, true); + assert_allowed(worker_address, alice); + assert_allowed(worker_address, bob); + + // add bob to the deny list, then assert that bob is not allowed even though he was on allow list + set_denylist(&make_call_ref_for_test(worker_address), bob, true); + assert_allowed(worker_address, alice); + assert!(!is_allowed(worker_address, bob), 0); + assert!(!is_allowed(worker_address, carol), 0); + + // remove all from lists, then assert that all are allowed + set_allowlist(&make_call_ref_for_test(worker_address), alice, false); + set_allowlist(&make_call_ref_for_test(worker_address), bob, false); + set_denylist(&make_call_ref_for_test(worker_address), bob, false); + set_denylist(&make_call_ref_for_test(worker_address), carol, false); + assert_allowed(worker_address, alice); + assert_allowed(worker_address, bob); + assert_allowed(worker_address, carol); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::ESENDER_DENIED)] + fun test_assert_allowed_fails_if_denied() { + let worker_address = @3001; + worker_common::worker_config::initialize_for_worker_test_only( + worker_address, + WORKER_ID_EXECUTOR(), + worker_address, + @0x501ead, + vector[@1234, @2345], + vector[], + @0xfee11b1, + ); + let carol = @5566; + + // add carol to the deny list, then assert that alice and bob are allowed + set_denylist(&make_call_ref_for_test(worker_address), carol, true); + assert_allowed(worker_address, carol); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::ESENDER_DENIED)] + fun test_assert_allowed_fails_if_not_in_an_existing_allowlist() { + let worker_address = @3001; + worker_common::worker_config::initialize_for_worker_test_only( + worker_address, + WORKER_ID_EXECUTOR(), + worker_address, + @0x501ead, + vector[@1234, @2345], + vector[], + @0xfee11b1, + ); + let alice = @1122; + let bob = @3344; + let carol = @5566; + set_denylist(&make_call_ref_for_test(worker_address), bob, true); + set_allowlist(&make_call_ref_for_test(worker_address), carol, true); + + // Since an allowlist exists, Alice must be on the allowlist to be authorized + assert_allowed(worker_address, alice); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config_store::EWORKER_ALREADY_ON_ALLOWLIST)] + fun test_set_allowlist_fails_if_already_on_allowlist() { + let worker_address = @3001; + worker_common::worker_config::initialize_for_worker_test_only( + worker_address, + WORKER_ID_EXECUTOR(), + worker_address, + @0x501ead, + vector[@1234, @2345], + vector[], + @0xfee11b1, + ); + let alice = @1122; + set_allowlist(&make_call_ref_for_test(worker_address), alice, true); + set_allowlist(&make_call_ref_for_test(worker_address), alice, true); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config_store::EWORKER_ALREADY_ON_DENYLIST)] + fun test_set_denylist_fails_if_already_on_denylist() { + let worker_address = @3001; + worker_common::worker_config::initialize_for_worker_test_only( + worker_address, + WORKER_ID_EXECUTOR(), + worker_address, + @0x501ead, + vector[@1234, @2345], + vector[], + @0xfee11b1, + ); + let alice = @1122; + set_denylist(&make_call_ref_for_test(worker_address), alice, true); + set_denylist(&make_call_ref_for_test(worker_address), alice, true); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config_store::EWORKER_NOT_ON_ALLOWLIST)] + fun test_set_allowlist_fails_if_not_on_allowlist() { + let worker_address = @3001; + worker_common::worker_config::initialize_for_worker_test_only( + worker_address, + WORKER_ID_EXECUTOR(), + worker_address, + @0x501ead, + vector[@1234, @2345], + vector[], + @0xfee11b1, + ); + let alice = @1122; + set_allowlist(&make_call_ref_for_test(worker_address), alice, false); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config_store::EWORKER_NOT_ON_DENYLIST)] + fun test_set_denylist_fails_if_not_on_denylist() { + let worker_address = @3001; + worker_common::worker_config::initialize_for_worker_test_only( + worker_address, + WORKER_ID_EXECUTOR(), + worker_address, + @0x501ead, + vector[@1234, @2345], + vector[], + @0xfee11b1, + ); + let alice = @1122; + set_denylist(&make_call_ref_for_test(worker_address), alice, false); + } + + #[test] + fun test_set_worker_fee_lib() { + let worker_address = @3001; + let fee_lib = @1234; + worker_common::worker_config::initialize_for_worker_test_only( + worker_address, + WORKER_ID_EXECUTOR(), + worker_address, + @0x501ead, + vector[@1234, @2345], + vector[], + @0xfee11b1, + ); + + worker_config::set_worker_fee_lib(&make_call_ref_for_test(worker_address), fee_lib); + assert!(was_event_emitted(&worker_config::worker_fee_lib_updated_event(worker_address, fee_lib)), 0); + let fee_lib_result = worker_config::get_worker_fee_lib(worker_address); + assert!(fee_lib == fee_lib_result, 0); + } +} diff --git a/packages/layerzero-v2/aptos/contracts/workers/dvn/Move.toml b/packages/layerzero-v2/aptos/contracts/workers/dvn/Move.toml new file mode 100644 index 00000000..92a4e003 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/workers/dvn/Move.toml @@ -0,0 +1,62 @@ +[package] +name = "dvn" +version = "1.0.0" +authors = [] + +[addresses] +dvn = "_" +router_node_0 = "_" +zro = "_" +simple_msglib = "_" +blocked_msglib = "_" +uln_302 = "_" +router_node_1 = "_" +endpoint_v2_common = "_" +endpoint_v2 = "_" +layerzero_admin = "_" +layerzero_treasury_admin = "_" +treasury = "_" +msglib_types = "_" +worker_common = "_" +price_feed_router_0 = "_" +price_feed_router_1 = "_" +price_feed_module_0 = "_" +executor_fee_lib_router_0 = "_" +executor_fee_lib_router_1 = "_" +dvn_fee_lib_router_0 = "_" +dvn_fee_lib_router_1 = "_" +executor_fee_lib_0 = "_" +dvn_fee_lib_0 = "_" + +[dev-addresses] +dvn = "0x3001" +router_node_0 = "0x9001" +zro = "0x1112" +simple_msglib = "0x9002" +blocked_msglib = "0x9003" +uln_302 = "0x9005" +router_node_1 = "0x9007" +endpoint_v2_common = "0x9098" +endpoint_v2 = "0x12345678" +layerzero_admin = "0x18943124" +layerzero_treasury_admin = "0x1894312499" +treasury = "0x123432432" +msglib_types = "0x97324123" +worker_common = "0x3999" +price_feed_router_0 = "0x65DD71A" +price_feed_router_1 = "0x65DD71AB" +price_feed_module_0 = "0x65DD71" +executor_fee_lib_router_0 = "0x30001" +executor_fee_lib_router_1 = "0x30002" +dvn_fee_lib_router_0 = "0x30001a" +dvn_fee_lib_router_1 = "0x30002a" +executor_fee_lib_0 = "0x3000" +dvn_fee_lib_0 = "0x3000a" + +[dependencies] +endpoint_v2_common = { local = "../../endpoint_v2_common" } +worker_common = { local = "../../worker_peripherals/worker_common" } +router_node_0 = { local = "../../msglib/routers/router_node_0" } + +[dev-dependencies] + diff --git a/packages/layerzero-v2/aptos/contracts/workers/dvn/sources/dvn.move b/packages/layerzero-v2/aptos/contracts/workers/dvn/sources/dvn.move new file mode 100644 index 00000000..6388f686 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/workers/dvn/sources/dvn.move @@ -0,0 +1,387 @@ +module dvn::dvn { + use std::signer::address_of; + use std::vector; + + use dvn::hashes::{ + create_quorum_change_admin_hash, + create_set_allowlist_hash, + create_set_denylist_hash, + create_set_dvn_signer_hash, + create_set_fee_lib_hash, + create_set_msglibs_hash, + create_set_pause_hash, + create_set_quorum_hash, + create_verify_hash, + }; + use endpoint_v2_common::bytes32; + use endpoint_v2_common::contract_identity::{Self, CallRef, ContractSigner, DynamicCallRef, make_call_ref}; + use endpoint_v2_common::packet_raw; + use endpoint_v2_common::universal_config; + use msglib_types::worker_options::DVN_WORKER_ID; + use router_node_0::router_node; + use worker_common::multisig::{Self, assert_all_and_add_to_history}; + use worker_common::worker_config; + + #[test_only] + friend dvn::dvn_tests; + + struct DvnStore has key { + contract_signer: ContractSigner, + } + + /// Initialize the DVN Store + fun init_module(account: &signer) { + move_to(account, DvnStore { contract_signer: contract_identity::create_contract_signer(account) }); + } + + #[test_only] + /// Initialize the DVN Store for testing + public fun init_module_for_test() { + init_module(&std::account::create_signer_for_test(@dvn)); + } + + /// Worker-only function to register and configure the DVN. This can only be called once, and should be called with + /// the contract (@dvn) as the signer. + public entry fun initialize( + account: &signer, + deposit_address: address, + admins: vector
, + dvn_signers: vector>, + quorum: u64, + supported_msglibs: vector
, + fee_lib: address, + ) { + assert!(address_of(account) == @dvn, EUNAUTHORIZED); + assert!(vector::length(&supported_msglibs) > 0, EDVN_MSGLIB_LESS_THAN_ONE); + + worker_config::initialize_for_worker( + account, + DVN_WORKER_ID(), + deposit_address, + // Instead of a role admin, this DVN allows regular admins or signers (quorum_change_admin()) to add or + // remove admins + @0x0, + admins, + supported_msglibs, + fee_lib, + ); + multisig::initialize_for_worker(move account, quorum, dvn_signers); + } + + // ================================== Protocol: DVN Verify (Admin /w Signatures) ================================== + + /// DVNs call dvn_verify() in uln_302/router_calls.move to verify a packet + /// Only admins can call this function and it requires a quorum of dvn_signer signatures to succeed + public entry fun verify( + account: &signer, + packet_header: vector, + payload_hash: vector, + confirmations: u64, + msglib: address, + expiration: u64, + signatures: vector, + ) acquires DvnStore { + assert_admin(address_of(move account)); + let packet_header_raw = packet_raw::bytes_to_raw_packet(packet_header); + let hash = create_verify_hash( + packet_header, + payload_hash, + confirmations, + msglib, + get_vid(), + expiration, + ); + assert_all_and_add_to_history(call_ref(), &signatures, expiration, hash); + let dvn_verify_params = msglib_types::dvn_verify_params::pack_dvn_verify_params( + packet_header_raw, + bytes32::to_bytes32(payload_hash), + confirmations, + ); + router_node::dvn_verify(msglib, dynamic_call_ref(msglib, b"dvn_verify"), dvn_verify_params); + } + + // ================================================== Admin Only ================================================== + + /// Add or remove an admin. `active` is true to add, false to remove. + /// Admins are required to call the majority of DVN non-view actions, including verifying messages. + public entry fun set_admin(account: &signer, admin: address, active: bool) acquires DvnStore { + assert_admin(address_of(move account)); + worker_config::set_worker_admin(call_ref(), admin, active); + } + + + /// Set the deposit address for the DVN (must be real account) + /// The message library is instructed to send DVN fees to this address + public entry fun set_deposit_address(account: &signer, deposit_address: address) acquires DvnStore { + assert_admin(address_of(move account)); + worker_config::set_deposit_address(call_ref(), deposit_address); + } + + /// Set the configuration for a specific destination EID + public entry fun set_dst_config( + account: &signer, + remote_eid: u32, + gas: u64, + multiplier_bps: u16, + floor_margin_usd: u128, + ) acquires DvnStore { + assert_admin(address_of(move account)); + worker_config::set_dvn_dst_config(call_ref(), remote_eid, gas, multiplier_bps, floor_margin_usd); + } + + /// Sets the price feed module address and the feed address for the dvn + public entry fun set_price_feed( + account: &signer, + price_feed: address, + feed_address: address, + ) acquires DvnStore { + assert_admin(address_of(move account)); + worker_config::set_price_feed(call_ref(), price_feed, feed_address); + } + + // =========================================== Admin /w Signatures Only =========================================== + + /// Add or remove a dvn signer (public key) + /// This will abort if it results in fewer signers than the quorum + public entry fun set_dvn_signer( + account: &signer, + dvn_signer: vector, + active: bool, + expiration: u64, + signatures: vector, + ) acquires DvnStore { + assert_admin(address_of(move account)); + let hash = create_set_dvn_signer_hash(dvn_signer, active, get_vid(), expiration); + assert_all_and_add_to_history(call_ref(), &signatures, expiration, hash); + multisig::set_signer(call_ref(), dvn_signer, active); + } + + /// Update the quorum threshold + /// This will abort if the new quorum is greater than the number of registered dvn signers + public entry fun set_quorum( + account: &signer, + quorum: u64, + expiration: u64, + signatures: vector, + ) acquires DvnStore { + assert_admin(address_of(move account)); + let hash = create_set_quorum_hash(quorum, get_vid(), expiration); + assert_all_and_add_to_history(call_ref(), &signatures, expiration, hash); + multisig::set_quorum(call_ref(), quorum); + } + + /// Add or remove a sender address from the allowlist + /// When the allowlist has as least one entry, only senders on the allowlist can send messages to the DVN + /// When the allowlist is empty, only denylist senders will be rejected + /// The allowlist and the denylist are enforced upon get fee + public entry fun set_allowlist( + account: &signer, + oapp: address, + allowed: bool, + expiration: u64, + signatures: vector, + ) acquires DvnStore { + assert_admin(address_of(move account)); + let hash = create_set_allowlist_hash(oapp, allowed, get_vid(), expiration); + assert_all_and_add_to_history(call_ref(), &signatures, expiration, hash); + worker_config::set_allowlist(call_ref(), oapp, allowed); + } + + /// Add or remove an oapp sender address from the denylist + /// A denylisted sender will be rejected by the DVN regardless of the allowlist status + /// The allowlist and the denylist are enforced upon get fee + public entry fun set_denylist( + account: &signer, + oapp: address, + denied: bool, + expiration: u64, + signatures: vector, + ) acquires DvnStore { + assert_admin(address_of(move account)); + let hash = create_set_denylist_hash(oapp, denied, get_vid(), expiration); + assert_all_and_add_to_history(call_ref(), &signatures, expiration, hash); + worker_config::set_denylist(call_ref(), oapp, denied); + } + + /// Set the supported message libraries for the DVN + /// The list provided will completely replace the existing list + public entry fun set_supported_msglibs( + account: &signer, + msglibs: vector
, + expiration: u64, + signatures: vector, + ) acquires DvnStore { + assert_admin(address_of(move account)); + let hash = create_set_msglibs_hash(msglibs, get_vid(), expiration); + assert_all_and_add_to_history(call_ref(), &signatures, expiration, hash); + worker_config::set_supported_msglibs(call_ref(), msglibs); + } + + /// Set the fee lib for the DVN + /// The fee lib will be used by the Message Library to route the call to the correct DVN Fee Lib + public entry fun set_fee_lib( + account: &signer, + fee_lib: address, + expiration: u64, + signatures: vector, + ) acquires DvnStore { + assert_admin(address_of(move account)); + let hash = create_set_fee_lib_hash(fee_lib, get_vid(), expiration); + assert_all_and_add_to_history(call_ref(), &signatures, expiration, hash); + worker_config::set_worker_fee_lib(call_ref(), fee_lib); + } + + // Pause or unpause the DVN + public entry fun set_pause( + account: &signer, + pause: bool, + expiration: u64, + signatures: vector, + ) acquires DvnStore { + assert_admin(address_of(move account)); + let hash = create_set_pause_hash(pause, get_vid(), expiration); + assert_all_and_add_to_history(call_ref(), &signatures, expiration, hash); + worker_config::set_worker_pause(call_ref(), pause); + } + + // ================================================= Signers Only ================================================= + + /// Add or remove an admin using a quorum of dvn signers + public entry fun quorum_change_admin( + admin: address, + active: bool, + expiration: u64, + signatures: vector, + ) acquires DvnStore { + let hash = create_quorum_change_admin_hash(admin, active, get_vid(), expiration); + assert_all_and_add_to_history(call_ref(), &signatures, expiration, hash); + worker_config::set_worker_admin(call_ref(), admin, active); + } + + // ================================================ View Functions ================================================ + + #[view] + /// Returns the admins of the DVN + public fun get_admins(): vector
{ + worker_config::get_worker_admins(@dvn) + } + + #[view] + /// Returns whether the account is an admin of the DVN + public fun is_admin(account: address): bool { worker_config::is_worker_admin(@dvn, account) } + + #[view] + /// Returns whether the worker is paused + public fun is_paused(): bool { worker_config::is_worker_paused(@dvn) } + + #[view] + /// Returns the deposit address for the DVN. The message library will send fees to this address + public fun get_deposit_address(): address { + worker_config::get_deposit_address(@dvn) + } + + #[view] + /// Returns whether a particular signer (public key) is one of the DVN signers + public fun is_dvn_signer(signer: vector): bool { multisig::is_signer(@dvn, signer) } + + #[view] + /// Returns the fee library selected for this DVN + public fun get_fee_lib(): address { + worker_config::get_worker_fee_lib(@dvn) + } + + #[view] + /// Returns the quorum count required by this DVN + public fun get_quorum(): u64 { multisig::get_quorum(@dvn) } + + #[view] + /// Returns the list of supported message libraries for the DVN + public fun get_supported_msglibs(): vector
{ worker_config::get_supported_msglibs(@dvn) } + + #[view] + /// Returns the fee lib for the DVN + public fun get_worker_fee_lib(): address { + let fee_lib = worker_config::get_worker_fee_lib(@dvn); + fee_lib + } + + #[view] + /// Returns the default multiplier bps for the premium calculation + public fun get_default_multiplier_bps(): u16 { + worker_config::get_default_multiplier_bps(@dvn) + } + + #[view] + /// Returns the supported option types for the DVN + public fun get_supported_option_types(): vector { + worker_config::get_supported_option_types(@dvn) + } + + #[view] + /// Returns whether a particular sender is on the allowlist + public fun allowlist_contains(sender: address): bool { worker_config::allowlist_contains(@dvn, sender) } + + #[view] + /// Returns whether a particular sender is on the denylist + public fun denylist_contains(sender: address): bool { worker_config::denylist_contains(@dvn, sender) } + + #[view] + /// Returns whether the sender is allowed to send messages to the DVN based on allowlist and denylist + public fun is_allowed(sender: address): bool { worker_config::is_allowed(@dvn, sender) } + + #[view] + /// Returns the DVN config values for the destination EID + /// @returns (gas, multiplier_bps, floor_margin_usd) + public fun get_dst_config(dst_eid: u32): (u64, u16, u128) { + worker_config::get_dvn_dst_config_values(@dvn, dst_eid) + } + + #[view] + /// Returns the VID for the DVN + public fun get_vid(): u32 { + universal_config::eid() % 30_000 + } + + #[view] + /// Get the number of other workers that are currently delegating to this dvn's price feed configuration + public fun get_count_price_feed_delegate_dependents(): u64 { + worker_config::get_count_price_feed_delegate_dependents(@dvn) + } + + // ==================================================== Helpers =================================================== + + /// Derive a call ref for the DVN worker for a given target contract + inline fun dynamic_call_ref(target_contract: address, authorization: vector): &DynamicCallRef { + &contract_identity::make_dynamic_call_ref(&store().contract_signer, target_contract, authorization) + } + + /// Get a Call Ref directed at the worker common contract + inline fun call_ref(): &CallRef { + &make_call_ref(&store().contract_signer) + } + + /// Borrow the DVN Store + inline fun store(): &DvnStore { borrow_global(@dvn) } + + /// Borrow a mutable DVN store + inline fun store_mut(): &mut DvnStore { borrow_global_mut(@dvn) } + + // ==================================================== Internal =================================================== + + /// Assert that the caller is an admin + inline fun assert_admin(admin: address) { + worker_config::assert_worker_admin(@dvn, admin); + } + + /// Assert that the VID for the DVN is the expected VID for this chain + /// VID is a endpoint v1-v2 compatible eid e.g. 30101 -> 101 + inline fun assert_vid(vid: u32) { + assert!(get_vid() == vid, EDVN_INVALID_VID); + } + + // ================================================== Error Codes ================================================= + + const EDVN_INVALID_VID: u64 = 2; + const EDVN_MSGLIB_LESS_THAN_ONE: u64 = 3; + const EUNAUTHORIZED: u64 = 4; +} diff --git a/packages/layerzero-v2/aptos/contracts/workers/dvn/sources/hashes.move b/packages/layerzero-v2/aptos/contracts/workers/dvn/sources/hashes.move new file mode 100644 index 00000000..ac0e96be --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/workers/dvn/sources/hashes.move @@ -0,0 +1,214 @@ +/// These functions are used to generate hashes against which signatures are created and verified +module dvn::hashes { + use std::aptos_hash; + use std::vector; + + use endpoint_v2_common::bytes32::{Bytes32, keccak256}; + use endpoint_v2_common::serde; + + // ================================================ Hash Generation =============================================== + + // These hashes are used by the DVN Multisig as an input to signature generation + + #[view] + /// Get a 4-byte hash that represents a given function name + public fun get_function_signature(function_name: vector): vector { + vector::slice(&aptos_hash::keccak256(std::bcs::to_bytes(&function_name)), 0, 4) + } + + #[view] + /// Create a hash for a verify function call + public fun create_verify_hash( + packet_header: vector, + payload_hash: vector, + confirmations: u64, + target: address, + vid: u32, + expiration: u64, + ): Bytes32 { + keccak256(build_verify_payload(packet_header, payload_hash, confirmations, target, vid, expiration)) + } + + #[view] + /// Create a hash for a set_quorum function call + public fun create_set_quorum_hash(quorum: u64, vid: u32, expiration: u64): Bytes32 { + keccak256(build_set_quorum_payload(quorum, vid, expiration)) + } + + #[view] + /// Create a hash for a set_dvn_signer function call + public fun create_set_dvn_signer_hash(dvn_signer: vector, active: bool, vid: u32, expiration: u64): Bytes32 { + keccak256(build_set_dvn_signer_payload(dvn_signer, active, vid, expiration)) + } + + #[view] + /// Create a hash for a set_allowlist function call + public fun create_set_allowlist_hash(sender: address, allowed: bool, vid: u32, expiration: u64): Bytes32 { + keccak256(build_set_allowlist_payload(sender, allowed, vid, expiration)) + } + + #[view] + /// Create a hash for a set_denylist function call + public fun create_set_denylist_hash(sender: address, denied: bool, vid: u32, expiration: u64): Bytes32 { + keccak256(build_set_denylist_payload(sender, denied, vid, expiration)) + } + + #[view] + /// Create a hash for a quorum_change_admin function call + public fun create_quorum_change_admin_hash( + admin: address, + active: bool, + vid: u32, + expiration: u64, + ): Bytes32 { + keccak256(build_quorum_change_admin_payload(admin, active, vid, expiration)) + } + + #[view] + /// Create a hash for a set_msglibs function call + public fun create_set_msglibs_hash(msglibs: vector
, vid: u32, expiration: u64): Bytes32 { + keccak256(build_set_msglibs_payload(msglibs, vid, expiration)) + } + + #[view] + public fun create_set_fee_lib_hash(fee_lib: address, vid: u32, expiration: u64): Bytes32 { + keccak256(build_set_fee_lib_payload(fee_lib, vid, expiration)) + } + + #[view] + public fun create_set_pause_hash(pause: bool, vid: u32, expiration: u64): Bytes32 { + keccak256(build_set_pause_payload(pause, vid, expiration)) + } + + // ============================================== Payload Generation ============================================== + + // Payloads are serialized data that are hashed to create a hash that can be signed by a worker + + #[view] + /// Build the serialized payload for a verify function call (for procuring a hash) + public fun build_verify_payload( + packet_header: vector, + payload_hash: vector, + confirmations: u64, + target: address, + vid: u32, + expiration: u64, + ): vector { + let payload = vector[]; + serde::append_bytes(&mut payload, get_function_signature(b"verify")); + serde::append_bytes(&mut payload, packet_header); + serde::append_bytes(&mut payload, payload_hash); + serde::append_u64(&mut payload, confirmations); + serde::append_bytes(&mut payload, std::bcs::to_bytes(&target)); + serde::append_u32(&mut payload, vid); + serde::append_u64(&mut payload, expiration); + payload + } + + #[view] + /// Build the serialized payload for a set_quorum function call (for procuring a hash) + public fun build_set_quorum_payload(quorum: u64, vid: u32, expiration: u64): vector { + let payload = vector[]; + serde::append_bytes(&mut payload, get_function_signature(b"set_quorum")); + serde::append_u64(&mut payload, quorum); + serde::append_u32(&mut payload, vid); + serde::append_u64(&mut payload, expiration); + payload + } + + #[view] + /// Build the serialized payload for a set_dvn_signer function call (for procuring a hash) + public fun build_set_dvn_signer_payload( + dvn_signer: vector, + active: bool, + vid: u32, + expiration: u64, + ): vector { + let active_value: u8 = if (active) 1 else 0; + let payload = vector[]; + serde::append_bytes(&mut payload, get_function_signature(b"set_dvn_signer")); + serde::append_bytes(&mut payload, dvn_signer); + serde::append_u8(&mut payload, active_value); + serde::append_u32(&mut payload, vid); + serde::append_u64(&mut payload, expiration); + payload + } + + #[view] + /// Build the serialized payload for a set_allowlist function call (for procuring a hash) + public fun build_set_allowlist_payload(sender: address, allowed: bool, vid: u32, expiration: u64): vector { + let allowed_value: u8 = if (allowed) 1 else 0; + let payload = vector[]; + serde::append_bytes(&mut payload, get_function_signature(b"set_allowlist")); + serde::append_bytes(&mut payload, std::bcs::to_bytes(&sender)); + serde::append_u8(&mut payload, allowed_value); + serde::append_u32(&mut payload, vid); + serde::append_u64(&mut payload, expiration); + payload + } + + #[view] + /// Build the serialized payload for a set_denylist function call (for procuring a hash) + public fun build_set_denylist_payload(sender: address, denied: bool, vid: u32, expiration: u64): vector { + let denied_value: u8 = if (denied) 1 else 0; + let payload = vector[]; + serde::append_bytes(&mut payload, get_function_signature(b"set_denylist")); + serde::append_bytes(&mut payload, std::bcs::to_bytes(&sender)); + serde::append_u8(&mut payload, denied_value); + serde::append_u32(&mut payload, vid); + serde::append_u64(&mut payload, expiration); + payload + } + + #[view] + /// Build the serialized payload for a quorum_change_admin function call (for procuring a hash) + public fun build_quorum_change_admin_payload( + admin: address, + active: bool, + vid: u32, + expiration: u64, + ): vector { + let active_value: u8 = if (active) 1 else 0; + let payload = vector[]; + serde::append_bytes(&mut payload, get_function_signature(b"quorum_change_admin")); + serde::append_bytes(&mut payload, std::bcs::to_bytes(&admin)); + serde::append_u8(&mut payload, active_value); + serde::append_u32(&mut payload, vid); + serde::append_u64(&mut payload, expiration); + payload + } + + #[view] + /// Build the serialized payload for a set_msglibs function call (for procuring a hash) + public fun build_set_msglibs_payload(msglibs: vector
, vid: u32, expiration: u64): vector { + let payload = vector[]; + serde::append_bytes(&mut payload, get_function_signature(b"set_msglibs")); + for (i in 0..vector::length(&msglibs)) { + let msglib = *vector::borrow(&msglibs, i); + serde::append_address(&mut payload, msglib); + }; + serde::append_u32(&mut payload, vid); + serde::append_u64(&mut payload, expiration); + payload + } + + #[view] + public fun build_set_fee_lib_payload(fee_lib: address, vid: u32, expiration: u64): vector { + let payload = vector[]; + serde::append_bytes(&mut payload, get_function_signature(b"set_fee_lib")); + serde::append_address(&mut payload, fee_lib); + serde::append_u32(&mut payload, vid); + serde::append_u64(&mut payload, expiration); + payload + } + + #[view] + public fun build_set_pause_payload(pause: bool, vid: u32, expiration: u64): vector { + let payload = vector[]; + serde::append_bytes(&mut payload, get_function_signature(b"set_pause")); + serde::append_u8(&mut payload, if (pause) 1 else 0); + serde::append_u32(&mut payload, vid); + serde::append_u64(&mut payload, expiration); + payload + } +} diff --git a/packages/layerzero-v2/aptos/contracts/workers/dvn/tests/dvn_tests.move b/packages/layerzero-v2/aptos/contracts/workers/dvn/tests/dvn_tests.move new file mode 100644 index 00000000..da430455 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/workers/dvn/tests/dvn_tests.move @@ -0,0 +1,555 @@ +#[test_only] +module dvn::dvn_tests { + use std::account::{create_account_for_test, create_signer_for_test}; + use std::event::was_event_emitted; + + use dvn::dvn::{ + get_fee_lib, get_quorum, init_module_for_test, initialize, is_admin, is_dvn_signer, is_paused, + quorum_change_admin, set_admin, set_allowlist, set_denylist, set_deposit_address, set_dst_config, + set_dvn_signer, set_fee_lib, set_pause, set_quorum, set_supported_msglibs, verify, + }; + use dvn::hashes::create_verify_hash; + use endpoint_v2_common::bytes32::{Self, from_bytes32}; + use endpoint_v2_common::contract_identity::make_call_ref_for_test; + use endpoint_v2_common::guid::compute_guid; + use endpoint_v2_common::packet_raw::get_packet_bytes; + use endpoint_v2_common::packet_v1_codec; + use endpoint_v2_common::serde::flatten; + use endpoint_v2_common::universal_config; + use worker_common::worker_config; + + const VID: u32 = 1; + const EXPIRATION: u64 = 2000; + + #[test] + #[expected_failure(abort_code = worker_common::worker_config_store::EWORKER_ALREADY_INITIALIZED)] + fun test_register_and_configure_dvn_cannot_initialize_twice() { + let pub_key_1: vector = x"3bd5f17b6bc7a9022402246dd8e1530f0acd1d6439089b4f3bd8868250c1656c08a9fc2e4bff170ed023fbf77e6645020a77eba9c7c03390ed1b316af1ab6f0c"; + universal_config::init_module_for_test(VID); + init_module_for_test(); + + let dvn = &create_account_for_test(@dvn); + initialize( + dvn, + @dvn, + vector[@1234], + vector[pub_key_1], + 1, + vector[@0xaaaa], + @dvn_fee_lib_router_0, + ); + initialize( + dvn, + @dvn, + vector[@1234], + vector[pub_key_1], + 1, + vector[@0xaaaa], + @dvn_fee_lib_router_0, + ) + } + + #[test] + fun test_initialization() { + let pub_key_1: vector = x"3bd5f17b6bc7a9022402246dd8e1530f0acd1d6439089b4f3bd8868250c1656c08a9fc2e4bff170ed023fbf77e6645020a77eba9c7c03390ed1b316af1ab6f0c"; + let pub_key_2: vector = x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d"; + let pub_key_3: vector = x"37bdab42a45e9d6cc56f7d0cc7897e871a0357bce3f0f4c99c93c54291b259a29a92111167a25ae188ef49b2f3df880d8aae8522e29cb6c299745258a200cfff"; + universal_config::init_module_for_test(VID); + init_module_for_test(); + let dvn = &create_account_for_test(@dvn); + initialize( + dvn, + @dvn, + vector[@1234, @2234, @3234], + vector[pub_key_1, pub_key_2], + 1, + vector[@0xaaaa], + @dvn_fee_lib_router_0, + ); + assert!(get_quorum() == 1, 0); + assert!(is_dvn_signer(pub_key_1), 1); + assert!(is_dvn_signer(pub_key_2), 2); + assert!(!is_dvn_signer(pub_key_3), 3); + assert!(is_admin(@1234), 4); + assert!(is_admin(@2234), 5); + assert!(is_admin(@3234), 6); + assert!(!is_admin(@9876), 7); + } + + #[test] + fun test_set_dvn_signer() { + let pub_key_1: vector = x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a"; + let pub_key_2: vector = x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d"; + universal_config::init_module_for_test(VID); + init_module_for_test(); + let dvn = &create_account_for_test(@dvn); + initialize( + dvn, + @dvn, + vector[@1234, @2234, @3234], + vector[pub_key_1], + 1, + vector[@0xaaaa], + @dvn_fee_lib_router_0, + ); + + let signature_1 = x"a13c94e82fc009f71f152f137bed7fb799fa7d75a91a0e3a4ed2000fd408ba052743f3b91ee00cf6a5e98cd4d12b3b2e4984213c0c1c5a251b4e98eeec54f7a800"; + + let native_framework = &create_signer_for_test(@std); + std::timestamp::set_time_has_started_for_testing(native_framework); + std::timestamp::update_global_time_for_test_secs(1000); + + let admin_1 = &create_signer_for_test(@1234); + set_dvn_signer( + admin_1, + pub_key_2, + true, + EXPIRATION, + signature_1, + ); + + assert!(is_dvn_signer(pub_key_2), 0); + } + + #[test] + fun test_set_dst_config() { + let pub_key_1: vector = x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a"; + universal_config::init_module_for_test(VID); + init_module_for_test(); + let dvn = &create_account_for_test(@dvn); + let dvn_address = std::signer::address_of(dvn); + initialize( + dvn, + @dvn, + vector[dvn_address], + vector[pub_key_1], + 1, + vector[@0xaaaa], + @dvn_fee_lib_router_0, + ); + + set_dst_config(dvn, 101, 77000, 12000, 1); + + let (gas, multiplier_bps, floor_margin_usd) = worker_config::get_dvn_dst_config_values( + std::signer::address_of(dvn), + 101, + ); + assert!(gas == 77000, 0); + assert!(multiplier_bps == 12000, 1); + assert!(floor_margin_usd == 1, 2); + } + + #[test] + fun test_set_quorum() { + let pub_key_1: vector = x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a"; + let pub_key_2: vector = x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d"; + universal_config::init_module_for_test(VID); + init_module_for_test(); + let dvn = &create_account_for_test(@dvn); + initialize( + dvn, + @dvn, + vector[@1234, @2234, @3234], + vector[pub_key_1, pub_key_2], + 1, + vector[@0xaaaa], + @dvn_fee_lib_router_0, + ); + + let signature_1 = x"456e6b632d0958e6dc3d2fff9998e9c4be8023884e4a7f05d63bfd55f0178c743902b838114150a715597c808832c6bc61215ddc5133beac665861d9c2d0e26800"; + + let native_framework = &create_signer_for_test(@std); + std::timestamp::set_time_has_started_for_testing(native_framework); + std::timestamp::update_global_time_for_test_secs(1000); + + let admin_1 = &create_signer_for_test(@1234); + set_quorum( + admin_1, + 2, + EXPIRATION, + signature_1, + ); + + assert!(get_quorum() == 2, 0); + } + + #[test] + fun test_set_allowlist() { + let pub_key_1: vector = x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a"; + let pub_key_2: vector = x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d"; + universal_config::init_module_for_test(VID); + init_module_for_test(); + + let dvn = &create_account_for_test(@dvn); + initialize( + dvn, + @dvn, + vector[@1234, @2234, @3234], + vector[pub_key_1, pub_key_2], + 1, + vector[@simple_msglib], + @dvn_fee_lib_router_0, + ); + + let signature_1_1 = x"d9a9aa95f0e21102aa8d05f2ada4261bc887b16f85d669df700c01ed985439187a278239f70567bcb879b1b5109d2f6fd4fa551535421188450e6cb1c74df7f200"; + + let native_framework = &create_signer_for_test(@std); + std::timestamp::set_time_has_started_for_testing(native_framework); + std::timestamp::update_global_time_for_test_secs(1000); + + let admin_1 = &create_signer_for_test(@1234); + + set_allowlist(admin_1, @9988, true, EXPIRATION, signature_1_1); // pubkey 1 adds oapp to allowlist + assert!(worker_config::allowlist_contains(@dvn, @9988), 0); + assert!(worker_config::is_allowed(@dvn, @9988), 1); + + let signature_1_2 = x"504be072fd7ca14b3ef724d4dcfe2eb24ed121651b9f293b56c1df3a0e3e5e17437308c3d0aff27f7a9608003447d11d1f8f2f5c521ba77abb751d6fa225693001"; + set_allowlist(admin_1, @9988, false, EXPIRATION, signature_1_2); // pubkey 1 removes oapp to allowlist + assert!(!worker_config::allowlist_contains(@dvn, @9988), 2); + } + + #[test] + fun test_set_denylist() { + let pub_key_1: vector = x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a"; + let pub_key_2: vector = x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d"; + universal_config::init_module_for_test(VID); + init_module_for_test(); + + let dvn = &create_account_for_test(@dvn); + initialize( + dvn, + @dvn, + vector[@1234, @2234, @3234], + vector[pub_key_1, pub_key_2], + 1, + vector[@simple_msglib], + @dvn_fee_lib_router_0, + ); + + let signature_1_1 = x"afbb4ac1ed62b3c63ee2ae9b9b5272f5fb7296990da96a29029e75bb6b97b3fc6471ad3ac62e897ab7681bf7dc4dd10f8b33325e391155d8d02d7c3ad455eaf001"; + + let native_framework = &create_signer_for_test(@std); + std::timestamp::set_time_has_started_for_testing(native_framework); + std::timestamp::update_global_time_for_test_secs(1000); + + let admin_1 = &create_signer_for_test(@1234); + + set_denylist(admin_1, @9988, true, EXPIRATION, signature_1_1); // pubkey 1 adds oapp to denylist + assert!(worker_config::denylist_contains(@dvn, @9988), 0); + assert!(!worker_config::is_allowed(@dvn, @9988), 1); + + let signature_1_2 = x"64526a94655175cb1553d36615b2cbae8c7df8465719e1508ea1905d47b833fe44aea8d6aebf48a1cb70f6d93ba92215b70a311b18dd468d290d02404c15b63e01"; + set_denylist(admin_1, @9988, false, EXPIRATION, signature_1_2); // pubkey 1 removes oapp to denylist + assert!(!worker_config::denylist_contains(@dvn, @9988), 2); + assert!(worker_config::is_allowed(@dvn, @9988), 3); + } + + #[test] + fun test_set_fee_lib() { + let pub_key_1: vector = x"1656867692ee1158567ecf944ea0755eff7d804b72fb3bdd7dda07758296cf14df3a10d6632e17023a4ed2aa47f6adf83b7aa6b0be4100efbcb7654cc40bcede"; + universal_config::init_module_for_test(VID); + init_module_for_test(); + + let dvn = &create_account_for_test(@dvn); + initialize( + dvn, + @dvn, + vector[@1234, @2234, @3234], + vector[pub_key_1], + 1, + vector[@simple_msglib], + @0xfee11b001, + ); + + let fee_lib_from_worker_config = worker_config::get_worker_fee_lib(@dvn); + assert!(fee_lib_from_worker_config == @0xfee11b001, 0); + let fee_lib_from_dvn = get_fee_lib(); + assert!(fee_lib_from_dvn == @0xfee11b001, 1); + + let signature_1 = x"8675d9d20931230c405c6a756d7d9e9f6c2bc9770e2e7d52a1d31b5a7a5fedd80f5e60a0af97ef2ee5a7063ebbf285f34c2de07630a5bc07bac24da80d0a055e00"; + + let native_framework = &create_signer_for_test(@std); + std::timestamp::set_time_has_started_for_testing(native_framework); + std::timestamp::update_global_time_for_test_secs(1000); + + let admin_1 = &create_signer_for_test(@1234); + set_fee_lib( + admin_1, + @0xfee11b002, + EXPIRATION, + signature_1, + ); + assert!(was_event_emitted(&worker_config::worker_fee_lib_updated_event(@dvn, @0xfee11b002)), 2); + + let fee_lib_from_worker_config = worker_config::get_worker_fee_lib(@dvn); + assert!(fee_lib_from_worker_config == @0xfee11b002, 0); + let fee_lib_from_dvn = get_fee_lib(); + assert!(fee_lib_from_dvn == @0xfee11b002, 1); + } + + #[test] + fun test_set_pause() { + let pub_key_1: vector = x"2f68cff6060b082c04370615bbd5097d2f55f6d4ec9e3ed6156db64095b43efe0894d94399cc394394cdfb1075515877049959398ef042fca64e03443f9e8a41"; + universal_config::init_module_for_test(VID); + init_module_for_test(); + + let dvn = &create_account_for_test(@dvn); + initialize( + dvn, + @dvn, + vector[@1234, @2234, @3234], + vector[pub_key_1], + 1, + vector[@simple_msglib], + @0xfee11b001, + ); + + let signature_1 = x"8675d9d20931230c405c6a756d7d9e9f6c2bc9770e2e7d52a1d31b5a7a5fedd80f5e60a0af97ef2ee5a7063ebbf285f34c2de07630a5bc07bac24da80d0a055e00"; + + let native_framework = &create_signer_for_test(@std); + std::timestamp::set_time_has_started_for_testing(native_framework); + std::timestamp::update_global_time_for_test_secs(1000); + + assert!(!is_paused(), 0); + let admin_1 = &create_signer_for_test(@1234); + set_pause(admin_1, true, EXPIRATION, signature_1); + assert!(was_event_emitted(&worker_config::paused_event(@dvn)), 2); + assert!(is_paused(), 0); + } + + #[test] + fun test_set_unpause() { + let pub_key_1: vector = x"7dd96c8221160d75f6ea7b11382755a907e80a90d03275f360f1febd21d8454819abbefaed3080505f6e3c2d5b33cf04e019cb8cfd90440c7715663ee4fe5483"; + universal_config::init_module_for_test(VID); + init_module_for_test(); + + let dvn = &create_account_for_test(@dvn); + initialize( + dvn, + @dvn, + vector[@1234, @2234, @3234], + vector[pub_key_1], + 1, + vector[@simple_msglib], + @0xfee11b001, + ); + + let signature_1 = x"8675d9d20931230c405c6a756d7d9e9f6c2bc9770e2e7d52a1d31b5a7a5fedd80f5e60a0af97ef2ee5a7063ebbf285f34c2de07630a5bc07bac24da80d0a055e00"; + + let native_framework = &create_signer_for_test(@std); + std::timestamp::set_time_has_started_for_testing(native_framework); + std::timestamp::update_global_time_for_test_secs(1000); + assert!(!is_paused(), 0); + worker_config::set_worker_pause(&make_call_ref_for_test(@dvn), true); + assert!(is_paused(), 0); + + let admin_1 = &create_signer_for_test(@1234); + set_pause(admin_1, false, EXPIRATION, signature_1); + assert!(was_event_emitted(&worker_config::unpaused_event(@dvn)), 2); + assert!(!is_paused(), 0); + } + + #[test] + fun test_quorum_change_admin() { + let pub_key_1: vector = x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a"; + let pub_key_2: vector = x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d"; + universal_config::init_module_for_test(VID); + init_module_for_test(); + + let dvn = &create_account_for_test(@dvn); + initialize( + dvn, + @dvn, + vector[@1234, @2234, @3234], + vector[pub_key_1, pub_key_2], + 1, + vector[@simple_msglib], + @dvn_fee_lib_router_0, + ); + + let signature_1 = x"8675d9d20931230c405c6a756d7d9e9f6c2bc9770e2e7d52a1d31b5a7a5fedd80f5e60a0af97ef2ee5a7063ebbf285f34c2de07630a5bc07bac24da80d0a055e00"; + let signature_2 = x"6525f9282e67057649022911b539663b92b9ba06b3b1a797c052499b61cf4e404f1aba7379fb1f75a7a891ce890ffa866c38704b0f978f8c76721ab2dcff437d01"; + + let native_framework = &create_signer_for_test(@std); + std::timestamp::set_time_has_started_for_testing(native_framework); + std::timestamp::update_global_time_for_test_secs(1000); + + assert!(!is_admin(@9988), 0); + quorum_change_admin( + @9988, + true, + EXPIRATION, + flatten(vector[signature_1, signature_2]), + ); + assert!(is_admin(@9988), 1); + } + + #[test] + fun test_set_supported_msglibs() { + let pub_key_1: vector = x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a"; + universal_config::init_module_for_test(VID); + init_module_for_test(); + + let dvn = &create_account_for_test(@dvn); + initialize( + dvn, + @dvn, + vector[@1234, @2234, @3234], + vector[pub_key_1], + 1, + vector[@simple_msglib], + @dvn_fee_lib_router_0, + ); + + let signature_1 = x"e7b2bfe8c1f079ea3aa1923ba76e3f15ae30fab716941352514b34656c6cd9b96c5a0ee0a5e4f579dcff9a8a339dcd44c53b7f56068fb7c97c7c589d2e4518a601"; + + let native_framework = &create_signer_for_test(@std); + std::timestamp::set_time_has_started_for_testing(native_framework); + std::timestamp::update_global_time_for_test_secs(1000); + + let admin_1 = &create_signer_for_test(@1234); + + assert!(!std::vector::contains(&worker_config::get_supported_msglibs(@dvn), &@2345), 1); + set_supported_msglibs( + admin_1, + vector[@1234, @2345], + EXPIRATION, + flatten(vector[signature_1]), + ); + assert!(std::vector::contains(&worker_config::get_supported_msglibs(@dvn), &@2345), 1); + } + + #[test] + fun test_verify() { + let pub_key_1: vector = x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a"; + let pub_key_2: vector = x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d"; + + universal_config::init_module_for_test(VID); + uln_302::msglib::initialize_for_test(); + init_module_for_test(); + + let dvn = &create_account_for_test(@dvn); + initialize( + dvn, + @dvn, + vector[@1234, @2234, @3234], + vector[pub_key_1, pub_key_2], + 1, + vector[@simple_msglib, @uln_302], + @dvn_fee_lib_router_0, + ); + + let signature_1 = x"0fdeda25570d3cb243b39764a167558e4445786c54b180afe8ed36ab87b95a9f04c26107fb149be923d0f82d54d064a2dfc18edaa9c3f00f572c20240a51064700"; + let signature_2 = x"344aa33801eab51e48fa0b1112f1cb3227316a6640a0d05dc9e1f9cf1c62494a69fe521a2fca7cdfa70271bf3126b7d4fa10696ad648a17c1c32e3e3c678eb0d00"; + + let native_framework = &create_signer_for_test(@std); + std::timestamp::set_time_has_started_for_testing(native_framework); + std::timestamp::update_global_time_for_test_secs(1000); + + // params to use for test + let src_eid = 1; + let sender = bytes32::from_address(@9999); + let dst_eid = 1; + let receiver = bytes32::from_address(@9999); + let nonce = 1; + let message = vector[1, 2, 3, 4]; + let guid = compute_guid(nonce, src_eid, sender, dst_eid, receiver); + let packet = endpoint_v2_common::packet_v1_codec::new_packet_v1( + src_eid, + sender, + dst_eid, + receiver, + nonce, + guid, + message, + ); + let packet_header = packet_v1_codec::extract_header(&packet); + let packet_header_bytes = get_packet_bytes(packet_header); + let payload_hash = endpoint_v2_common::packet_v1_codec::get_payload_hash(&packet); + + let admin = &create_signer_for_test(@1234); + + verify( + admin, + packet_header_bytes, + bytes32::from_bytes32(payload_hash), + 10, + @uln_302, + EXPIRATION, + flatten(vector[signature_1, signature_2]), + ); + + let expected_used_hash = create_verify_hash( + packet_header_bytes, + bytes32::from_bytes32(payload_hash), + 10, + @uln_302, + dst_eid, + EXPIRATION, + ); + assert!(worker_common::multisig::was_hash_used(@dvn, from_bytes32(expected_used_hash)), 0) + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::EUNAUTHORIZED)] + fun test_set_admin_fails_if_not_admin() { + let pub_key_1: vector = x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a"; + universal_config::init_module_for_test(VID); + init_module_for_test(); + + let dvn = &create_account_for_test(@dvn); + initialize( + dvn, + @dvn, + vector[@1234, @2234, @3234], + vector[pub_key_1], + 1, + vector[@simple_msglib], + @dvn_fee_lib_router_0, + ); + let admin = &create_signer_for_test(@3333); + set_admin(admin, @8888, true); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::EUNAUTHORIZED)] + fun test_set_deposit_address_fails_if_not_admin() { + let pub_key_1: vector = x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a"; + universal_config::init_module_for_test(VID); + init_module_for_test(); + + let dvn = &create_account_for_test(@dvn); + initialize( + dvn, + @dvn, + vector[@1234, @2234, @3234], + vector[pub_key_1], + 1, + vector[@simple_msglib], + @dvn_fee_lib_router_0, + ); + let admin = &create_signer_for_test(@1111); + set_deposit_address(admin, @8888); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::EUNAUTHORIZED)] + fun test_set_dst_config_fails_if_not_admin() { + let pub_key_1: vector = x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a"; + universal_config::init_module_for_test(VID); + init_module_for_test(); + + let dvn = &create_account_for_test(@dvn); + initialize( + dvn, + @dvn, + vector[@1234, @2234, @3234], + vector[pub_key_1], + 1, + vector[@simple_msglib], + @dvn_fee_lib_router_0, + ); + let admin = &create_signer_for_test(@1111); + set_dst_config(admin, 101, 77000, 12000, 1); + } +} diff --git a/packages/layerzero-v2/aptos/contracts/workers/dvn/tests/hashes_test.move b/packages/layerzero-v2/aptos/contracts/workers/dvn/tests/hashes_test.move new file mode 100644 index 00000000..5fbc9b3d --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/workers/dvn/tests/hashes_test.move @@ -0,0 +1,125 @@ +#[test_only] +module dvn::hashes_test { + use dvn::hashes; + use endpoint_v2_common::bytes32::to_bytes32; + + const VID: u32 = 1; + const EXPIRATION: u64 = 2000; + + #[test] + fun test_get_function_signature() { + assert!(hashes::get_function_signature(b"verify") == x"7c40a351", 0); + assert!(hashes::get_function_signature(b"set_dvn_signer") == x"1372c8d1", 1); + assert!(hashes::get_function_signature(b"set_quorum") == x"17b7ccf9", 2); + assert!(hashes::get_function_signature(b"set_allowlist") == x"934ff7eb", 3); + assert!(hashes::get_function_signature(b"set_denylist") == x"8442b40b", 4); + assert!(hashes::get_function_signature(b"quorum_change_admin") == x"73028773", 5); + } + + #[test] + fun test_create_verify_hash() { + // Test params + let packet_header = x"010000000000000001000000010000000000000000000000000000000000000000000000000000000000009099000000010000000000000000000000000000000000000000000000000000000000009099"; + let payload_hash = x"cc35f70cc84269e2bfe02824b3d69e4120e6a58302a8129c4e11d9d9777a38c0"; + let confirmations = 10; + let target = @0x0000000000000000000000000000000000000000000000000000000000009005; + // Expected results + let expected_payload = x"7c40a351010000000000000001000000010000000000000000000000000000000000000000000000000000000000009099000000010000000000000000000000000000000000000000000000000000000000009099cc35f70cc84269e2bfe02824b3d69e4120e6a58302a8129c4e11d9d9777a38c0000000000000000a00000000000000000000000000000000000000000000000000000000000090050000000100000000000007d0"; + let expected_hash = to_bytes32(x"e3e8219995b9d75e7748415b6d54235f1d1cbec86f12d6602f423b7b20353799"); + assert!( + hashes::build_verify_payload( + packet_header, + payload_hash, + confirmations, + target, + VID, + EXPIRATION, + ) == expected_payload, + 0, + ); + assert!( + hashes::create_verify_hash( + packet_header, + payload_hash, + confirmations, + target, + VID, + EXPIRATION, + ) == expected_hash, + 1, + ); + } + + #[test] + fun test_create_set_quorum_hash() { + // Test params + let quorum = 2; + // Expected results + let expected_payload = x"17b7ccf900000000000000020000000100000000000007d0"; + let expected_hash = to_bytes32(x"3064e840a7183166bb439dfb1de0f6befc2e1731efcbb90cc6178b6b42cf3584"); + assert!(hashes::build_set_quorum_payload(quorum, VID, EXPIRATION) == expected_payload, 0); + assert!(hashes::create_set_quorum_hash(quorum, VID, EXPIRATION) == expected_hash, 1); + } + + #[test] + fun test_create_set_dvn_signer_hash() { + // Test params + let dvn_signer = x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d"; + let active = true; + // Expected results + let expected_payload = x"1372c8d1505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d010000000100000000000007d0"; + let expected_hash = to_bytes32(x"ad2262753ab5dab4d29c2437dd09d5bc6bdb4632e781d424f031d5cd5970728b"); + assert!(hashes::build_set_dvn_signer_payload(dvn_signer, active, VID, EXPIRATION) == expected_payload, 0); + assert!(hashes::create_set_dvn_signer_hash(dvn_signer, active, VID, EXPIRATION) == expected_hash, 1); + } + + #[test] + fun test_create_set_allowlist_hash() { + // Test params + let sender = @9988; + let allowed = true; + // Expected results + let expected_payload = x"934ff7eb0000000000000000000000000000000000000000000000000000000000002704010000000100000000000007d0"; + let expected_hash = to_bytes32(x"d418cb1c18cfd5d3fc1fbdac34a9d71fb4b56a8f2d074e36d497c8e0489c7a15"); + assert!(hashes::build_set_allowlist_payload(sender, allowed, VID, EXPIRATION) == expected_payload, 0); + assert!(hashes::create_set_allowlist_hash(sender, allowed, VID, EXPIRATION) == expected_hash, 1); + } + + #[test] + fun test_create_set_denylist_hash() { + // Test params + let sender = @9988; + let denied = true; + // Expected results + let expected_payload = x"8442b40b0000000000000000000000000000000000000000000000000000000000002704010000000100000000000007d0"; + let expected_hash = to_bytes32(x"67f702d40c8e4bd7c2d59cc2d772b0a8c2398a08336176f38118fd0f33704817"); + assert!(hashes::build_set_denylist_payload(sender, denied, VID, EXPIRATION) == expected_payload, 0); + assert!(hashes::create_set_denylist_hash(sender, denied, VID, EXPIRATION) == expected_hash, 1); + } + + #[test] + fun test_create_quorum_change_admin_hash() { + // Test params + let admin = @0x0000000000000000000000000000000000000000000000000000000000002704; + let active = true; + // Expected results + let expected_payload = x"730287730000000000000000000000000000000000000000000000000000000000002704010000000100000000000007d0"; + let expected_hash = to_bytes32(x"c8ee741967867d5a99e739baa2a57c8b480a438855a4fc0d7b5ea2e28a8deaa5"); + assert!( + hashes::build_quorum_change_admin_payload(admin, active, VID, EXPIRATION) == expected_payload, + 0, + ); + assert!(hashes::create_quorum_change_admin_hash(admin, active, VID, EXPIRATION) == expected_hash, 1); + } + + #[test] + fun test_create_set_msglibs_hash() { + // Test params + let msglibs = vector
[@1234, @2345]; + // Expected results + let expected_payload = x"6456530e00000000000000000000000000000000000000000000000000000000000004d200000000000000000000000000000000000000000000000000000000000009290000000100000000000007d0"; + let expected_hash = to_bytes32(x"8796cf58bf29e0d42e08c3e9b3544b451c1881223d2d3e76ad6e39ff1ba8ec8b"); + assert!(hashes::build_set_msglibs_payload(msglibs, VID, EXPIRATION) == expected_payload, 0); + assert!(hashes::create_set_msglibs_hash(msglibs, VID, EXPIRATION) == expected_hash, 1); + } +} \ No newline at end of file diff --git a/packages/layerzero-v2/aptos/contracts/workers/executor/Move.toml b/packages/layerzero-v2/aptos/contracts/workers/executor/Move.toml new file mode 100644 index 00000000..ee09b532 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/workers/executor/Move.toml @@ -0,0 +1,25 @@ +[package] +name = "executor" +version = "1.0.0" +authors = [] + +[addresses] +executor = "_" +endpoint_v2 = "_" +endpoint_v2_common = "_" +layerzero_admin = "_" +layerzero_treasury_admin = "_" +worker_common = "_" +native_token_metadata_address = "0xa" + +[dev-addresses] +executor = "0x6001" +endpoint_v2 = "0x13241234" +endpoint_v2_common = "0x9098" +layerzero_admin = "0x18943124" +layerzero_treasury_admin = "0x1894312499" +worker_common = "0x3999" + +[dependencies] +endpoint_v2_common = { local = "../../endpoint_v2_common" } +worker_common = { local = "../../worker_peripherals/worker_common" } diff --git a/packages/layerzero-v2/aptos/contracts/workers/executor/sources/executor.move b/packages/layerzero-v2/aptos/contracts/workers/executor/sources/executor.move new file mode 100644 index 00000000..9de9c330 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/workers/executor/sources/executor.move @@ -0,0 +1,442 @@ +module executor::executor { + use std::event::emit; + use std::fungible_asset::{Self, Metadata}; + use std::object::address_to_object; + use std::primary_fungible_store; + use std::signer::address_of; + use std::vector; + + use endpoint_v2_common::contract_identity::{ + CallRef, + ContractSigner, + create_contract_signer, + make_call_ref, + }; + use endpoint_v2_common::native_token; + use executor::native_drop_params::{ + calculate_total_amount, deserialize_native_drop_params, NativeDropParams, unpack_native_drop_params, + }; + use worker_common::worker_config::{Self, WORKER_ID_EXECUTOR}; + + struct ExecutorConfig has key { + contract_signer: ContractSigner, + } + + fun init_module(account: &signer) { + move_to(account, ExecutorConfig { contract_signer: create_contract_signer(account) }); + } + + #[test_only] + public fun init_module_for_test() { + let account = &std::account::create_signer_for_test(@executor); + init_module(account); + } + + /// Initializes the executor + /// This function must be called before using the executor + /// + /// @param account the signer of the transaction + /// @param role_admin the address of the default admin + /// @param admins the list of admins + /// @param suppored_msglibs the list of supported msglibs + /// @param fee_lib the fee lib to use + /// @param price_feed the price feed to use + /// @param feed_address the address of the feed within the module + /// @return the total fee and the deposit address + /// @dev this function must be called before using the executor + public entry fun initialize( + account: &signer, + deposit_address: address, + role_admin: address, + admins: vector
, + supported_msglibs: vector
, + fee_lib: address, + ) { + assert!(address_of(account) == @executor, EUNAUTHORIZED); + worker_config::initialize_for_worker( + account, + WORKER_ID_EXECUTOR(), + deposit_address, + role_admin, + admins, + supported_msglibs, + fee_lib, + ); + } + + // ================================================ Role Admin Only =============================================== + + /// Sets the role admin for the executor + /// A role admin can only be set by another role admin + public entry fun set_role_admin( + account: &signer, + role_admin: address, + active: bool, + ) acquires ExecutorConfig { + assert_role_admin(address_of(move account)); + worker_config::set_worker_role_admin(call_ref(), role_admin, active); + } + + /// Sets the fee lib for the executor + public entry fun set_admin( + account: &signer, + admin: address, + active: bool, + ) acquires ExecutorConfig { + assert_role_admin(address_of(move account)); + worker_config::set_worker_admin(call_ref(), admin, active); + } + + /// Pauses or unpauses the executor + public entry fun set_pause(account: &signer, pause: bool) acquires ExecutorConfig { + assert_role_admin(address_of(move account)); + worker_config::set_worker_pause(call_ref(), pause); + } + + // ================================================== Admin Only ================================================== + + /// Applies a native drop + /// This is intended to be called by the executor in concert with lz_receive (on the OApp), either separately from + /// the offchain or in a ad-hoc script. + /// This call will withdraw the total amount from the executor's primary fungible store and distribute it to the + /// receivers specified in the params. + public entry fun native_drop( + account: &signer, + src_eid: u32, + sender: vector, + nonce: u64, + dst_eid: u32, + oapp: address, + serialized_params: vector, + ) { + assert_admin(address_of(account)); + + let params = deserialize_native_drop_params(&serialized_params); + let total_amount = calculate_total_amount(params); + let metadata = address_to_object(@native_token_metadata_address); + + // Last signer use + let fa_total = native_token::withdraw(account, total_amount); + + for (i in 0..vector::length(¶ms)) { + let (receiver, amount) = unpack_native_drop_params(*vector::borrow(¶ms, i)); + let receiver_store = primary_fungible_store::ensure_primary_store_exists(receiver, metadata); + let fa = fungible_asset::extract(&mut fa_total, amount); + fungible_asset::deposit(receiver_store, fa); + }; + fungible_asset::destroy_zero(fa_total); + + emit(NativeDropApplied { + src_eid, + sender, + nonce, + dst_eid, + oapp, + params, + }); + } + + public fun emit_lz_receive_value_provided( + admin: &signer, + receiver: address, + src_eid: u32, + sender: vector, + nonce: u64, + guid: vector, + lz_receive_value: u64, + ) { + assert_admin(address_of(move admin)); + emit(LzReceiveValueProvided { receiver, src_eid, sender, nonce, guid, lz_receive_value }); + } + + public fun emit_lz_compose_value_provided( + admin: &signer, + from: address, + to: address, + index: u16, + guid: vector, + lz_compose_value: u64, + ) { + assert_admin(address_of(move admin)); + emit(LzComposeValueProvided { from, to, index, guid, lz_compose_value }); + } + + /// Sets the deposit address to which the fee lib will send payment for the executor + public entry fun set_deposit_address(account: &signer, deposit_address: address) acquires ExecutorConfig { + assert_admin(address_of(move account)); + worker_config::set_deposit_address(call_ref(), deposit_address); + } + + /// Sets the multiplier premium for the executor + public entry fun set_default_multiplier_bps(account: &signer, default_multiplier_bps: u16) acquires ExecutorConfig { + assert_admin(address_of(move account)); + worker_config::set_default_multiplier_bps(call_ref(), default_multiplier_bps); + } + + /// Sets the supported option types for the executor + public entry fun set_supported_option_types(account: &signer, option_types: vector) acquires ExecutorConfig { + assert_admin(address_of(move account)); + worker_config::set_supported_option_types(call_ref(), option_types); + } + + /// Sets the destination config for an eid on the executor + /// + /// @param account the signer of the transaction (must be admin) + /// @param remote_eid the destination eid + /// @param lz_receive_base_gas the base gas for lz receive + /// @param multiplier_bps the multiplier in basis points + /// @param floor_margin_usd the floor margin in USD + /// @param native_cap the native cap + /// @param lz_compose_base_gas the base gas for lz compose + public entry fun set_dst_config( + account: &signer, + remote_eid: u32, + lz_receive_base_gas: u64, + multiplier_bps: u16, + floor_margin_usd: u128, + native_cap: u128, + lz_compose_base_gas: u64, + ) acquires ExecutorConfig { + assert_admin(address_of(move account)); + worker_config::set_executor_dst_config( + call_ref(), + remote_eid, + lz_receive_base_gas, + multiplier_bps, + floor_margin_usd, + native_cap, + lz_compose_base_gas, + ); + } + + + #[view] + /// Checks whether the executor is paused + public fun is_paused(): bool { + worker_config::is_worker_paused(@executor) + } + + /// Sets the price feed module address and the feed address for the executor + public entry fun set_price_feed( + account: &signer, + price_feed: address, + feed_address: address, + ) acquires ExecutorConfig { + assert_admin(address_of(move account)); + worker_config::set_price_feed(call_ref(), price_feed, feed_address); + } + + /// Sets a price feed delegate for the executor + /// This is used to allow the executor delegate to the configuration defined in another module + /// When there is a delegation, the worker_config get_effective_price_feed will return the price feed of the + /// delegate + public entry fun set_price_feed_delegate(account: &signer, price_feed_delegate: address) acquires ExecutorConfig { + assert_admin(address_of(move account)); + worker_config::set_price_feed_delegate(call_ref(), price_feed_delegate); + } + + /// Add (allowed = true) or remove (allowed = false) a sender to/from the allowlist + /// A non-empty allowlist restrict senders to only those on the list (minus any on the denylist) + public entry fun set_allowlist( + account: &signer, + oapp: address, + allowed: bool, + ) acquires ExecutorConfig { + assert_admin(address_of(move account)); + worker_config::set_allowlist(call_ref(), oapp, allowed); + } + + /// Add (denied = true) or remove (denied = false) a sender from the denylist + /// Denylist members will be disallowed from interacting with the executor regardless of allowlist status + public entry fun set_denylist( + account: &signer, + oapp: address, + denied: bool, + ) acquires ExecutorConfig { + assert_admin(address_of(move account)); + worker_config::set_denylist(call_ref(), oapp, denied); + } + + /// Sets the supported message libraries for the executor + /// The provided list will entirely replace the previously configured list + public entry fun set_supported_msglibs( + account: &signer, + msglibs: vector
, + ) acquires ExecutorConfig { + assert_admin(address_of(move account)); + worker_config::set_supported_msglibs(call_ref(), msglibs); + } + + /// Sets the fee lib for the executor + public entry fun set_fee_lib( + account: &signer, + fee_lib: address, + ) acquires ExecutorConfig { + assert_admin(address_of(move account)); + worker_config::set_worker_fee_lib(call_ref(), fee_lib); + } + + // ================================================ View Functions ================================================ + + #[view] + /// Gets the list of role admins for the executor + public fun get_role_admins(): vector
{ + worker_config::get_worker_role_admins(@executor) + } + + #[view] + /// Gets the list of admins for the executor + public fun get_admins(): vector
{ + worker_config::get_worker_admins(@executor) + } + + #[view] + /// Gets the deposit address for the executor + public fun get_deposit_address(): address { + worker_config::get_deposit_address(@executor) + } + + #[view] + /// Gets the list of supported message libraries for the executor + public fun get_supported_msglibs(): vector
{ worker_config::get_supported_msglibs(@executor) } + + #[view] + /// Gets the fee lib address selected by the executor + public fun get_fee_lib(): address { + worker_config::get_worker_fee_lib(@executor) + } + + #[view] + /// Gets multiplier premium for the executor + public fun get_default_multiplier_bps(): u16 { + worker_config::get_default_multiplier_bps(@executor) + } + + #[view] + /// Gets the supported option types for the executor + public fun get_supported_option_types(): vector { + worker_config::get_supported_option_types(@executor) + } + + #[view] + /// Returns whether a sender is on the executor's allowlist + public fun allowlist_contains(sender: address): bool { worker_config::allowlist_contains(@executor, sender) } + + #[view] + /// Returns whether a sender is on the executor's denylist + public fun denylist_contains(sender: address): bool { worker_config::denylist_contains(@executor, sender) } + + #[view] + /// Returns whether a sender is allowed to interact with the executor based on the allowlist and denylist + public fun is_allowed(sender: address): bool { worker_config::is_allowed(@executor, sender) } + + #[view] + /// Gets the destination config for an eid on the executor + /// @return the lz_receive_base_gas, multiplier_bps, floor_margin_usd, native_cap, lz_compose_base_gas + public fun get_dst_config(dst_eid: u32): (u64, u16, u128, u128, u64) { + worker_config::get_executor_dst_config_values(@executor, dst_eid) + } + + #[view] + /// Get the number of other workers that are currently delegating to this executor's price feed configuration + public fun get_count_price_feed_delegate_dependents(): u64 { + worker_config::get_count_price_feed_delegate_dependents(@executor) + } + + // ================================================ Helper Functions ================================================ + + /// Asserts that an an address is a role admin + inline fun assert_role_admin(role_admin: address) { + worker_config::assert_worker_role_admin(@executor, role_admin); + } + + /// Asserts that an address is an admin + inline fun assert_admin(admin: address) { + worker_config::assert_worker_admin(@executor, admin); + } + + /// Derives the call ref targetting the worker_common module + inline fun call_ref(): &CallRef { + let contract_signer = &borrow_global(@executor).contract_signer; + &make_call_ref(contract_signer) + } + + // ==================================================== Events ==================================================== + + #[event] + /// Emits when a Native Drop is Applied + struct NativeDropApplied has store, drop { + src_eid: u32, + sender: vector, + nonce: u64, + dst_eid: u32, + oapp: address, + params: vector, + } + + #[event] + struct LzReceiveValueProvided has store, drop { + receiver: address, + src_eid: u32, + sender: vector, + nonce: u64, + guid: vector, + lz_receive_value: u64, + } + + #[event] + struct LzComposeValueProvided has store, drop { + from: address, + to: address, + index: u16, + guid: vector, + lz_compose_value: u64, + } + + #[test_only] + /// Creates a NativeDropApplied event for testing + public fun native_drop_applied_event( + src_eid: u32, + sender: vector, + nonce: u64, + dst_eid: u32, + oapp: address, + params: vector, + ): NativeDropApplied { + NativeDropApplied { + src_eid, + sender, + nonce, + dst_eid, + oapp, + params, + } + } + + #[test_only] + public fun lz_receive_value_provided_event( + receiver: address, + src_eid: u32, + sender: vector, + nonce: u64, + guid: vector, + lz_receive_value: u64, + ): LzReceiveValueProvided { + LzReceiveValueProvided { receiver, src_eid, sender, nonce, guid, lz_receive_value } + } + + #[test_only] + public fun lz_compose_value_provided_event( + from: address, + to: address, + index: u16, + guid: vector, + lz_compose_value: u64, + ): LzComposeValueProvided { + LzComposeValueProvided { from, to, index, guid, lz_compose_value } + } + + // ================================================== Error Codes ================================================= + + const EUNAUTHORIZED: u64 = 1; +} diff --git a/packages/layerzero-v2/aptos/contracts/workers/executor/sources/types/native_drop_params.move b/packages/layerzero-v2/aptos/contracts/workers/executor/sources/types/native_drop_params.move new file mode 100644 index 00000000..19df1c94 --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/workers/executor/sources/types/native_drop_params.move @@ -0,0 +1,52 @@ +module executor::native_drop_params { + use std::vector; + + use endpoint_v2_common::serde; + + struct NativeDropParams has copy, store, drop { + receiver: address, + amount: u64, + } + + public fun new_native_drop_params(receiver: address, amount: u64): NativeDropParams { + NativeDropParams { receiver, amount } + } + + public fun unpack_native_drop_params(params: NativeDropParams): (address, u64) { + let NativeDropParams { receiver, amount } = params; + (receiver, amount) + } + + public fun deserialize_native_drop_params(params_serialized: &vector): vector { + let params = vector[]; + let pos = 0; + let len = vector::length(params_serialized); + while (pos < len) { + let receiver = serde::extract_address(params_serialized, &mut pos); + let amount = serde::extract_u64(params_serialized, &mut pos); + vector::push_back(&mut params, new_native_drop_params(receiver, amount)); + }; + params + } + + public fun serialize_native_drop_params(params: vector): vector { + let params_serialized = vector[]; + for (i in 0..vector::length(¶ms)) { + let item = vector::borrow(¶ms, i); + let receiver = item.receiver; + let amount = item.amount; + serde::append_address(&mut params_serialized, receiver); + serde::append_u64(&mut params_serialized, amount); + }; + params_serialized + } + + public fun calculate_total_amount(params: vector): u64 { + let total_amount = 0; + for (i in 0..vector::length(¶ms)) { + let item = vector::borrow(¶ms, i); + total_amount = total_amount + item.amount; + }; + total_amount + } +} diff --git a/packages/layerzero-v2/aptos/contracts/workers/executor/tests/executor_tests.move b/packages/layerzero-v2/aptos/contracts/workers/executor/tests/executor_tests.move new file mode 100644 index 00000000..cfc8b02e --- /dev/null +++ b/packages/layerzero-v2/aptos/contracts/workers/executor/tests/executor_tests.move @@ -0,0 +1,283 @@ +#[test_only] +module executor::executor_tests { + use std::account::{create_account_for_test, create_signer_for_test}; + use std::event::was_event_emitted; + use std::fungible_asset; + use std::fungible_asset::Metadata; + use std::object::address_to_object; + use std::primary_fungible_store::ensure_primary_store_exists; + + use endpoint_v2_common::bytes32::{Self, from_bytes32}; + use endpoint_v2_common::native_token_test_helpers::mint_native_token_for_test; + use executor::executor; + use executor::executor::{get_fee_lib, native_drop, native_drop_applied_event}; + use executor::native_drop_params; + use executor::native_drop_params::new_native_drop_params; + use worker_common::worker_config; + + const EXECUTOR_FEE_LIB: address = @100000009; + const PRICE_FEED_MODULE: address = @100000010; + + #[test] + fun test_native_drop() { + // initialize worker + executor::init_module_for_test(); + executor::initialize( + &create_account_for_test(@executor), + @executor, + @111111111, + vector[@9999], + vector[], + EXECUTOR_FEE_LIB, + ); + + let params = vector[ + new_native_drop_params(@1111, 10), + new_native_drop_params(@2222, 20), + new_native_drop_params(@3333, 30), + ]; + let fa = mint_native_token_for_test(1000); + let metadata = address_to_object(@native_token_metadata_address); + let admin_store = ensure_primary_store_exists(@9999, metadata); + fungible_asset::deposit(admin_store, fa); + + let admin = &create_signer_for_test(@9999); + native_drop( + admin, + 10101, + bytes32::from_bytes32(bytes32::from_address(@12345)), + 10, + 10202, + @50000, + native_drop_params::serialize_native_drop_params(params), + ); + + assert!(fungible_asset::balance(admin_store) == 940, 0); + assert!(was_event_emitted(&native_drop_applied_event( + 10101, from_bytes32(bytes32::from_address(@12345)), 10, 10202, @50000, params)), 0); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::EUNAUTHORIZED)] + fun test_native_drop_fails_if_not_admin() { + // initialize worker + executor::init_module_for_test(); + executor::initialize( + &create_account_for_test(@executor), + @executor, + @111111111, + vector[@9999], + vector[], + EXECUTOR_FEE_LIB, + ); + + let params = vector[ + new_native_drop_params(@1111, 10), + new_native_drop_params(@2222, 20), + new_native_drop_params(@3333, 30), + ]; + let fa = mint_native_token_for_test(1000); + let metadata = address_to_object(@native_token_metadata_address); + let admin_store = ensure_primary_store_exists(@9999, metadata); + fungible_asset::deposit(admin_store, fa); + + let admin = &create_signer_for_test(@9900); + native_drop( + admin, + 10101, + bytes32::from_bytes32(bytes32::from_address(@12345)), + 10, + 10202, + @50000, + native_drop_params::serialize_native_drop_params(params), + ); + } + + #[test] + #[expected_failure(abort_code = 0x10004, location = std::fungible_asset)] + fun test_native_drop_fails_if_insufficient_balance() { + // initialize worker + executor::init_module_for_test(); + executor::initialize( + &create_account_for_test(@executor), + @executor, + @111111111, + vector[@9999], + vector[], + EXECUTOR_FEE_LIB, + ); + + let params = vector[ + new_native_drop_params(@1111, 10), + new_native_drop_params(@2222, 20), + new_native_drop_params(@3333, 30), + ]; + let fa = mint_native_token_for_test(1); + let metadata = address_to_object(@native_token_metadata_address); + let admin_store = ensure_primary_store_exists(@9999, metadata); + fungible_asset::deposit(admin_store, fa); + + let admin = &create_signer_for_test(@9999); + native_drop( + admin, + 10101, + bytes32::from_bytes32(bytes32::from_address(@12345)), + 10, + 10202, + @50000, + native_drop_params::serialize_native_drop_params(params), + ); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::EUNAUTHORIZED)] + fun test_set_price_feed_should_fail_if_not_admin() { + // initialize worker + executor::init_module_for_test(); + executor::initialize( + &create_account_for_test(@executor), + @executor, + @111111111, + vector[@9999], + vector[], + EXECUTOR_FEE_LIB, + ); + + let admin = &create_signer_for_test(@8888); + executor::set_price_feed(admin, PRICE_FEED_MODULE, @1111); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::EUNAUTHORIZED)] + fun test_set_price_feed_delegate_should_fail_if_not_admin() { + // initialize worker + executor::init_module_for_test(); + executor::initialize( + &create_account_for_test(@executor), + @executor, + @111111111, + vector[@9999], + vector[], + EXECUTOR_FEE_LIB, + ); + + let admin = &create_signer_for_test(@8888); + executor::set_price_feed_delegate(admin, PRICE_FEED_MODULE); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::EUNAUTHORIZED)] + fun test_set_allowlist_should_fail_if_not_admin() { + // initialize worker + executor::init_module_for_test(); + executor::initialize( + &create_account_for_test(@executor), + @executor, + @111111111, + vector[@9999], + vector[], + EXECUTOR_FEE_LIB, + ); + + let admin = &create_signer_for_test(@8888); + executor::set_allowlist(admin, @1234, true); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::EUNAUTHORIZED)] + fun test_set_denylist_should_fail_is_not_admin() { + // initialize worker + executor::init_module_for_test(); + executor::initialize( + &create_account_for_test(@executor), + @executor, + @111111111, + vector[@9999], + vector[], + EXECUTOR_FEE_LIB, + ); + + let admin = &create_signer_for_test(@8888); + executor::set_denylist(admin, @1234, true); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::EUNAUTHORIZED)] + fun test_set_supported_msglibs_should_fail_if_not_admin() { + // initialize worker + executor::init_module_for_test(); + executor::initialize( + &create_account_for_test(@executor), + @executor, + @111111111, + vector[@9999], + vector[], + EXECUTOR_FEE_LIB, + ); + + let admin = &create_signer_for_test(@8888); + executor::set_supported_msglibs(admin, vector[@1234]); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::EUNAUTHORIZED)] + fun set_admin_should_fail_if_not_role_admin() { + // initialize worker + executor::init_module_for_test(); + executor::initialize( + &create_account_for_test(@executor), + @executor, + @111111111, + vector[@9999], + vector[], + EXECUTOR_FEE_LIB, + ); + + let admin = &create_signer_for_test(@9999); + executor::set_admin(admin, @8888, true); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::EUNAUTHORIZED)] + fun set_role_admin_should_fail_if_not_role_admin() { + // initialize worker + executor::init_module_for_test(); + executor::initialize( + &create_account_for_test(@executor), + @executor, + @111111111, + vector[@9999], + vector[], + EXECUTOR_FEE_LIB, + ); + + let admin = &create_signer_for_test(@9999); + executor::set_role_admin(admin, @8888, true); + } + + #[test] + fun set_fee_lib() { + // initialize worker + executor::init_module_for_test(); + executor::initialize( + &create_account_for_test(@executor), + @executor, + @111111111, + vector[@9999], + vector[], + EXECUTOR_FEE_LIB, + ); + let fee_lib_from_worker_config = worker_config::get_worker_fee_lib(@executor); + assert!(fee_lib_from_worker_config == EXECUTOR_FEE_LIB, 0); + let fee_lib_from_executor = get_fee_lib(); + assert!(fee_lib_from_executor == EXECUTOR_FEE_LIB, 0); + + let admin = &create_signer_for_test(@9999); + executor::set_fee_lib(admin, @1111); + assert!(was_event_emitted(&worker_config::worker_fee_lib_updated_event(@executor, @1111)), 0); + let fee_lib_from_worker_config = worker_config::get_worker_fee_lib(@executor); + assert!(fee_lib_from_worker_config == @1111, 0); + let fee_lib_from_executor = get_fee_lib(); + assert!(fee_lib_from_executor == @1111, 0); + } +} diff --git a/packages/layerzero-v2/initia/contracts/endpoint_v2/Move.toml b/packages/layerzero-v2/initia/contracts/endpoint_v2/Move.toml new file mode 100644 index 00000000..0eeaa2a8 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/endpoint_v2/Move.toml @@ -0,0 +1,66 @@ +[package] +name = "endpoint_v2" +version = "1.0.0" +authors = [] + +[addresses] +router_node_0 = "_" +simple_msglib = "_" +blocked_msglib = "_" +uln_302 = "_" +router_node_1 = "_" +endpoint_v2_common = "_" +endpoint_v2 = "_" +layerzero_admin = "_" +layerzero_treasury_admin = "_" +msglib_types = "_" +treasury = "_" +executor_fee_lib_router_0 = "_" +executor_fee_lib_router_1 = "_" +dvn_fee_lib_router_0 = "_" +dvn_fee_lib_router_1 = "_" +price_feed_router_0 = "_" +price_feed_router_1 = "_" +price_feed_module_0 = "_" +worker_common = "_" +executor_fee_lib_0 = "_" +dvn_fee_lib_0 = "_" +dvn = "_" +native_token_metadata_address = "0x8e4733bdabcf7d4afc3d14f0dd46c9bf52fb0fce9e4b996c939e195b8bc891d9" + +[dev-addresses] +router_node_0 = "0x9001" +simple_msglib = "0x9002" +blocked_msglib = "0x9003" +uln_302 = "0x9005" +router_node_1 = "0x9007" +endpoint_v2_common = "0x9098" +endpoint_v2 = "0x12345678" +layerzero_admin = "0x18943124" +layerzero_treasury_admin = "0x1894312499" +msglib_types = "0x5211234" +treasury = "0x123432432" +executor_fee_lib_router_0 = "0x30001" +executor_fee_lib_router_1 = "0x3465342143" +dvn_fee_lib_router_0 = "0x30001a" +dvn_fee_lib_router_1 = "0x3465342143a" +price_feed_router_0 = "0x65DD71A" +price_feed_router_1 = "0x65DD71AB" +price_feed_module_0 = "0x65DD71" +worker_common = "0x3999" +executor_fee_lib_0 = "0x3000" +dvn_fee_lib_0 = "0x3000a" +dvn = "0x22314321" + +[dev-dependencies] +simple_msglib = { local = "../msglib/libs/simple_msglib" } +uln_302 = { local = "../msglib/libs/uln_302" } +worker_common = { local = "../worker_peripherals/worker_common" } +executor_fee_lib_0 = { local = "../worker_peripherals/fee_libs/executor_fee_lib_0" } +dvn_fee_lib_0 = { local = "../worker_peripherals/fee_libs/dvn_fee_lib_0" } + +[dependencies] +msglib_types = { local = "../msglib/msglib_types" } +router_node_0 = { local = "../msglib/routers/router_node_0" } +endpoint_v2_common = { local = "../endpoint_v2_common" } +price_feed_module_0 = { local = "../worker_peripherals/price_feed_modules/price_feed_module_0" } \ No newline at end of file diff --git a/packages/layerzero-v2/initia/contracts/endpoint_v2/sources/admin.move b/packages/layerzero-v2/initia/contracts/endpoint_v2/sources/admin.move new file mode 100644 index 00000000..89972ffe --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/endpoint_v2/sources/admin.move @@ -0,0 +1,61 @@ +/// This module isolates functions related to Endpoint administration +module endpoint_v2::admin { + use std::signer::address_of; + + use endpoint_v2::msglib_manager; + + /// Register new message library + /// A library must be registered to be called by this endpoint. Once registered, it cannot be unregistered. The + /// library must be connected to the router node before it can be registered + public entry fun register_library(account: &signer, lib: address) { + assert_admin(address_of(move account)); + msglib_manager::register_library(lib); + } + + /// Sets the default sending message library for the given destination EID + public entry fun set_default_send_library(account: &signer, dst_eid: u32, lib: address) { + assert_admin(address_of(move account)); + msglib_manager::set_default_send_library(dst_eid, lib); + } + + /// Set the default receive message library for the given source EID + public entry fun set_default_receive_library( + account: &signer, + src_eid: u32, + lib: address, + grace_period: u64, + ) { + assert_admin(address_of(move account)); + msglib_manager::set_default_receive_library(src_eid, lib, grace_period); + } + + /// Updates the default receive library timeout for the given source EID + /// The provided expiry is in a specific block number. The fallback library will be disabled once the block height + /// equals this block number + public entry fun set_default_receive_library_timeout( + account: &signer, + src_eid: u32, + fallback_lib: address, + expiry: u64, + ) { + assert_admin(address_of(move account)); + msglib_manager::set_default_receive_library_timeout(src_eid, fallback_lib, expiry); + } + + // ==================================================== Helpers =================================================== + + /// Internal function to assert that the caller is the endpoint admin + inline fun assert_admin(admin: address) { + assert!(admin == @layerzero_admin, EUNAUTHORIZED); + } + + #[test_only] + /// Test-only function to initialize the endpoint and EID + public fun initialize_endpoint_for_test() { + endpoint_v2::store::init_module_for_test(); + } + + // ================================================== Error Codes ================================================= + + const EUNAUTHORIZED: u64 = 1; +} diff --git a/packages/layerzero-v2/initia/contracts/endpoint_v2/sources/endpoint.move b/packages/layerzero-v2/initia/contracts/endpoint_v2/sources/endpoint.move new file mode 100644 index 00000000..7813b710 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/endpoint_v2/sources/endpoint.move @@ -0,0 +1,597 @@ +/// Primary entrypoint for all OApps to interact with the LayerZero protocol +module endpoint_v2::endpoint { + use std::fungible_asset::FungibleAsset; + use std::option::Option; + use std::signer::address_of; + use std::string::String; + + use endpoint_v2::channels; + use endpoint_v2::messaging_composer; + use endpoint_v2::messaging_receipt::MessagingReceipt; + use endpoint_v2::msglib_manager; + use endpoint_v2::registration; + use endpoint_v2::timeout; + use endpoint_v2_common::bytes32::{Self, Bytes32, from_bytes32}; + use endpoint_v2_common::contract_identity::{CallRef, get_call_ref_caller}; + use endpoint_v2_common::packet_raw; + use endpoint_v2_common::packet_v1_codec::compute_payload; + + #[test_only] + friend endpoint_v2::endpoint_tests; + + // ============================================== OApp Administration ============================================= + + struct EndpointOAppConfigTarget {} + + /// Initialize the OApp while registering the lz_receive module name + /// + /// The lz_receive module selection is permanent and cannot be changed + /// When delivering a message, the offchain entity will call `::::lz_receive()` + public fun register_oapp(oapp: &signer, lz_receive_module: String) { + let oapp_address = address_of(move oapp); + registration::register_oapp(oapp_address, lz_receive_module); + } + + /// Registers the Composer with the lz_compose function + /// + /// This function must be called before the Composer can start to receive composer messages + /// The lz_compose module selection is permanent and cannot be changed + public fun register_composer(composer: &signer, lz_compose_module: String) { + let composer_address = address_of(move composer); + registration::register_composer(composer_address, lz_compose_module); + } + + /// Register a Receive pathway for the given remote EID + /// + /// This function must be called before any message can be verified + /// After a message is verified, the lz_receive() function call is permissionless; therefore, this provides + /// important protection against the verification of messages from an EID, whose security configuration has not been + /// accepted by the OApp. + /// + /// Once a pathway is registered, receives can happen under either the default configuration or the configuration + /// provided by the OApp. + public fun register_receive_pathway(call_ref: &CallRef, src_eid: u32, sender: Bytes32) { + let oapp = get_oapp_caller(call_ref); + channels::register_receive_pathway(oapp, src_eid, sender) + } + + /// Set the send library for a given EID + /// + /// Setting to a @0x0 msglib address will unset the send library and cause the OApp to use the default instead + public fun set_send_library(call_ref: &CallRef, remote_eid: u32, msglib: address) { + let oapp = get_oapp_caller(call_ref); + msglib_manager::set_send_library(oapp, remote_eid, msglib); + } + + /// Set the receiving message library for the given remote EID + /// + /// Setting to a @0x0 msglib address will unset the receive library and cause the OApp to use the default instead + /// The `grace_period` is the maximum number of blocks that the OApp will continue to accept a message from the + /// prior receive library. + /// + /// A grace period cannot be set when switching to or from the unset / default setting. + /// + /// To emulate a grace period when switching from default, first set the receive library explicitly to the default + /// with no grace period, then set to the desired new library with a grace period. + /// To emulate a grace period when switching to default, first set the receive library explicitly to the default + /// library address with a grace period, then when the grace period expires, unset the receive library (set to @0x0) + /// without a grace period + public fun set_receive_library( + call_ref: &CallRef, + remote_eid: u32, + msglib: address, + grace_period: u64, + ) { + let oapp = get_oapp_caller(call_ref); + msglib_manager::set_receive_library(oapp, remote_eid, msglib, grace_period); + } + + /// Update the timeout settings for the receive library + /// + /// The `expiry` is the block number at which the OApp will no longer confirm message using the provided + /// fallback receive library. + /// + /// The timeout cannot be set to fall back to the default receive library (@0x0) or when the current receive library + /// is unset (uses the default). This will also revert if the expiry is not set to a future block number. + public fun set_receive_library_timeout( + call_ref: &CallRef, + remote_eid: u32, + msglib: address, + expiry: u64, + ) { + let oapp = get_oapp_caller(call_ref); + msglib_manager::set_receive_library_timeout(oapp, remote_eid, msglib, expiry); + } + + /// Set the serialized message Library configuration using the serialized format for a specific message library + public fun set_config( + call_ref: &CallRef, + msglib: address, + eid: u32, + config_type: u32, + config: vector, + ) { + let oapp = get_oapp_caller(call_ref); + msglib_manager::set_config(oapp, msglib, eid, config_type, config); + } + + // ==================================================== Sending =================================================== + + struct EndpointV2SendingTarget {} + + /// Gets a quote for a message given all the send parameters + /// + /// The response is the (Native fee, ZRO fee). If `pay_in_zro` is true, than whatever portion of the fee that + /// can be paid in ZRO wil be returned as the ZRO fee, and the remaining portion will be returned as the Native fee. + /// If `pay_in_zro` is false, the entire fee will be returned as the Native fee, and 0 will be returned as the ZRO + /// fee. + public fun quote( + sender: address, + dst_eid: u32, + receiver: Bytes32, + message: vector, + options: vector, + pay_in_zro: bool, + ): (u64, u64) { + channels::quote(sender, dst_eid, receiver, message, options, pay_in_zro) + } + + #[view] + /// View function to get a quote for a message given all the send parameters + public fun quote_view( + sender: address, + dst_eid: u32, + receiver: vector, + message: vector, + options: vector, + pay_in_zro: bool, + ): (u64, u64) { + channels::quote(sender, dst_eid, bytes32::to_bytes32(receiver), message, options, pay_in_zro) + } + + /// Send a message through the LayerZero protocol + /// + /// This will be passed to the Message Library to calculate the fee of the message and emit messages that trigger + /// offchain entities to deliver and verify the message. If `zro_token` is not option::none, whatever component of + /// the fee that can be paid in ZRO will be paid in ZRO. + /// If the amounts provided are insuffient, the transaction will revert + public fun send( + call_ref: &CallRef, + dst_eid: u32, + receiver: Bytes32, + message: vector, + options: vector, + native_token: &mut FungibleAsset, + zro_token: &mut Option, + ): MessagingReceipt { + let sender = get_oapp_caller(call_ref); + channels::send( + sender, + dst_eid, + receiver, + message, + options, + native_token, + zro_token, + ) + } + + // ============================================= Clearing "Hot Potato" ============================================ + + /// Non-droppable guid that requires a call to `clear()` or `clear_compose()` to delete + struct WrappedGuid { guid: Bytes32 } + + struct WrappedGuidAndIndex { guid: Bytes32, index: u16 } + + /// Wrap a guid (lz_receive), so that it can only be unwrapped (and disposed of) by the endpoint + public fun wrap_guid(guid: Bytes32): WrappedGuid { WrappedGuid { guid } } + + /// Wrap a guid and index (lz_compose), so that it can only be unwrapped (and disposed of) by the endpoint + public fun wrap_guid_and_index(guid: Bytes32, index: u16): WrappedGuidAndIndex { + WrappedGuidAndIndex { guid, index } + } + + /// Get the guid from a WrappedGuid without destorying the WrappedGuid + public fun get_guid_from_wrapped(wrapped: &WrappedGuid): Bytes32 { wrapped.guid } + + /// Get the guid and index from a WrappedGuidAndIndex without destorying the WrappedGuidAndIndex + public fun get_guid_and_index_from_wrapped(wrapped: &WrappedGuidAndIndex): (Bytes32, u16) { + (wrapped.guid, wrapped.index) + } + + /// Endpoint-only call to destroy a WrappedGuid and provide its value; only called by `clear()` + fun unwrap_guid(wrapped_guid: WrappedGuid): Bytes32 { + let WrappedGuid { guid } = wrapped_guid; + guid + } + + /// Endpoint-only call to destroy a WrappedGuidAndIndex and provide its values; only called by `clear_compose()` + fun unwrap_guid_and_index(wrapped_guid_and_index: WrappedGuidAndIndex): (Bytes32, u16) { + let WrappedGuidAndIndex { guid, index } = wrapped_guid_and_index; + (guid, index) + } + + // ==================================================== Receiving ================================================= + + struct EndpointV2ReceivingTarget {} + + /// Validate and Clear a message received through the LZ Receive protocol + /// + /// This should be called by the lz_receive function on the OApp. + /// In this "pull model", the off-chain entity will call the OApp's lz_receive function. The lz_receive function + /// must call `clear()` to validate and clear the message. If `clear()` is not called, lz_receive may receive + /// messages but will not be able to ensure the validity of the messages. + public fun clear( + call_ref: &CallRef, + src_eid: u32, + sender: Bytes32, + nonce: u64, + guid: WrappedGuid, + message: vector, + ) { + let oapp = get_oapp_caller(call_ref); + channels::clear_payload( + oapp, + src_eid, + sender, + nonce, + compute_payload(unwrap_guid(guid), message) + ); + } + + /// Function to alert that the lz_receive function has reverted + /// + /// This may be used to create a record that the offchain entity has made an unsuccessful attempt to deliver a + /// message. + public entry fun lz_receive_alert( + executor: &signer, + receiver: address, + src_eid: u32, + sender: vector, + nonce: u64, + guid: vector, + gas: u64, + value: u64, + message: vector, + extra_data: vector, + reason: String, + ) { + channels::lz_receive_alert( + receiver, + address_of(executor), + src_eid, + bytes32::to_bytes32(sender), + nonce, + bytes32::to_bytes32(guid), + gas, + value, + message, + extra_data, + reason, + ); + } + + /// This prevents verification of the next message in the sequence. An example use case is to skip + /// a message when precrime throws an alert. The nonce must be provided to avoid the possibility of skipping the + /// unintended nonce. + public fun skip( + call_ref: &CallRef, + src_eid: u32, + sender: Bytes32, + nonce: u64, + ) { + let oapp = get_oapp_caller(call_ref); + channels::skip(oapp, src_eid, sender, nonce); + } + + /// This prevents delivery and any possible reverification of an already verified message + public fun burn( + call_ref: &CallRef, + src_eid: u32, + sender: Bytes32, + nonce: u64, + payload_hash: Bytes32, + ) { + let oapp = get_oapp_caller(call_ref); + channels::burn(oapp, src_eid, sender, nonce, payload_hash); + } + + /// This maintains a packet's status as verified but prevents delivery until the packet is verified again + /// A non-verified nonce can be nilified by passing 0x00*32 as the payload hashs + public fun nilify( + call_ref: &CallRef, + src_eid: u32, + sender: Bytes32, + nonce: u64, + payload_hash: Bytes32, + ) { + let oapp = get_oapp_caller(call_ref); + channels::nilify(oapp, src_eid, sender, nonce, payload_hash); + } + + + // ==================================================== Compose =================================================== + + struct EndpointV2SendComposeTarget {} + + struct EndpointV2ComposerTarget {} + + /// Initiates a compose message + /// + /// This should be called by the lz_receive function on the OApp when it receives a message that indicates that + /// a compose is required. The composer will then call the target Composer's lz_compose with the message function to + /// complete the compose + public fun send_compose( + call_ref: &CallRef, + to: address, + index: u16, + guid: Bytes32, + message: vector, + ) { + let caller = get_call_ref_caller(call_ref); + messaging_composer::send_compose(caller, to, index, guid, message); + } + + /// Internal function to get the address of the OApp caller, and assert that the caller is a registered OApp and + /// the endpoint is the intended recipient + public(friend) fun get_oapp_caller(call_ref: &CallRef): address { + let caller = get_call_ref_caller(call_ref); + assert!(registration::is_registered_oapp(caller), EUNREGISTERED); + caller + } + + // =================================================== Composer =================================================== + + /// Function to clear the compose message + /// This should be called from the Composer's lz_compose function. This will both check the validity of the message + /// and clear the message from the Composer's compose queue. If this is not called, the Composer will not be able to + /// ensure the validity of received messages + public fun clear_compose( + call_ref: &CallRef, + from: address, + guid_and_index: WrappedGuidAndIndex, + message: vector, + ) { + let composer = get_compose_caller(call_ref); + let (guid, index) = unwrap_guid_and_index(guid_and_index); + messaging_composer::clear_compose(from, composer, guid, index, message); + } + + /// Function to alert that the lz_compose function has reverted + /// + /// This may be used to create a record that the offchain composer has made an unsuccessful attempt to deliver a + /// compose message + public entry fun lz_compose_alert( + executor: &signer, + from: address, + to: address, + guid: vector, + index: u16, + gas: u64, + value: u64, + message: vector, + extra_data: vector, + reason: String, + ) { + messaging_composer::lz_compose_alert( + address_of(move executor), + from, + to, + bytes32::to_bytes32(guid), + index, + gas, + value, + message, + extra_data, + reason, + ); + } + + /// Internal function to get the address of the Composer caller, and assert that the caller is a registered Composer + /// and that the endpoint is the intended recipient + public(friend) fun get_compose_caller(call_ref: &CallRef): address { + let caller = get_call_ref_caller(call_ref); + assert!(registration::is_registered_composer(caller), EUNREGISTERED); + caller + } + + // ================================================= Verification ================================================= + + + /// This verifies a message by storing the payload hash on the receive channel + /// + /// Once a message has been verified, it can be permissionlessly delivered. This will revert if the message library + /// has not committed the verification. + public entry fun verify( + receive_lib: address, + packet_header: vector, + payload_hash: vector, + extra_data: vector, + ) { + let packet_header = packet_raw::bytes_to_raw_packet(packet_header); + channels::verify(receive_lib, packet_header, bytes32::to_bytes32(payload_hash), extra_data); + } + + /// Confirms that a message has been verified by the message library, and is ready to be verified (confirmed) on the + /// endpoint. + /// + /// This can be called prior to calling `verify` to ensure that the message is ready to be verified. + /// This is also exposed as a view function below called `verifiable_view()` + public fun verifiable( + src_eid: u32, + sender: Bytes32, + nonce: u64, + receiver: address, + ): bool { + channels::verifiable(receiver, src_eid, sender, nonce) + } + + /// Confirms that a message has a payload hash (has been verified). + /// + /// This generally suggests that the message can be delievered, with the exeption of the case where the message has + /// been nilified + public fun has_payload_hash( + src_eid: u32, + sender: Bytes32, + nonce: u64, + receiver: address, + ): bool { + channels::has_payload_hash(receiver, src_eid, sender, nonce) + } + + /// Gets the payload hash for a given message + public fun payload_hash(receiver: address, src_eid: u32, sender: Bytes32, nonce: u64): Bytes32 { + channels::get_payload_hash(receiver, src_eid, sender, nonce) + } + + // ===================================================== View ===================================================== + + // Get the serialized message Library configuration using the serialized format for a specific message library + #[view] + public fun get_config(oapp: address, msglib: address, eid: u32, config_type: u32): vector { + msglib_manager::get_config(oapp, msglib, eid, config_type) + } + + #[view] + public fun get_quote( + sender: address, + dst_eid: u32, + receiver: vector, + message: vector, + options: vector, + pay_in_zro: bool, + ): (u64, u64) { + quote(sender, dst_eid, bytes32::to_bytes32(receiver), message, options, pay_in_zro) + } + + #[view] + public fun get_payload_hash(receiver: address, src_eid: u32, sender: vector, nonce: u64): vector { + from_bytes32(payload_hash(receiver, src_eid, bytes32::to_bytes32(sender), nonce)) + } + + #[view] + /// This is called by offchain entities to know what function to call on the OApp for lz_receive + public fun get_lz_receive_module(oapp: address): String { + registration::lz_receive_module(oapp) + } + + #[view] + /// This is called by offchain entities to know what function to call on the Composer for lz_compose + public fun get_lz_compose_module(composer: address): String { + registration::lz_compose_module(composer) + } + + #[view] + /// Gets the effective receive library for the given source EID, returns both the library and a flag indicating if + /// this is a fallback to the default (meaning the library is not configured for the oapp) + public fun get_effective_receive_library(oapp_address: address, src_eid: u32): (address, bool) { + msglib_manager::get_effective_receive_library(oapp_address, src_eid) + } + + #[view] + public fun get_effective_send_library(oapp_address: address, dst_eid: u32): (address, bool) { + msglib_manager::get_effective_send_library(oapp_address, dst_eid) + } + + #[view] + public fun get_outbound_nonce(sender: address, dst_eid: u32, receiver: vector): u64 { + channels::outbound_nonce(sender, dst_eid, bytes32::to_bytes32(receiver)) + } + + #[view] + public fun get_lazy_inbound_nonce(receiver: address, src_eid: u32, sender: vector): u64 { + channels::lazy_inbound_nonce(receiver, src_eid, bytes32::to_bytes32(sender)) + } + + #[view] + public fun get_inbound_nonce(receiver: address, src_eid: u32, sender: vector): u64 { + channels::inbound_nonce(receiver, src_eid, bytes32::to_bytes32(sender)) + } + + #[view] + public fun is_registered_library(msglib: address): bool { + msglib_manager::is_registered_library(msglib) + } + + #[view] + public fun is_valid_receive_library_for_oapp(oapp: address, src_eid: u32, msglib: address): bool { + msglib_manager::is_valid_receive_library_for_oapp(oapp, src_eid, msglib) + } + + #[view] + public fun has_payload_hash_view(src_eid: u32, sender: vector, nonce: u64, receiver: address): bool { + has_payload_hash(src_eid, bytes32::to_bytes32(sender), nonce, receiver) + } + + #[view] + /// Get the hash of the composed message with the given parameters with the following special case values: + /// NOT SENT: 0x00(*32) + /// SENT: hash value + /// RECEIVED / CLEARED: 0xff(*32) + public fun get_compose_message_hash(from: address, to: address, guid: vector, index: u16): vector { + from_bytes32( + messaging_composer::get_compose_message_hash(from, to, bytes32::to_bytes32(guid), index) + ) + } + + #[view] + public fun get_next_guid(oapp: address, dst_eid: u32, receiver: vector): vector { + from_bytes32(channels::next_guid(oapp, dst_eid, bytes32::to_bytes32(receiver))) + } + + #[view] + public fun initializable(src_eid: u32, sender: vector, receiver: address): bool { + channels::receive_pathway_registered(receiver, src_eid, bytes32::to_bytes32(sender)) + } + + #[view] + public fun verifiable_view( + src_eid: u32, + sender: vector, + nonce: u64, + receiver: address, + ): bool { + verifiable(src_eid, bytes32::to_bytes32(sender), nonce, receiver) + } + + #[view] + /// Get the default send library for the given destination EID + public fun get_default_send_library(remote_eid: u32): address { + msglib_manager::get_default_send_library(remote_eid) + } + + #[view] + /// Get the default receive library for the given source EID + public fun get_default_receive_library(remote_eid: u32): address { + msglib_manager::get_default_receive_library(remote_eid) + } + + #[view] + public fun get_registered_libraries(start_index: u64, max_entries: u64): vector
{ + msglib_manager::get_registered_libraries(start_index, max_entries) + } + + #[view] + public fun get_receive_library_timeout(oapp: address, remote_eid: u32): (u64, address) { + timeout::unpack_timeout(msglib_manager::get_receive_library_timeout(oapp, remote_eid)) + } + + #[view] + public fun get_default_receive_library_timeout(remote_eid: u32): (u64, address) { + timeout::unpack_timeout(msglib_manager::get_default_receive_library_timeout(remote_eid)) + } + + #[view] + public fun is_registered_oapp(oapp: address): bool { + registration::is_registered_oapp(oapp) + } + + #[view] + public fun is_registered_composer(composer: address): bool { + registration::is_registered_composer(composer) + } + + // ================================================== Error Codes ================================================= + + const EUNREGISTERED: u64 = 1; +} diff --git a/packages/layerzero-v2/initia/contracts/endpoint_v2/sources/internal/channels.move b/packages/layerzero-v2/initia/contracts/endpoint_v2/sources/internal/channels.move new file mode 100644 index 00000000..27a554ec --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/endpoint_v2/sources/internal/channels.move @@ -0,0 +1,610 @@ +module endpoint_v2::channels { + use std::event::emit; + use std::fungible_asset::{Self, FungibleAsset, Metadata}; + use std::object::{address_to_object, Object}; + use std::option::{Self, Option}; + use std::string::String; + + use endpoint_v2::messaging_receipt::{Self, MessagingReceipt}; + use endpoint_v2::msglib_manager; + use endpoint_v2::store; + use endpoint_v2_common::bytes32::{Self, Bytes32, from_bytes32, to_bytes32}; + use endpoint_v2_common::guid::compute_guid; + use endpoint_v2_common::packet_raw::{get_packet_bytes, RawPacket}; + use endpoint_v2_common::send_packet; + use endpoint_v2_common::send_packet::{new_send_packet, SendPacket}; + use endpoint_v2_common::universal_config; + use router_node_0::router_node as msglib_router; + + friend endpoint_v2::endpoint; + + #[test_only] + friend endpoint_v2::channels_tests; + + const NIL_PAYLOAD_HASH: vector = x"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; + const EMPTY_PAYLOAD_HASH: vector = x"0000000000000000000000000000000000000000000000000000000000000000"; + + // ==================================================== Helpers =================================================== + + inline fun is_native_token(native_metadata: Object): bool { + native_metadata == address_to_object(@native_token_metadata_address) + } + + // ==================================================== Sending =================================================== + + /// Generate the next GUID for the given destination EID + public(friend) fun next_guid(sender: address, dst_eid: u32, receiver: Bytes32): Bytes32 { + let next_nonce = store::outbound_nonce(sender, dst_eid, receiver) + 1; + compute_guid(next_nonce, universal_config::eid(), bytes32::from_address(sender), dst_eid, receiver) + } + + public(friend) fun outbound_nonce(sender: address, dst_eid: u32, receiver: Bytes32): u64 { + store::outbound_nonce(sender, dst_eid, receiver) + } + + /// Send a message to the given destination EID + public(friend) fun send( + sender: address, + dst_eid: u32, + receiver: Bytes32, + message: vector, + options: vector, + native_token: &mut FungibleAsset, + zro_token: &mut Option, + ): MessagingReceipt { + send_internal( + sender, + dst_eid, + receiver, + message, + options, + native_token, + zro_token, + // Message Library Router Send Function + |send_lib, send_packet| msglib_router::send( + send_lib, + &store::make_dynamic_call_ref(send_lib, b"send"), + send_packet, + options, + native_token, + zro_token, + ), + ) + } + + /// This send function receives the send message library call as a lambda function + /// + /// @param sender: The address of the sender + /// @param dst_eid: The destination EID + /// @param receiver: The receiver's address + /// @param message: The message to be sent + /// @param options: The options to be sent with the message + /// @param native_token: The native token to be sent with the message + /// @param zro_token: The optional ZRO token to be sent with the message + /// @param msglib_send: The lambda function that calls the send library + /// |send_lib, send_packet| (native_fee, zro_fee, encoded_packet): The lambda function that calls the send + /// library + public(friend) inline fun send_internal( + sender: address, + dst_eid: u32, + receiver: Bytes32, + message: vector, + options: vector, + native_token: &mut FungibleAsset, + zro_token: &mut Option, + msglib_send: |address, SendPacket| (u64, u64, RawPacket), + ): MessagingReceipt { + let (send_lib, _) = msglib_manager::get_effective_send_library(sender, dst_eid); + + // increment the outbound nonce and get the next nonce + let nonce = store::increment_outbound_nonce(sender, dst_eid, receiver); + + // check token metadatas + assert!(is_native_token(fungible_asset::asset_metadata(native_token)), err_EUNSUPPORTED_PAYMENT()); + if (option::is_some(zro_token)) { + assert!(universal_config::is_zro(option::borrow(zro_token)), err_EUNSUPPORTED_PAYMENT()); + }; + + let packet = new_send_packet( + nonce, + universal_config::eid(), + bytes32::from_address(sender), + dst_eid, + receiver, + message, + ); + let guid = send_packet::get_guid(&packet); + + let (native_fee, zro_fee, encoded_packet) = msglib_send(send_lib, packet); + emit_packet_sent_event(encoded_packet, options, send_lib); + + messaging_receipt::new_messaging_receipt( + guid, + nonce, + native_fee, + zro_fee, + ) + } + + /// This function provides the cost of sending a message in native and ZRO tokens (if pay_in_zro = true) without + /// sending the message + public(friend) fun quote( + sender: address, + dst_eid: u32, + receiver: Bytes32, + message: vector, + options: vector, + pay_in_zro: bool, + ): (u64, u64) { + quote_internal( + sender, + dst_eid, + receiver, + message, + // Message Library Router Quote Function + |send_lib, send_packet| msglib_router::quote( + send_lib, + send_packet, + options, + pay_in_zro, + ), + ) + } + + /// This is the internal portion of the quote function with the message library call provided as a lambda function + /// + /// @param sender: The address of the sender + /// @param dst_eid: The destination EID + /// @param receiver: The receiver's address + /// @param message: The message to be sent + /// @param msglib_quote: The lambda function that calls the send library's quote function + /// |send_lib, send_packet| (native_fee, zro_fee) + public(friend) inline fun quote_internal( + sender: address, + dst_eid: u32, + receiver: Bytes32, + message: vector, + msglib_quote: |address, SendPacket| (u64, u64) + ): (u64, u64) { + let nonce = store::outbound_nonce(sender, dst_eid, receiver) + 1; + let (send_lib, _) = msglib_manager::get_effective_send_library(sender, dst_eid); + let packet = new_send_packet( + nonce, + universal_config::eid(), + bytes32::from_address(sender), + dst_eid, + receiver, + message, + ); + msglib_quote(send_lib, packet) + } + + // =================================================== Receiving ================================================== + + /// Enable verification (committing) of messages from a given src_eid and sender to the receiver OApp + public(friend) fun register_receive_pathway(receiver: address, src_eid: u32, sender: Bytes32) { + store::register_receive_pathway(receiver, src_eid, sender); + emit(ReceivePathwayRegistered { receiver, src_eid, sender: from_bytes32(sender) }); + } + + /// Check if the receive pathway is registered + public(friend) fun receive_pathway_registered(receiver: address, src_eid: u32, sender: Bytes32): bool { + store::receive_pathway_registered(receiver, src_eid, sender) + } + + /// Sets the payload hash for the given nonce + public(friend) fun inbound(receiver: address, src_eid: u32, sender: Bytes32, nonce: u64, payload_hash: Bytes32) { + assert!(!bytes32::is_zero(&payload_hash), EEMPTY_PAYLOAD_HASH); + store::set_payload_hash(receiver, src_eid, sender, nonce, payload_hash); + } + + // Get the lazy inbound nonce for the given sender + public(friend) fun lazy_inbound_nonce(receiver: address, src_eid: u32, sender: Bytes32): u64 { + store::lazy_inbound_nonce(receiver, src_eid, sender) + } + + /// Returns the max index of the gapless sequence of verfied msg nonces starting at the lazy_inbound_nonce. This is + /// initially 0, and the first nonce is always 1 + public(friend) fun inbound_nonce(receiver: address, src_eid: u32, sender: Bytes32): u64 { + let i = store::lazy_inbound_nonce(receiver, src_eid, sender); + loop { + i = i + 1; + if (!store::has_payload_hash(receiver, src_eid, sender, i)) { + return i - 1 + } + } + } + + /// Skip the nonce. The nonce must be the next nonce in the sequence + public(friend) fun skip(receiver: address, src_eid: u32, sender: Bytes32, nonce_to_skip: u64) { + assert!(nonce_to_skip == inbound_nonce(receiver, src_eid, sender) + 1, EINVALID_NONCE); + store::set_lazy_inbound_nonce(receiver, src_eid, sender, nonce_to_skip); + emit(InboundNonceSkipped { src_eid, sender: from_bytes32(sender), receiver, nonce: nonce_to_skip }) + } + + /// Marks a packet as verified but disables execution until it is re-verified + /// A non-verified nonce can be nilified by passing EMPTY_PAYLOAD_HASH for payload_hash + public(friend) fun nilify(receiver: address, src_eid: u32, sender: Bytes32, nonce: u64, payload_hash: Bytes32) { + let has_payload_hash = store::has_payload_hash(receiver, src_eid, sender, nonce); + assert!(nonce > store::lazy_inbound_nonce(receiver, src_eid, sender) || has_payload_hash, EINVALID_NONCE); + + let stored_payload_hash = if (has_payload_hash) { + store::get_payload_hash(receiver, src_eid, sender, nonce) + } else { + to_bytes32(EMPTY_PAYLOAD_HASH) + }; + + assert!(payload_hash == stored_payload_hash, EPAYLOAD_HASH_DOES_NOT_MATCH); + + // Set the hash to 0xff*32 to indicate that the packet is nilified + store::set_payload_hash(receiver, src_eid, sender, nonce, bytes32::to_bytes32(NIL_PAYLOAD_HASH)); + emit( + PacketNilified { + src_eid, sender: from_bytes32(sender), receiver, nonce, payload_hash: from_bytes32(payload_hash), + } + ) + } + + /// Marks a nonce as unexecutable and unverifiable. The nonce can never be re-verified or executed. + /// Only packets less than or equal to the current lazy inbound nonce can be burnt. + public(friend) fun burn(receiver: address, src_eid: u32, sender: Bytes32, nonce: u64, payload_hash: Bytes32) { + assert!(nonce <= store::lazy_inbound_nonce(receiver, src_eid, sender), EINVALID_NONCE); + + // check that the hash provided matches the stored hash and clear + let inbound_payload_hash = store::remove_payload_hash(receiver, src_eid, sender, nonce); + assert!(inbound_payload_hash == payload_hash, EPAYLOAD_HASH_DOES_NOT_MATCH); + + emit( + PacketBurnt { + src_eid, sender: from_bytes32(sender), receiver, nonce, payload_hash: from_bytes32(payload_hash), + } + ) + } + + /// Clear the stored message and increment the lazy_inbound_nonce to the provided nonce + /// This is used in the receive pathway as the final step because it validates the hash as well + /// If a lot of messages are queued, the messages can be cleared with a smaller step size to prevent OOG + public(friend) fun clear_payload( + receiver: address, + src_eid: u32, + sender: Bytes32, + nonce: u64, + payload: vector, + ) { + let current_nonce = store::lazy_inbound_nonce(receiver, src_eid, sender); + + // Make sure that all hashes are present up to the clear-to point, clear them, and update the lazy nonce + if (nonce > current_nonce) { + current_nonce = current_nonce + 1; + while (current_nonce <= nonce) { + assert!(store::has_payload_hash(receiver, src_eid, sender, current_nonce), ENO_PAYLOAD_HASH); + current_nonce = current_nonce + 1 + }; + store::set_lazy_inbound_nonce(receiver, src_eid, sender, nonce); + }; + + // Check if the payload hash matches the provided payload + let actual_hash = bytes32::keccak256(payload); + assert!(store::has_payload_hash(receiver, src_eid, sender, nonce), ENO_PAYLOAD_HASH); + + // Clear and check the payload hash + let expected_hash = store::remove_payload_hash(receiver, src_eid, sender, nonce); + assert!(actual_hash == expected_hash, EPAYLOAD_HASH_DOES_NOT_MATCH); + + emit(PacketDelivered { src_eid, sender: from_bytes32(sender), nonce, receiver }); + } + + /// Checks if the payload hash exists for the given nonce + public(friend) fun has_payload_hash(receiver: address, src_eid: u32, sender: Bytes32, nonce: u64): bool { + store::has_payload_hash(receiver, src_eid, sender, nonce) + } + + /// Get the payload hash for a given nonce + public(friend) fun get_payload_hash(receiver: address, src_eid: u32, sender: Bytes32, nonce: u64): Bytes32 { + store::get_payload_hash(receiver, src_eid, sender, nonce) + } + + /// Check if a message is verifiable (committable) + public(friend) fun verifiable(receiver: address, src_eid: u32, sender: Bytes32, nonce: u64): bool { + if (!store::receive_pathway_registered(receiver, src_eid, sender)) { + return false + }; + let lazy_inbound_nonce = store::lazy_inbound_nonce(receiver, src_eid, sender); + nonce > lazy_inbound_nonce || has_payload_hash(receiver, src_eid, sender, nonce) + } + + /// Verify (commit) a message + public(friend) fun verify( + receive_lib: address, + packet_header: RawPacket, + payload_hash: Bytes32, + extra_data: vector, + ) { + let call_ref = &store::make_dynamic_call_ref(receive_lib, b"commit_verification"); + let (receiver, src_eid, sender, nonce) = msglib_router::commit_verification( + receive_lib, + call_ref, + packet_header, + payload_hash, + extra_data, + ); + + verify_internal( + receive_lib, + payload_hash, + receiver, + src_eid, + sender, + nonce, + ); + } + + /// This function is the internal portion of verifying a message. This receives the decoded packet header + /// information (that verify() gets from the message library) as an input + /// + /// @param receive_lib: The address of the receive library + /// @param packet_header: The packet header + /// @param payload_hash: The hash of the payload + /// @params receiver: The address of the receiver + /// @params src_eid: The source EID + /// @params sender: The sender's address + /// @params nonce: The nonce of the message + public(friend) inline fun verify_internal( + receive_lib: address, + payload_hash: Bytes32, + receiver: address, + src_eid: u32, + sender: Bytes32, + nonce: u64, + ) { + // This is the same assertion as initializable() in EVM + store::assert_receive_pathway_registered( + receiver, + src_eid, + sender, + ); + assert!( + msglib_manager::is_valid_receive_library_for_oapp(receiver, src_eid, receive_lib), + err_EINVALID_MSGLIB() + ); + assert!(verifiable(receiver, src_eid, sender, nonce), err_ENOT_VERIFIABLE()); + + // insert the message into the messaging channel + inbound(receiver, src_eid, sender, nonce, payload_hash); + emit_packet_verified_event(src_eid, from_bytes32(sender), nonce, receiver, from_bytes32(payload_hash)); + } + + + /// Send an alert if lz_receive reverts + public(friend) fun lz_receive_alert( + receiver: address, + executor: address, + src_eid: u32, + sender: Bytes32, + nonce: u64, + guid: Bytes32, + gas: u64, + value: u64, + message: vector, + extra_data: vector, + reason: String, + ) { + emit(LzReceiveAlert { + receiver, + executor, + src_eid, + sender: from_bytes32(sender), + nonce, + guid: from_bytes32(guid), + gas, + value, + message, + extra_data, + reason, + }); + } + + // ==================================================== Events ==================================================== + + #[event] + struct ReceivePathwayRegistered has store, drop { + receiver: address, + src_eid: u32, + sender: vector, + } + + #[event] + struct PacketBurnt has store, drop { + src_eid: u32, + sender: vector, + receiver: address, + nonce: u64, + payload_hash: vector, + } + + #[event] + struct PacketNilified has store, drop { + src_eid: u32, + sender: vector, + receiver: address, + nonce: u64, + payload_hash: vector, + } + + #[event] + struct InboundNonceSkipped has store, drop { + src_eid: u32, + sender: vector, + receiver: address, + nonce: u64, + } + + #[event] + struct PacketDelivered has drop, store { + src_eid: u32, + sender: vector, + nonce: u64, + receiver: address, + } + + #[event] + struct PacketSent has drop, store { + encoded_packet: vector, + options: vector, + send_library: address, + } + + #[event] + struct PacketVerified has drop, store { + src_eid: u32, + sender: vector, + nonce: u64, + receiver: address, + payload_hash: vector, + } + + + #[event] + struct LzReceiveAlert has drop, store { + receiver: address, + executor: address, + src_eid: u32, + sender: vector, + nonce: u64, + guid: vector, + gas: u64, + value: u64, + message: vector, + extra_data: vector, + reason: String, + } + + // These 2 emit_ functions are needed so that inline functions can still emit when called from the test module + + public(friend) fun emit_packet_sent_event(encoded_packet: RawPacket, options: vector, send_library: address) { + emit(PacketSent { encoded_packet: get_packet_bytes(encoded_packet), options, send_library }); + } + + public(friend) fun emit_packet_verified_event( + src_eid: u32, + sender: vector, + nonce: u64, + receiver: address, + payload_hash: vector, + ) { + emit(PacketVerified { src_eid, sender, nonce, receiver, payload_hash }); + } + + #[test_only] + public fun receive_pathway_registered_event( + receiver: address, + src_eid: u32, + sender: vector, + ): ReceivePathwayRegistered { + ReceivePathwayRegistered { receiver, src_eid, sender } + } + + #[test_only] + public fun packet_burnt_event( + src_eid: u32, + sender: vector, + receiver: address, + nonce: u64, + payload_hash: vector, + ): PacketBurnt { + PacketBurnt { src_eid, sender, receiver, nonce, payload_hash } + } + + #[test_only] + public fun packet_nilified_event( + src_eid: u32, + sender: vector, + receiver: address, + nonce: u64, + payload_hash: vector, + ): PacketNilified { + PacketNilified { src_eid, sender, receiver, nonce, payload_hash } + } + + #[test_only] + public fun inbound_nonce_skipped_event( + src_eid: u32, + sender: vector, + receiver: address, + nonce: u64, + ): InboundNonceSkipped { + InboundNonceSkipped { src_eid, sender, receiver, nonce } + } + + #[test_only] + public fun packet_delivered_event( + src_eid: u32, + sender: vector, + nonce: u64, + receiver: address, + ): PacketDelivered { + PacketDelivered { src_eid, sender, nonce, receiver } + } + + #[test_only] + public fun packet_sent_event(encoded_packet: RawPacket, options: vector, send_library: address): PacketSent { + PacketSent { encoded_packet: get_packet_bytes(encoded_packet), options, send_library } + } + + #[test_only] + public fun packet_verified_event( + src_eid: u32, + sender: vector, + nonce: u64, + receiver: address, + payload_hash: vector, + ): PacketVerified { + PacketVerified { src_eid, sender, nonce, receiver, payload_hash } + } + + #[test_only] + public fun lz_receive_alert_event( + receiver: address, + executor: address, + src_eid: u32, + sender: vector, + nonce: u64, + guid: vector, + gas: u64, + value: u64, + message: vector, + extra_data: vector, + reason: String, + ): LzReceiveAlert { + LzReceiveAlert { + receiver, + executor, + src_eid, + sender, + nonce, + guid, + gas, + value, + message, + extra_data, + reason, + } + } + + // ================================================== Error Codes ================================================= + + const EEMPTY_PAYLOAD_HASH: u64 = 1; + const EINVALID_MSGLIB: u64 = 2; + const EINVALID_NONCE: u64 = 3; + const ENOT_VERIFIABLE: u64 = 4; + const ENO_PAYLOAD_HASH: u64 = 5; + const EPAYLOAD_HASH_DOES_NOT_MATCH: u64 = 6; + const EUNSUPPORTED_PAYMENT: u64 = 7; + + // These wrapper error functions are needed to support testing inline functions in a different testing module + public(friend) fun err_EUNSUPPORTED_PAYMENT(): u64 { EUNSUPPORTED_PAYMENT } + + public(friend) fun err_EINVALID_MSGLIB(): u64 { EINVALID_MSGLIB } + + public(friend) fun err_ENOT_VERIFIABLE(): u64 { ENOT_VERIFIABLE } +} \ No newline at end of file diff --git a/packages/layerzero-v2/initia/contracts/endpoint_v2/sources/internal/messaging_composer.move b/packages/layerzero-v2/initia/contracts/endpoint_v2/sources/internal/messaging_composer.move new file mode 100644 index 00000000..826294fa --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/endpoint_v2/sources/internal/messaging_composer.move @@ -0,0 +1,140 @@ +/// The messaging composer is responsible for managing composed messages +module endpoint_v2::messaging_composer { + use std::event::emit; + use std::string::String; + + use endpoint_v2::store; + use endpoint_v2_common::bytes32::{Self, Bytes32, from_bytes32}; + + friend endpoint_v2::endpoint; + + #[test_only] + friend endpoint_v2::messaging_composer_tests; + + // ================================================ Core Functions ================================================ + + /// Called by the OApp when receiving a message to trigger the sending of a composed message + /// This preps the message for delivery by lz_compose + public(friend) fun send_compose(oapp: address, to: address, index: u16, guid: Bytes32, message: vector) { + assert!(!store::has_compose_message_hash(oapp, to, guid, index), ECOMPOSE_ALREADY_SENT); + let message_hash = bytes32::keccak256(message); + store::set_compose_message_hash(oapp, to, guid, index, message_hash); + emit(ComposeSent { from: oapp, to, guid: from_bytes32(guid), index, message }); + } + + /// This clears the composed message from the store + /// This should be triggered by lz_compose on the OApp, and it confirms that the message was delivered + public(friend) fun clear_compose(from: address, to: address, guid: Bytes32, index: u16, message: vector) { + // Make sure the message hash matches the expected hash + assert!(store::has_compose_message_hash(from, to, guid, index), ECOMPOSE_NOT_FOUND); + let expected_hash = store::get_compose_message_hash(from, to, guid, index); + let actual_hash = bytes32::keccak256(message); + assert!(expected_hash == actual_hash, ECOMPOSE_ALREADY_CLEARED); + + // Set the message 0xff(*32) to prevent sending the message again + store::set_compose_message_hash(from, to, guid, index, bytes32::ff_bytes32()); + emit(ComposeDelivered { from, to, guid: from_bytes32(guid), index }); + } + + /// Get the hash of the composed message with the given parameters + /// NOT SENT: 0x00(*32) + /// SENT: hash value + /// RECEIVED / CLEARED: 0xff(*32) + public(friend) fun get_compose_message_hash(from: address, to: address, guid: Bytes32, index: u16): Bytes32 { + if (store::has_compose_message_hash(from, to, guid, index)) { + store::get_compose_message_hash(from, to, guid, index) + } else { + bytes32::zero_bytes32() + } + } + + /// Emit an event that indicates that the off-chain executor failed to deliver the compose message + public(friend) fun lz_compose_alert( + executor: address, + from: address, + to: address, + guid: Bytes32, + index: u16, + gas: u64, + value: u64, + message: vector, + extra_data: vector, + reason: String, + ) { + emit(LzComposeAlert { + from, to, executor, guid: from_bytes32(guid), index, gas, value, message, extra_data, reason, + }); + } + + // ==================================================== Events ==================================================== + + #[event] + struct ComposeSent has copy, drop, store { + from: address, + to: address, + guid: vector, + index: u16, + message: vector, + } + + #[event] + struct ComposeDelivered has copy, drop, store { + from: address, + to: address, + guid: vector, + index: u16, + } + + #[event] + struct LzComposeAlert has copy, drop, store { + from: address, + to: address, + executor: address, + guid: vector, + index: u16, + gas: u64, + value: u64, + message: vector, + extra_data: vector, + reason: String, + } + + #[test_only] + public fun compose_sent_event( + from: address, + to: address, + guid: vector, + index: u16, + message: vector, + ): ComposeSent { + ComposeSent { from, to, guid, index, message } + } + + #[test_only] + public fun compose_delivered_event(from: address, to: address, guid: vector, index: u16): ComposeDelivered { + ComposeDelivered { from, to, guid, index } + } + + #[test_only] + public fun lz_compose_alert_event( + from: address, + to: address, + executor: address, + guid: vector, + index: u16, + gas: u64, + value: u64, + message: vector, + extra_data: vector, + reason: String, + ): LzComposeAlert { + LzComposeAlert { from, to, executor, guid, index, gas, value, message, extra_data, reason } + } + + // ================================================== Error Codes ================================================= + + const ECOMPOSE_ALREADY_CLEARED: u64 = 1; + const ECOMPOSE_ALREADY_SENT: u64 = 2; + const ECOMPOSE_NOT_FOUND: u64 = 3; +} + diff --git a/packages/layerzero-v2/initia/contracts/endpoint_v2/sources/internal/messaging_receipt.move b/packages/layerzero-v2/initia/contracts/endpoint_v2/sources/internal/messaging_receipt.move new file mode 100644 index 00000000..a99eb82d --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/endpoint_v2/sources/internal/messaging_receipt.move @@ -0,0 +1,74 @@ +/// Messaging Receipt is returned upon endpoint::send and provides proof of the message being sent and fees paid +module endpoint_v2::messaging_receipt { + use endpoint_v2_common::bytes32::Bytes32; + + friend endpoint_v2::channels; + + #[test_only] + friend endpoint_v2::messaging_receipt_tests; + + // For indirect dependency: `channels_tests` calls friend functions via inline functions in `endpoint_v2::channels` + #[test_only] + friend endpoint_v2::channels_tests; + + /// Messaging receipt is returned from endpoint::send and provides proof of the message being sent and fees paid + struct MessagingReceipt has store, drop { + guid: Bytes32, + nonce: u64, + native_fee: u64, + zro_fee: u64, + } + + /// Constructs a new messaging receipt + /// This is a friend-only function so that the Messaging Receipt cannot be forged by a 3rd party + public(friend) fun new_messaging_receipt( + guid: Bytes32, + nonce: u64, + native_fee: u64, + zro_fee: u64, + ): MessagingReceipt { + MessagingReceipt { guid, nonce, native_fee, zro_fee } + } + + #[test_only] + public fun new_messaging_receipt_for_test( + guid: Bytes32, + nonce: u64, + native_fee: u64, + zro_fee: u64, + ): MessagingReceipt { + MessagingReceipt { guid, nonce, native_fee, zro_fee } + } + + /// Get the guid of a MessagingReceipt in the format of a bytes array + public fun get_guid(self: &MessagingReceipt): Bytes32 { + self.guid + } + + /// Gets the nonce of a MessagingReceipt + public fun get_nonce(self: &MessagingReceipt): u64 { + self.nonce + } + + /// Gets the native fee of a MessagingReceipt + public fun get_native_fee(self: &MessagingReceipt): u64 { + self.native_fee + } + + /// Gets the zro fee of a MessagingReceipt + public fun get_zro_fee(self: &MessagingReceipt): u64 { + self.zro_fee + } + + /// Unpacks the fields of a MessagingReceipt + /// @return (guid, nonce, native_fee, zro_fee) + public fun unpack_messaging_receipt(receipt: MessagingReceipt): (Bytes32, u64, u64, u64) { + let MessagingReceipt { + guid, + nonce, + native_fee, + zro_fee, + } = receipt; + (guid, nonce, native_fee, zro_fee) + } +} \ No newline at end of file diff --git a/packages/layerzero-v2/initia/contracts/endpoint_v2/sources/internal/msglib_manager.move b/packages/layerzero-v2/initia/contracts/endpoint_v2/sources/internal/msglib_manager.move new file mode 100644 index 00000000..4ec7a2d5 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/endpoint_v2/sources/internal/msglib_manager.move @@ -0,0 +1,469 @@ +/// The message library manager is responsible for managing the message libraries and providing default message library +/// selections for the endpoint. +/// The registration of a message library happens in several steps: +/// 1. The message library is deployed and made available through the message library router. +/// 2. The message library address is registered with the message library manager: this irrevocably enables the use of +/// the message library through this endpoint (but not on any pathways). +/// 3. A default message library is registered for each individual pathway: this irreversibly enables the use of the +/// message library for the given pathway. +/// +/// Once a message library's default is set, the OApp can override the default message library for any pathway through +/// the separate OApp configuration interface. +module endpoint_v2::msglib_manager { + use std::event::emit; + + use endpoint_v2::store; + use endpoint_v2::timeout; + use endpoint_v2::timeout::{new_timeout_from_expiry, Timeout}; + use router_node_0::router_node; + + friend endpoint_v2::admin; + friend endpoint_v2::endpoint; + friend endpoint_v2::channels; + + #[test_only] + friend endpoint_v2::msglib_manager_tests; + #[test_only] + friend endpoint_v2::test_helpers; + #[test_only] + friend endpoint_v2::channels_tests; + + // ==================================================== Helpers =================================================== + + inline fun assert_library_registered(lib: address) { + assert!(store::is_registered_msglib(lib), EUNREGISTERED_MSGLIB); + } + + inline fun is_in_grace_period(src_eid: u32): bool { + store::has_default_receive_library_timeout(src_eid) && + timeout::is_active(&store::get_default_receive_library_timeout(src_eid)) + } + + inline fun get_default_receive_library_timeout_prior_lib(src_eid: u32): address { + timeout::get_library(&store::get_default_receive_library_timeout(src_eid)) + } + + // ================================================ Core Functions ================================================ + + /// Register a new message library + public(friend) fun register_library(lib: address) { + // Make sure it is available to be registered + assert_connected_to_router(lib); + // Make sure it is not already registered + assert!(!store::is_registered_msglib(lib), EALREADY_REGISTERED); + // Add the message library and emit + store::add_msglib(lib); + emit(LibraryRegistered { new_lib: lib }); + } + + /// Asserts that a library is connected to the router and not a placeholder. + inline fun assert_connected_to_router(lib: address) { + // The version call will fail if a message library is not in the router or if it is a placeholder. + router_node::version(lib); + } + + /// Gets a list of all message libraries that are registered + public(friend) fun get_registered_libraries(start_index: u64, max_entries: u64): vector
{ + store::registered_msglibs(start_index, max_entries) + } + + /// Checks if a particular library address is registered + public(friend) fun is_registered_library(lib: address): bool { + store::is_registered_msglib(lib) + } + + public(friend) fun set_config( + oapp: address, + lib: address, + eid: u32, + config_type: u32, + config: vector, + ) { + assert_library_registered(lib); + let to_msglib_call_ref = &store::make_dynamic_call_ref(lib, b"set_config"); + router_node::set_config(lib, to_msglib_call_ref, oapp, eid, config_type, config); + } + + public(friend) fun get_config( + oapp: address, + lib: address, + eid: u32, + config_type: u32, + ): vector { + assert_library_registered(lib); + router_node::get_config(lib, oapp, eid, config_type) + } + + // ============================================ Default Send Libraries ============================================ + + fun assert_msglib_supports_send_eid(lib: address, dst_eid: u32) { + assert!(router_node::is_supported_send_eid(lib, dst_eid), EUNSUPPORTED_DST_EID); + } + + /// Gets the default send library + public(friend) fun get_default_send_library(dst_eid: u32): address { + store::get_default_send_library(dst_eid) + } + + /// Set the default Endpoint default send library for a dest_eid. + /// If there is no default send library set, the dest_eid is deemed to be unsupported. + public(friend) fun set_default_send_library(dst_eid: u32, new_lib: address) { + assert_library_registered(new_lib); + assert_msglib_supports_send_eid(new_lib, dst_eid); + + if (store::has_default_send_library(dst_eid)) { + let old_lib = store::get_default_send_library(dst_eid); + assert!(old_lib != new_lib, EATTEMPTED_TO_SET_CURRENT_LIBRARY); + }; + + store::set_default_send_library(dst_eid, new_lib); + emit(DefaultSendLibrarySet { eid: dst_eid, new_lib }); + } + + // ============================================== Oapp Send Libraries ============================================= + + public(friend) fun set_send_library( + sender: address, + dst_eid: u32, + msglib: address, + ) { + if (msglib == @0x0) { + assert!(store::has_send_library(sender, dst_eid), EOAPP_SEND_LIB_NOT_SET); + store::unset_send_library(sender, dst_eid); + } else { + assert_library_registered(msglib); + assert_msglib_supports_send_eid(msglib, dst_eid); + + if (store::has_send_library(sender, dst_eid)) { + let old_lib = store::get_send_library(sender, dst_eid); + assert!(old_lib != msglib, EATTEMPTED_TO_SET_CURRENT_LIBRARY); + }; + store::set_send_library(sender, dst_eid, msglib); + }; + emit(SendLibrarySet { sender, eid: dst_eid, new_lib: msglib }); + } + + /// Gets the effective send library for the given destination EID, returns both the library and a flag indicating if + /// this is a fallback to the default (meaning the library is not configured for the oapp) + public(friend) fun get_effective_send_library(sender: address, dst_eid: u32): (address, bool) { + if (store::has_send_library(sender, dst_eid)) { + (store::get_send_library(sender, dst_eid), false) + } else { + (store::get_default_send_library(dst_eid), true) + } + } + + + // =========================================== Default Receive Libraries ========================================== + + fun assert_msglib_supports_receive_eid(lib: address, src_eid: u32) { + assert!(router_node::is_supported_receive_eid(lib, src_eid), EUNSUPPORTED_SRC_EID); + } + + /// Set the default receive library. + /// If the grace_period is non-zero, also set the grace message library and expiry + public(friend) fun set_default_receive_library(src_eid: u32, new_lib: address, grace_period: u64) { + assert_library_registered(new_lib); + assert_msglib_supports_receive_eid(new_lib, src_eid); + + if (store::has_default_receive_library(src_eid)) { + let old_lib = store::get_default_receive_library(src_eid); + assert!(old_lib != new_lib, EATTEMPTED_TO_SET_CURRENT_LIBRARY); + }; + + let old_lib = if (store::has_default_receive_library(src_eid)) { + store::get_default_receive_library(src_eid) + } else @0x0; + + // Set the grace period if it is greater than 0 + if (grace_period > 0) { + assert!(old_lib != @0x0, ENO_PRIOR_LIBRARY_FOR_FALLBACK); + let timeout = timeout::new_timeout_from_grace_period(grace_period, old_lib); + store::set_default_receive_library_timeout(src_eid, timeout); + + let expiry = timeout::get_expiry(&timeout); + emit(DefaultReceiveLibraryTimeoutSet { eid: src_eid, old_lib, expiry }); + } else { + if (store::has_default_receive_library_timeout(src_eid)) { + store::unset_default_receive_library_timeout(src_eid); + }; + emit(DefaultReceiveLibraryTimeoutSet { eid: src_eid, old_lib, expiry: 0 }); + }; + store::set_default_receive_library(src_eid, new_lib); + emit(DefaultReceiveLibrarySet { eid: src_eid, new_lib }); + } + + public(friend) fun set_default_receive_library_timeout( + src_eid: u32, + lib: address, + expiry: u64, + ) { + if (expiry == 0) { + store::unset_default_receive_library_timeout(src_eid); + emit(DefaultReceiveLibraryTimeoutSet { eid: src_eid, expiry: 0, old_lib: lib }); + } else { + assert_library_registered(lib); + assert_msglib_supports_receive_eid(lib, src_eid); + let timeout = timeout::new_timeout_from_expiry(expiry, lib); + assert!(timeout::is_active(&timeout), EEXPIRY_IS_IN_PAST); + store::set_default_receive_library_timeout(src_eid, timeout); + emit(DefaultReceiveLibraryTimeoutSet { eid: src_eid, expiry, old_lib: lib }); + }; + } + + /// Asserts that a given receive library is the default receive library for a given source EID. + /// This will also check the grace period (prior library) if active. + public(friend) fun matches_default_receive_library(src_eid: u32, actual_receive_lib: address): bool { + if (actual_receive_lib == store::get_default_receive_library(src_eid)) { return true }; + + // If no match, check the grace period + if (!is_in_grace_period(src_eid)) { return false }; + + get_default_receive_library_timeout_prior_lib(src_eid) == actual_receive_lib + } + + public(friend) fun get_default_receive_library(src_eid: u32): address { + store::get_default_receive_library(src_eid) + } + + /// Get the default receive library timeout or return an empty Timeout if one is not set + public(friend) fun get_default_receive_library_timeout(src_eid: u32): Timeout { + if (store::has_default_receive_library_timeout(src_eid)) { + store::get_default_receive_library_timeout(src_eid) + } else { + new_timeout_from_expiry(0, @0x0) + } + } + + // ================================================= Receive Oapp ================================================= + + + public(friend) fun set_receive_library( + receiver: address, + src_eid: u32, + msglib: address, + grace_period: u64, + ) { + // If MsgLib is @0x0, unset the library + if (msglib == @0x0) { + // Setting a grace period is not supported when unsetting the library to match EVM spec + // This can still be achieved by explicitly setting the OApp to the default library with a grace period + // And then after the grace period, unsetting the library by setting it to @0x0 + assert!(grace_period == 0, ECANNOT_SET_GRACE_PERIOD_ON_RECEIVE_LIBRARY_UNSET); + + assert!(store::has_receive_library(receiver, src_eid), ERECEIVE_LIB_NOT_SET); + let old_lib = store::get_receive_library(receiver, src_eid); + store::unset_receive_library(receiver, src_eid); + emit(ReceiveLibrarySet { receiver, eid: src_eid, new_lib: @0x0 }); + if (store::has_receive_library_timeout(receiver, src_eid)) { + store::unset_receive_library_timeout(receiver, src_eid); + }; + emit(ReceiveLibraryTimeoutSet { receiver, eid: src_eid, old_lib, timeout: 0 }); + return + }; + + // Check if the library is registered and supports the receive EID + assert_library_registered(msglib); + assert_msglib_supports_receive_eid(msglib, src_eid); + + // Make sure we are not setting the same library + if (store::has_receive_library(receiver, src_eid)) { + let old_lib = store::get_receive_library(receiver, src_eid); + assert!(old_lib != msglib, EATTEMPTED_TO_SET_CURRENT_LIBRARY); + }; + + let (old_lib, is_default) = get_effective_receive_library(receiver, src_eid); + + store::set_receive_library(receiver, src_eid, msglib); + emit(ReceiveLibrarySet { receiver, eid: src_eid, new_lib: msglib }); + + if (grace_period == 0) { + // If there is a timeout and grace_period is 0, remove the current timeout + if (store::has_receive_library_timeout(receiver, src_eid)) { + store::unset_receive_library_timeout(receiver, src_eid); + }; + let non_default_old_lib = if (is_default) { @0x0 } else { old_lib }; + emit(ReceiveLibraryTimeoutSet { receiver, eid: src_eid, old_lib: non_default_old_lib, timeout: 0 }); + } else { + // Setting a grace period is not supported when setting the library from the default library + // This can still be achieved by explicitly setting the OApp to the default library, then setting it to the + // desired library with a default + assert!(!is_default, ETIMEOUT_SET_FOR_DEFAULT_RECEIVE_LIBRARY); + + let grace_period = timeout::new_timeout_from_grace_period(grace_period, old_lib); + store::set_receive_library_timeout(receiver, src_eid, grace_period); + let expiry = timeout::get_expiry(&grace_period); + emit(ReceiveLibraryTimeoutSet { receiver, eid: src_eid, old_lib, timeout: expiry }); + } + } + + + public(friend) fun set_receive_library_timeout( + receiver: address, + src_eid: u32, + lib: address, + expiry: u64, + ) { + if (expiry == 0) { + // Abort if there is no timeout to delete + assert!(store::has_receive_library_timeout(receiver, src_eid), ENO_TIMEOUT_TO_DELETE); + let timeout = store::get_receive_library_timeout(receiver, src_eid); + assert!(timeout::is_active(&timeout), ENO_TIMEOUT_TO_DELETE); + store::unset_receive_library_timeout(receiver, src_eid); + emit(ReceiveLibraryTimeoutSet { receiver, eid: src_eid, old_lib: lib, timeout: 0 }); + } else { + assert!(store::has_receive_library(receiver, src_eid), ERECEIVE_LIB_NOT_SET); + assert_msglib_supports_receive_eid(lib, src_eid); + assert_library_registered(lib); + let timeout = timeout::new_timeout_from_expiry(expiry, lib); + assert!(timeout::is_active(&timeout), EEXPIRY_IS_IN_PAST); + store::set_receive_library_timeout(receiver, src_eid, timeout); + emit(ReceiveLibraryTimeoutSet { receiver, eid: src_eid, old_lib: lib, timeout: expiry }); + }; + } + + /// Get the receive library timeout or return an empty timeout if it is not set + public(friend) fun get_receive_library_timeout(receiver: address, src_eid: u32): Timeout { + if (store::has_receive_library_timeout(receiver, src_eid)) { + store::get_receive_library_timeout(receiver, src_eid) + } else { + new_timeout_from_expiry(0, @0x0) + } + } + + /// Gets the effective receive library for the given source EID, returns both the library and a flag indicating if + /// this is a fallback to the default (meaning the library is not configured) + public(friend) fun get_effective_receive_library(receiver: address, src_eid: u32): (address, bool) { + if (store::has_receive_library(receiver, src_eid)) { + (store::get_receive_library(receiver, src_eid), false) + } else { + (store::get_default_receive_library(src_eid), true) + } + } + + /// Check if the provided message library is valid for the given OApp and source EID. + public(friend) fun is_valid_receive_library_for_oapp(receiver: address, src_eid: u32, msglib: address): bool { + if (!is_registered_library(msglib)) { return false }; + + // 1. Check if it is the default library, if it is not already set + if (!store::has_receive_library(receiver, src_eid)) { + return matches_default_receive_library(src_eid, msglib) + }; + + // 2. Check if it is the configured library + if (store::get_receive_library(receiver, src_eid) == msglib) { return true }; + + // 3. If it is in the grace period, check the prior library + if (store::has_receive_library_timeout(receiver, src_eid)) { + let timeout = store::get_receive_library_timeout(receiver, src_eid); + let is_active = timeout::is_active(&timeout); + if (is_active && timeout::get_library(&timeout) == msglib) { return true }; + }; + + // 4. Otherwise, it is invalid + false + } + + // ==================================================== Events ==================================================== + + #[event] + struct LibraryRegistered has drop, copy, store { new_lib: address } + + #[event] + struct DefaultSendLibrarySet has drop, copy, store { eid: u32, new_lib: address } + + #[event] + struct DefaultReceiveLibrarySet has drop, copy, store { + eid: u32, + new_lib: address, + } + + #[event] + struct DefaultReceiveLibraryTimeoutSet has drop, copy, store { + eid: u32, + old_lib: address, + expiry: u64, + } + + #[event] + struct SendLibrarySet has drop, copy, store { + sender: address, + eid: u32, + new_lib: address, + } + + #[event] + struct ReceiveLibrarySet has drop, copy, store { + receiver: address, + eid: u32, + new_lib: address, + } + + #[event] + struct ReceiveLibraryTimeoutSet has drop, copy, store { + receiver: address, + eid: u32, + old_lib: address, + timeout: u64, + } + + #[test_only] + public fun library_registered_event(lib: address): LibraryRegistered { LibraryRegistered { new_lib: lib } } + + #[test_only] + public fun default_send_library_set_event( + eid: u32, + new_lib: address, + ): DefaultSendLibrarySet { DefaultSendLibrarySet { eid, new_lib } } + + #[test_only] + public fun default_receive_library_set_event( + eid: u32, + new_lib: address, + ): DefaultReceiveLibrarySet { DefaultReceiveLibrarySet { eid, new_lib } } + + #[test_only] + public fun default_receive_library_timeout_set_event( + eid: u32, + old_lib: address, + expiry: u64, + ): DefaultReceiveLibraryTimeoutSet { DefaultReceiveLibraryTimeoutSet { eid, old_lib, expiry } } + + #[test_only] + public fun send_library_set_event( + sender: address, + eid: u32, + new_lib: address, + ): SendLibrarySet { SendLibrarySet { sender, eid, new_lib } } + + #[test_only] + public fun receive_library_set_event( + receiver: address, + eid: u32, + new_lib: address, + ): ReceiveLibrarySet { ReceiveLibrarySet { receiver, eid, new_lib } } + + #[test_only] + public fun receive_library_timeout_set_event( + receiver: address, + eid: u32, + old_lib: address, + timeout: u64, + ): ReceiveLibraryTimeoutSet { ReceiveLibraryTimeoutSet { receiver, eid, old_lib, timeout } } + + // ================================================== Error Codes ================================================= + + const EALREADY_REGISTERED: u64 = 1; + const EATTEMPTED_TO_SET_CURRENT_LIBRARY: u64 = 2; + const ECANNOT_SET_GRACE_PERIOD_ON_RECEIVE_LIBRARY_UNSET: u64 = 3; + const EEXPIRY_IS_IN_PAST: u64 = 4; + const ENO_PRIOR_LIBRARY_FOR_FALLBACK: u64 = 5; + const ENO_TIMEOUT_TO_DELETE: u64 = 6; + const EOAPP_SEND_LIB_NOT_SET: u64 = 7; + const ERECEIVE_LIB_NOT_SET: u64 = 8; + const ETIMEOUT_SET_FOR_DEFAULT_RECEIVE_LIBRARY: u64 = 9; + const EUNREGISTERED_MSGLIB: u64 = 10; + const EUNSUPPORTED_DST_EID: u64 = 11; + const EUNSUPPORTED_SRC_EID: u64 = 12; +} + diff --git a/packages/layerzero-v2/initia/contracts/endpoint_v2/sources/internal/registration.move b/packages/layerzero-v2/initia/contracts/endpoint_v2/sources/internal/registration.move new file mode 100644 index 00000000..726ecf36 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/endpoint_v2/sources/internal/registration.move @@ -0,0 +1,85 @@ +module endpoint_v2::registration { + use std::event::emit; + use std::string::{Self, String}; + + use endpoint_v2::store; + + friend endpoint_v2::endpoint; + + #[test_only] + friend endpoint_v2::registration_tests; + #[test_only] + friend endpoint_v2::channels_tests; + + /// Initialize the OApp with registering the lz_receive function + /// register_oapp() must also be called before the OApp can receive messages + public(friend) fun register_oapp(oapp: address, lz_receive_module: String) { + assert!(string::length(&lz_receive_module) <= 100, EEXCESSIVE_STRING_LENGTH); + assert!(!store::is_registered_oapp(oapp), EALREADY_REGISTERED); + store::register_oapp(oapp, lz_receive_module); + emit(OAppRegistered { + oapp, + lz_receive_module, + }); + } + + /// Registers the Composer with the lz_compose function + public(friend) fun register_composer(composer: address, lz_compose_module: String) { + assert!(string::length(&lz_compose_module) <= 100, EEXCESSIVE_STRING_LENGTH); + assert!(!store::is_registered_composer(composer), EALREADY_REGISTERED); + store::register_composer(composer, lz_compose_module); + emit(ComposerRegistered { + composer, + lz_compose_module, + }); + } + + /// Checks if the OApp is registered + public(friend) fun is_registered_oapp(oapp: address): bool { + store::is_registered_oapp(oapp) + } + + /// Checks if the Composer is registered + public(friend) fun is_registered_composer(composer: address): bool { + store::is_registered_composer(composer) + } + + /// Returns the lz_receive_module of the OApp + public(friend) fun lz_receive_module(oapp: address): String { + store::lz_receive_module(oapp) + } + + /// Returns the lz_compose_module of the Composer + public(friend) fun lz_compose_module(composer: address): String { + store::lz_compose_module(composer) + } + + // ==================================================== Events ==================================================== + + #[event] + struct OAppRegistered has drop, store { + oapp: address, + lz_receive_module: String, + } + + #[event] + struct ComposerRegistered has drop, store { + composer: address, + lz_compose_module: String, + } + + #[test_only] + public fun oapp_registered_event(oapp: address, lz_receive_module: String): OAppRegistered { + OAppRegistered { oapp, lz_receive_module } + } + + #[test_only] + public fun composer_registered_event(composer: address, lz_compose_module: String): ComposerRegistered { + ComposerRegistered { composer, lz_compose_module } + } + + // ================================================== Error Codes ================================================= + + const EALREADY_REGISTERED: u64 = 1; + const EEXCESSIVE_STRING_LENGTH: u64 = 2; +} diff --git a/packages/layerzero-v2/initia/contracts/endpoint_v2/sources/internal/store.move b/packages/layerzero-v2/initia/contracts/endpoint_v2/sources/internal/store.move new file mode 100644 index 00000000..2a01d346 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/endpoint_v2/sources/internal/store.move @@ -0,0 +1,493 @@ +module endpoint_v2::store { + use std::math64::min; + use std::string::{Self, String}; + use std::table::{Self, Table}; + use std::vector; + + use endpoint_v2::timeout::Timeout; + use endpoint_v2_common::bytes32::Bytes32; + use endpoint_v2_common::contract_identity::{Self, ContractSigner, DynamicCallRef}; + + friend endpoint_v2::channels; + friend endpoint_v2::messaging_composer; + friend endpoint_v2::msglib_manager; + friend endpoint_v2::registration; + + #[test_only] + friend endpoint_v2::admin; + #[test_only] + friend endpoint_v2::endpoint_tests; + #[test_only] + friend endpoint_v2::channels_tests; + #[test_only] + friend endpoint_v2::msglib_manager_tests; + #[test_only] + friend endpoint_v2::messaging_composer_tests; + #[test_only] + friend endpoint_v2::registration_tests; + + struct EndpointStore has key { + contract_signer: ContractSigner, + oapps: Table, + composers: Table, + msglibs: Table, + msglib_count: u64, + msglibs_registered: Table, + msglibs_default_send_libs: Table, + msglibs_default_receive_libs: Table, + msglibs_default_receive_lib_timeout_configs: Table, + } + + struct OAppStore has store { + lz_receive_module: String, + channels: Table, + msglibs_send_libs: Table, + msglibs_receive_libs: Table, + msglibs_receive_lib_timeout_configs: Table, + } + + struct ComposerStore has store { + lz_compose_module: String, + compose_message_hashes: Table, + } + + struct Channel has store { + outbound_nonce: u64, + inbound_pathway_registered: bool, + inbound_lazy_nonce: u64, + inbound_hashes: Table, + } + + struct ChannelKey has store, drop, copy { remote_eid: u32, remote_address: Bytes32 } + + struct ComposeKey has store, drop, copy { from: address, guid: Bytes32, index: u16 } + + fun init_module(account: &signer) { + move_to(account, EndpointStore { + contract_signer: contract_identity::create_contract_signer(account), + oapps: table::new(), + composers: table::new(), + msglibs: table::new(), + msglib_count: 0, + msglibs_registered: table::new(), + msglibs_default_send_libs: table::new(), + msglibs_default_receive_libs: table::new(), + msglibs_default_receive_lib_timeout_configs: table::new(), + }) + } + + #[test_only] + public fun init_module_for_test() { + let account = &std::account::create_signer_for_test(@endpoint_v2); + init_module(account); + } + + // ==================================================== General =================================================== + + public(friend) fun make_dynamic_call_ref( + target_contract: address, + authorization: vector, + ): DynamicCallRef acquires EndpointStore { + contract_identity::make_dynamic_call_ref(&store().contract_signer, target_contract, authorization) + } + + // OApp - Helpers + inline fun store(): &EndpointStore { borrow_global(@endpoint_v2) } + + inline fun store_mut(): &mut EndpointStore { borrow_global_mut(@endpoint_v2) } + + inline fun oapps(): &Table { &store().oapps } + + inline fun oapps_mut(): &mut Table { &mut store_mut().oapps } + + inline fun composers(): &Table { &store().composers } + + inline fun composers_mut(): &mut Table { &mut store_mut().composers } + + inline fun oapp_store(oapp: address): &OAppStore { + assert!(table::contains(oapps(), oapp), EUNREGISTERED_OAPP); + table::borrow(oapps(), oapp) + } + + inline fun oapp_store_mut(oapp: address): &mut OAppStore { + assert!(table::contains(oapps(), oapp), EUNREGISTERED_OAPP); + table::borrow_mut(oapps_mut(), oapp) + } + + inline fun composer_store(composer: address): &ComposerStore { + assert!(table::contains(composers(), composer), EUNREGISTERED_COMPOSER); + table::borrow(composers(), composer) + } + + inline fun composer_store_mut(composer: address): &mut ComposerStore { + assert!(table::contains(composers(), composer), EUNREGISTERED_COMPOSER); + table::borrow_mut(composers_mut(), composer) + } + + inline fun has_channel(oapp: address, remote_eid: u32, remote_address: Bytes32): bool { + table::contains(&oapp_store(oapp).channels, ChannelKey { remote_eid, remote_address }) + } + + inline fun create_channel(oapp: address, remote_eid: u32, remote_address: Bytes32) acquires EndpointStore { + table::add(&mut oapp_store_mut(oapp).channels, ChannelKey { remote_eid, remote_address }, Channel { + outbound_nonce: 0, + inbound_pathway_registered: false, + inbound_lazy_nonce: 0, + inbound_hashes: table::new(), + }); + } + + /// Get a reference to an OApp channel. The initialization of a channel should be implicit and transparent to the + /// user. + /// Therefore caller should generally check that the channel exists before calling this function; if it doesn't + /// exist, it should return the value that is consistent with the initial state of a channel + inline fun channel(oapp: address, remote_eid: u32, remote_address: Bytes32): &Channel { + let channel_key = ChannelKey { remote_eid, remote_address }; + table::borrow(&oapp_store(oapp).channels, channel_key) + } + + /// Get a mutable reference to an OApp channel. If the channel doesn't exist, create it + inline fun channel_mut(oapp: address, remote_eid: u32, remote_address: Bytes32): &mut Channel { + let channel_key = ChannelKey { remote_eid, remote_address }; + if (!table::contains(&oapp_store(oapp).channels, channel_key)) { + create_channel(oapp, remote_eid, remote_address); + }; + table::borrow_mut(&mut oapp_store_mut(oapp).channels, channel_key) + } + + // ================================================= OApp General ================================================= + + public(friend) fun register_oapp(oapp: address, lz_receive_module: String) acquires EndpointStore { + assert!(!string::is_empty(&lz_receive_module), EEMPTY_MODULE_NAME); + table::add(oapps_mut(), oapp, OAppStore { + channels: table::new(), + lz_receive_module, + msglibs_send_libs: table::new(), + msglibs_receive_libs: table::new(), + msglibs_receive_lib_timeout_configs: table::new(), + }); + } + + public(friend) fun is_registered_oapp(oapp: address): bool acquires EndpointStore { + table::contains(oapps(), oapp) + } + + public(friend) fun lz_receive_module(receiver: address): String acquires EndpointStore { + oapp_store(receiver).lz_receive_module + } + + // ================================================= OApp Outbound ================================================ + + public(friend) fun outbound_nonce(sender: address, dst_eid: u32, receiver: Bytes32): u64 acquires EndpointStore { + if (has_channel(sender, dst_eid, receiver)) { + channel(sender, dst_eid, receiver).outbound_nonce + } else { + 0 + } + } + + public(friend) fun increment_outbound_nonce( + sender: address, + dst_eid: u32, + receiver: Bytes32, + ): u64 acquires EndpointStore { + let outbound_nonce = &mut channel_mut(sender, dst_eid, receiver).outbound_nonce; + *outbound_nonce = *outbound_nonce + 1; + *outbound_nonce + } + + // ================================================= OApp Inbound ================================================= + + public(friend) fun receive_pathway_registered( + receiver: address, + src_eid: u32, + sender: Bytes32, + ): bool acquires EndpointStore { + if (is_registered_oapp(receiver) && has_channel(receiver, src_eid, sender)) { + channel(receiver, src_eid, sender).inbound_pathway_registered + } else { + false + } + } + + public(friend) fun register_receive_pathway( + receiver: address, + src_eid: u32, + sender: Bytes32, + ) acquires EndpointStore { + if (!has_channel(receiver, src_eid, sender)) { create_channel(receiver, src_eid, sender) }; + let channel = channel_mut(receiver, src_eid, sender); + channel.inbound_pathway_registered = true; + } + + public(friend) fun assert_receive_pathway_registered( + receiver: address, + src_eid: u32, + sender: Bytes32, + ) acquires EndpointStore { + assert!(receive_pathway_registered(receiver, src_eid, sender), EUNREGISTERED_PATHWAY); + } + + public(friend) fun lazy_inbound_nonce( + receiver: address, + src_eid: u32, + sender: Bytes32, + ): u64 acquires EndpointStore { + if (has_channel(receiver, src_eid, sender)) { + channel(receiver, src_eid, sender).inbound_lazy_nonce + } else { + 0 + } + } + + public(friend) fun set_lazy_inbound_nonce( + receiver: address, + src_eid: u32, + sender: Bytes32, + nonce: u64, + ) acquires EndpointStore { + channel_mut(receiver, src_eid, sender).inbound_lazy_nonce = nonce; + } + + public(friend) fun has_payload_hash( + receiver: address, + src_eid: u32, + sender: Bytes32, + nonce: u64, + ): bool acquires EndpointStore { + if (has_channel(receiver, src_eid, sender)) { + table::contains(&channel(receiver, src_eid, sender).inbound_hashes, nonce) + } else { + false + } + } + + public(friend) fun get_payload_hash( + receiver: address, + src_eid: u32, + sender: Bytes32, + nonce: u64, + ): Bytes32 acquires EndpointStore { + assert!(has_channel(receiver, src_eid, sender), EUNREGISTERED_PATHWAY); + let hashes = &channel(receiver, src_eid, sender).inbound_hashes; + assert!(table::contains(hashes, nonce), ENO_PAYLOAD_HASH); + *table::borrow(hashes, nonce) + } + + public(friend) fun set_payload_hash( + receiver: address, + src_eid: u32, + sender: Bytes32, + nonce: u64, + hash: Bytes32, + ) acquires EndpointStore { + table::upsert(&mut channel_mut(receiver, src_eid, sender).inbound_hashes, nonce, hash); + } + + public(friend) fun remove_payload_hash( + receiver: address, + src_eid: u32, + sender: Bytes32, + nonce: u64, + ): Bytes32 acquires EndpointStore { + let hashes = &mut channel_mut(receiver, src_eid, sender).inbound_hashes; + assert!(table::contains(hashes, nonce), ENO_PAYLOAD_HASH); + table::remove(hashes, nonce) + } + + // ==================================================== Compose =================================================== + + public(friend) fun register_composer(composer: address, lz_compose_module: String) acquires EndpointStore { + assert!(!string::is_empty(&lz_compose_module), EEMPTY_MODULE_NAME); + table::add(composers_mut(), composer, ComposerStore { + lz_compose_module, + compose_message_hashes: table::new(), + }); + } + + public(friend) fun is_registered_composer(composer: address): bool acquires EndpointStore { + table::contains(composers(), composer) + } + + public(friend) fun lz_compose_module(composer: address): String acquires EndpointStore { + composer_store(composer).lz_compose_module + } + + public(friend) fun has_compose_message_hash( + from: address, + to: address, + guid: Bytes32, + index: u16, + ): bool acquires EndpointStore { + let compose_key = ComposeKey { from, guid, index }; + table::contains(&composer_store(to).compose_message_hashes, compose_key) + } + + public(friend) fun get_compose_message_hash( + from: address, + to: address, + guid: Bytes32, + index: u16, + ): Bytes32 acquires EndpointStore { + let compose_key = ComposeKey { from, guid, index }; + *table::borrow(&composer_store(to).compose_message_hashes, compose_key) + } + + public(friend) fun set_compose_message_hash( + from: address, + to: address, + guid: Bytes32, + index: u16, + hash: Bytes32, + ) acquires EndpointStore { + let compose_key = ComposeKey { from, guid, index }; + table::upsert(&mut composer_store_mut(to).compose_message_hashes, compose_key, hash); + } + + // =============================================== Message Libraries ============================================== + + public(friend) fun registered_msglibs(start: u64, max_count: u64): vector
acquires EndpointStore { + let msglibs = &store().msglibs; + let end = min(start + max_count, store().msglib_count); + if (start >= end) { return vector[] }; + + let result = vector[]; + for (i in start..end) { + let lib = *table::borrow(msglibs, i); + vector::push_back(&mut result, lib); + }; + result + } + + public(friend) fun add_msglib(lib: address) acquires EndpointStore { + let count = store().msglib_count; + table::add(&mut store_mut().msglibs, count, lib); + store_mut().msglib_count = count + 1; + + table::add(&mut store_mut().msglibs_registered, lib, true); + } + + public(friend) fun is_registered_msglib(lib: address): bool acquires EndpointStore { + table::contains(&store().msglibs_registered, lib) + } + + public(friend) fun has_default_send_library(dst_eid: u32): bool acquires EndpointStore { + table::contains(&store().msglibs_default_send_libs, dst_eid) + } + + public(friend) fun get_default_send_library(dst_eid: u32): address acquires EndpointStore { + let default_send_libs = &store().msglibs_default_send_libs; + assert!(table::contains(default_send_libs, dst_eid), EUNSUPPORTED_DST_EID); + *table::borrow(default_send_libs, dst_eid) + } + + public(friend) fun set_default_send_library(dst_eid: u32, lib: address) acquires EndpointStore { + table::upsert(&mut store_mut().msglibs_default_send_libs, dst_eid, lib); + } + + public(friend) fun has_default_receive_library(src_eid: u32): bool acquires EndpointStore { + table::contains(&store().msglibs_default_receive_libs, src_eid) + } + + public(friend) fun get_default_receive_library(src_eid: u32): address acquires EndpointStore { + let default_receive_libs = &store().msglibs_default_receive_libs; + assert!(table::contains(default_receive_libs, src_eid), EUNSUPPORTED_SRC_EID); + *table::borrow(default_receive_libs, src_eid) + } + + public(friend) fun set_default_receive_library(src_eid: u32, lib: address) acquires EndpointStore { + table::upsert(&mut store_mut().msglibs_default_receive_libs, src_eid, lib); + } + + public(friend) fun has_default_receive_library_timeout(src_eid: u32): bool acquires EndpointStore { + table::contains(&store().msglibs_default_receive_lib_timeout_configs, src_eid) + } + + public(friend) fun get_default_receive_library_timeout( + src_eid: u32, + ): Timeout acquires EndpointStore { + *table::borrow(&store().msglibs_default_receive_lib_timeout_configs, src_eid) + } + + public(friend) fun set_default_receive_library_timeout( + src_eid: u32, + config: Timeout, + ) acquires EndpointStore { + table::upsert(&mut store_mut().msglibs_default_receive_lib_timeout_configs, src_eid, config); + } + + public(friend) fun unset_default_receive_library_timeout( + src_eid: u32, + ): Timeout acquires EndpointStore { + table::remove(&mut store_mut().msglibs_default_receive_lib_timeout_configs, src_eid) + } + + public(friend) fun has_send_library(sender: address, dst_eid: u32): bool acquires EndpointStore { + table::contains(&oapp_store(sender).msglibs_send_libs, dst_eid) + } + + public(friend) fun get_send_library(sender: address, dst_eid: u32): address acquires EndpointStore { + *table::borrow(&oapp_store(sender).msglibs_send_libs, dst_eid) + } + + public(friend) fun set_send_library(sender: address, dst_eid: u32, lib: address) acquires EndpointStore { + table::upsert(&mut oapp_store_mut(sender).msglibs_send_libs, dst_eid, lib); + } + + public(friend) fun unset_send_library(sender: address, dst_eid: u32): address acquires EndpointStore { + table::remove(&mut oapp_store_mut(sender).msglibs_send_libs, dst_eid) + } + + public(friend) fun has_receive_library(receiver: address, src_eid: u32): bool acquires EndpointStore { + table::contains(&oapp_store(receiver).msglibs_receive_libs, src_eid) + } + + public(friend) fun get_receive_library(receiver: address, src_eid: u32): address acquires EndpointStore { + *table::borrow(&oapp_store(receiver).msglibs_receive_libs, src_eid) + } + + public(friend) fun set_receive_library( + receiver: address, + src_eid: u32, + lib: address, + ) acquires EndpointStore { + table::upsert(&mut oapp_store_mut(receiver).msglibs_receive_libs, src_eid, lib); + } + + public(friend) fun unset_receive_library(receiver: address, src_eid: u32): address acquires EndpointStore { + table::remove(&mut oapp_store_mut(receiver).msglibs_receive_libs, src_eid) + } + + public(friend) fun has_receive_library_timeout(receiver: address, src_eid: u32): bool acquires EndpointStore { + table::contains(&oapp_store(receiver).msglibs_receive_lib_timeout_configs, src_eid) + } + + public(friend) fun get_receive_library_timeout(receiver: address, src_eid: u32): Timeout acquires EndpointStore { + *table::borrow(&oapp_store(receiver).msglibs_receive_lib_timeout_configs, src_eid) + } + + public(friend) fun set_receive_library_timeout( + receiver: address, + src_eid: u32, + config: Timeout, + ) acquires EndpointStore { + table::upsert(&mut oapp_store_mut(receiver).msglibs_receive_lib_timeout_configs, src_eid, config); + } + + public(friend) fun unset_receive_library_timeout( + receiver: address, + src_eid: u32, + ): Timeout acquires EndpointStore { + table::remove(&mut oapp_store_mut(receiver).msglibs_receive_lib_timeout_configs, src_eid) + } + + // ================================================== Error Codes ================================================= + + const EEMPTY_MODULE_NAME: u64 = 1; + const ENO_PAYLOAD_HASH: u64 = 2; + const EUNREGISTERED_OAPP: u64 = 3; + const EUNREGISTERED_COMPOSER: u64 = 4; + const EUNREGISTERED_PATHWAY: u64 = 5; + const EUNSUPPORTED_DST_EID: u64 = 6; + const EUNSUPPORTED_SRC_EID: u64 = 7; +} diff --git a/packages/layerzero-v2/initia/contracts/endpoint_v2/sources/internal/timeout.move b/packages/layerzero-v2/initia/contracts/endpoint_v2/sources/internal/timeout.move new file mode 100644 index 00000000..c4001784 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/endpoint_v2/sources/internal/timeout.move @@ -0,0 +1,51 @@ +/// The Timeout configuration is used when migrating the Receive message library, and it defines the window of blocks +/// that the prior library will be allowed to receive messages. When the grace period expires, messages from the prior +/// library will be rejected. +module endpoint_v2::timeout { + use std::block::get_current_block_height; + + friend endpoint_v2::msglib_manager; + + friend endpoint_v2::endpoint; + + #[test_only] + friend endpoint_v2::msglib_manager_tests; + + struct Timeout has store, drop, copy { + // The block number at which the grace period expires + expiry: u64, + // The address of the fallback library + lib: address, + } + + /// Create a new timeout configuration + public(friend) fun new_timeout_from_grace_period(grace_period_in_blocks: u64, lib: address): Timeout { + let expiry = get_current_block_height() + grace_period_in_blocks; + Timeout { expiry, lib } + } + + /// Create a new timeout configuration using an expiry block number + public(friend) fun new_timeout_from_expiry(expiry: u64, lib: address): Timeout { + Timeout { expiry, lib } + } + + public(friend) fun unpack_timeout(timeout: Timeout): (u64, address) { + (timeout.expiry, timeout.lib) + } + + /// Check if the timeout is active + /// A timeout is active so long as the current block height is less than the expiry block number + public(friend) fun is_active(self: &Timeout): bool { + self.expiry > get_current_block_height() + } + + /// Get the address of the fallback library + public(friend) fun get_library(self: &Timeout): address { + self.lib + } + + /// Get the expiry block number + public(friend) fun get_expiry(self: &Timeout): u64 { + self.expiry + } +} \ No newline at end of file diff --git a/packages/layerzero-v2/initia/contracts/endpoint_v2/tests/endpoint_tests.move b/packages/layerzero-v2/initia/contracts/endpoint_v2/tests/endpoint_tests.move new file mode 100644 index 00000000..74e2bc77 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/endpoint_v2/tests/endpoint_tests.move @@ -0,0 +1,137 @@ +#[test_only] +module endpoint_v2::endpoint_tests { + use std::account::create_signer_for_test; + use std::fungible_asset::FungibleAsset; + use std::option; + use std::signer::address_of; + use std::string; + + use endpoint_v2::admin; + use endpoint_v2::channels; + use endpoint_v2::endpoint::{ + Self, + EndpointOAppConfigTarget, + quote, + register_composer, + register_oapp, send, + }; + use endpoint_v2::test_helpers; + use endpoint_v2_common::bytes32; + use endpoint_v2_common::contract_identity::make_call_ref_for_test; + use endpoint_v2_common::guid::compute_guid; + use endpoint_v2_common::native_token_test_helpers::{burn_token_for_test, mint_native_token_for_test}; + use endpoint_v2_common::packet_v1_codec::new_packet_v1; + use endpoint_v2_common::universal_config; + + #[test] + fun test_quote() { + let oapp = &create_signer_for_test(@1234); + let local_eid = 1u32; + endpoint_v2::test_helpers::setup_layerzero_for_test(@simple_msglib, local_eid, local_eid); + register_oapp(oapp, string::utf8(b"test")); + + let sender = std::signer::address_of(oapp); + let receiver = bytes32::from_address(sender); + let message = vector[1, 2, 3, 4]; + let options = vector[]; + let (native_fee, zro_fee) = quote(sender, local_eid, receiver, message, options, false); + assert!(native_fee == 0, 0); + assert!(zro_fee == 0, 1); + } + + #[test] + fun test_send() { + let oapp = &create_signer_for_test(@1234); + // account setup + let sender = std::signer::address_of(oapp); + + let local_eid = 1u32; + test_helpers::setup_layerzero_for_test(@simple_msglib, local_eid, local_eid); + register_oapp(oapp, string::utf8(b"test")); + + let receiver = bytes32::from_address(sender); + let message = vector[1, 2, 3, 4]; + let options = vector[]; + + let (native_fee, _) = quote(sender, local_eid, receiver, message, options, false); + + let payment_native = mint_native_token_for_test(native_fee); + let payment_zro = option::none(); + send( + &make_call_ref_for_test(sender), + local_eid, + receiver, + message, + options, + &mut payment_native, + &mut payment_zro, + ); + burn_token_for_test(payment_native); + + // check that the packet was emitted + let guid = compute_guid(1, local_eid, bytes32::from_address(sender), local_eid, receiver); + let packet = new_packet_v1( + universal_config::eid(), + bytes32::from_address(sender), + local_eid, + receiver, + 1, + guid, + message, + ); + let expected_event = channels::packet_sent_event(packet, options, @simple_msglib); + assert!(std::event::was_event_emitted(&expected_event), 0); + + option::destroy_none(payment_zro); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::endpoint::EUNREGISTERED)] + fun test_get_oapp_caller_fails_if_not_registered() { + test_helpers::setup_layerzero_for_test(@simple_msglib, 100, 100); + let oapp = &create_signer_for_test(@1234); + // registering composer should not count as registering oapp + register_composer(oapp, string::utf8(b"test")); + // Must provide type since get_oapp_caller is generic + let call_ref = &make_call_ref_for_test(address_of(oapp)); + endpoint_v2::endpoint::get_oapp_caller(call_ref); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::endpoint::EUNREGISTERED)] + fun test_get_compose_caller_fails_if_not_registered() { + test_helpers::setup_layerzero_for_test(@simple_msglib, 100, 100); + let composer = &create_signer_for_test(@1234); + // registering oapp should not count as registering composer + register_oapp(composer, string::utf8(b"test")); + let call_ref = &make_call_ref_for_test(address_of(composer)); + // Must provide type since get_oapp_caller is generic + endpoint_v2::endpoint::get_compose_caller(call_ref); + } + + #[test] + fun get_default_receive_library_timeout_should_return_none_if_not_set() { + test_helpers::setup_layerzero_for_test(@simple_msglib, 100, 100); + let (timeout, library) = endpoint::get_default_receive_library_timeout(1); + assert!(library == @0x0, 0); + assert!(timeout == 0, 1); + } + + #[test] + fun get_default_receive_library_timeout_should_return_the_timeout_if_set() { + let std = &std::account::create_account_for_test(@std); + std::block::initialize_for_test(std, 1_000_000); + std::reconfiguration::initialize_for_test(std); + + test_helpers::setup_layerzero_for_test(@simple_msglib, 100, 100); + admin::set_default_receive_library_timeout( + &create_signer_for_test(@layerzero_admin), + 12, + @simple_msglib, + 10000 + ); + let (timeout, library) = endpoint::get_default_receive_library_timeout(12); + assert!(library == @simple_msglib, 0); + assert!(timeout == 10000, 1); + } +} diff --git a/packages/layerzero-v2/initia/contracts/endpoint_v2/tests/internal/channels_tests.move b/packages/layerzero-v2/initia/contracts/endpoint_v2/tests/internal/channels_tests.move new file mode 100644 index 00000000..7ebceb12 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/endpoint_v2/tests/internal/channels_tests.move @@ -0,0 +1,992 @@ +#[test_only] +module endpoint_v2::channels_tests { + use std::account::create_signer_for_test; + use std::event::{emitted_events, was_event_emitted}; + use std::fungible_asset::FungibleAsset; + use std::option::{Self, destroy_none}; + use std::string; + use std::vector; + + use endpoint_v2::channels; + use endpoint_v2::channels::{ + burn, + clear_payload, + get_payload_hash, + has_payload_hash, + inbound, + inbound_nonce, + inbound_nonce_skipped_event, + nilify, + packet_burnt_event, + packet_nilified_event, + packet_verified_event, + PacketVerified, + send_internal, + skip, + verify, + verify_internal, + }; + use endpoint_v2::endpoint::register_oapp; + use endpoint_v2::messaging_receipt; + use endpoint_v2::msglib_manager; + use endpoint_v2::registration; + use endpoint_v2::store; + use endpoint_v2_common::bytes32; + use endpoint_v2_common::bytes32::{from_bytes32, to_bytes32}; + use endpoint_v2_common::guid; + use endpoint_v2_common::guid::compute_guid; + use endpoint_v2_common::native_token_test_helpers::{burn_token_for_test, mint_native_token_for_test}; + use endpoint_v2_common::packet_raw; + use endpoint_v2_common::packet_v1_codec; + use endpoint_v2_common::send_packet::unpack_send_packet; + use endpoint_v2_common::universal_config; + + #[test] + fun test_send_internal() { + let dst_eid = 102; + let oapp = @123; + universal_config::init_module_for_test(104); + store::init_module_for_test(); + let oapp_signer = &create_signer_for_test(oapp); + register_oapp(oapp_signer, string::utf8(b"receiver")); + + // register the default send library + store::set_default_send_library(dst_eid, @11114444); + + let packet = packet_raw::bytes_to_raw_packet(b"123456"); + let native_token = mint_native_token_for_test(100); + let zro_token = option::none(); + + let called = false; + assert!(!called, 0); // needed to avoid unused variable warning + + let receiver = bytes32::from_address(@0x1234); + let expected_guid = guid::compute_guid( + 1, + 104, + bytes32::from_address(oapp), + 102, + receiver, + ); + + let receipt = send_internal( + oapp, + dst_eid, + receiver, + b"payload", + b"options", + &mut native_token, + &mut zro_token, + |msglib, send_packet| { + called = true; + + // called correct message library + assert!(msglib == @11114444, 1); + + // called with correct packet + let (nonce, src_eid, sender, dst_eid, receiver, guid, message) = unpack_send_packet(send_packet); + assert!(nonce == 1, 2); + assert!(src_eid == 104, 3); + assert!(sender == bytes32::from_address(oapp), 4); + assert!(dst_eid == 102, 5); + assert!(receiver == receiver, 6); + assert!(guid == expected_guid, 7); + assert!(message == b"payload", 8); + + (200, 300, packet) + } + ); + + let (guid, nonce, native_fee, zro_fee) = messaging_receipt::unpack_messaging_receipt(receipt); + assert!(guid == expected_guid, 9); + assert!(nonce == 1, 10); + assert!(native_fee == 200, 11); + assert!(zro_fee == 300, 12); + + assert!(called, 1); + called = false; + assert!(!called, 1); + + // Try again (nonce = 2) with a OApp configured send library (instead of the default) + store::set_send_library(oapp, dst_eid, @22223333); + let receipt = send_internal( + oapp, + dst_eid, + receiver, + b"payload", + b"options", + &mut native_token, + &mut zro_token, + |msglib, _send_packet| { + called = true; + // called the msglib matches what is configured for the oapp + assert!(msglib == @22223333, 1); + (2000, 3000, packet) + } + ); + assert!(called, 1); + + let (guid, nonce, native_fee, zro_fee) = messaging_receipt::unpack_messaging_receipt(receipt); + + // nonce should increase to 2 + assert!(nonce == 2, 10); + assert!(native_fee == 2000, 11); + assert!(zro_fee == 3000, 12); + + let expected_guid = guid::compute_guid( + 2, + 104, + bytes32::from_address(oapp), + 102, + receiver, + ); + assert!(guid == expected_guid, 13); + + burn_token_for_test(native_token); + destroy_none(zro_token); + } + + #[test] + fun test_quote_internal() { + let dst_eid = 102; + let oapp = @123; + + universal_config::init_module_for_test(104); + store::init_module_for_test(); + let oapp_signer = &create_signer_for_test(oapp); + + register_oapp(oapp_signer, string::utf8(b"receiver")); + + // register the default send library + store::set_default_send_library(dst_eid, @11114444); + + let called = false; + assert!(!called, 0); // needed to avoid unused variable warning + + let (native_fee, zro_fee) = channels::quote_internal( + oapp, + dst_eid, + bytes32::from_address(@0x1234), + b"payload", + |msglib, send_packet| { + called = true; + assert!(msglib == @11114444, 1); + let (nonce, src_eid, sender, dst_eid, receiver, guid, message) = unpack_send_packet(send_packet); + assert!(nonce == 1, 2); + assert!(src_eid == 104, 3); + assert!(sender == bytes32::from_address(@123), 4); + assert!(dst_eid == 102, 5); + assert!(receiver == bytes32::from_address(@0x1234), 6); + assert!(message == b"payload", 7); + + let expected_guid = compute_guid( + 1, + 104, + bytes32::from_address(oapp), + dst_eid, + bytes32::from_address(@0x1234), + ); + + assert!(guid == expected_guid, 8); + (200, 300) + } + ); + assert!(called, 1); + assert!(native_fee == 200, 2); + assert!(zro_fee == 300, 3); + + // call send() to increment nonce + let native_token = mint_native_token_for_test(100); + let zro_token = option::none(); + send_internal( + oapp, + dst_eid, + bytes32::from_address(@0x1234), + b"payload", + b"options", + &mut native_token, + &mut zro_token, + |msglib, _send_packet| { + called = true; + // called the msglib matches what is configured for the oapp + assert!(msglib == @11114444, 1); + let packet = packet_raw::bytes_to_raw_packet(b"123456"); + (2000, 3000, packet) + } + ); + + burn_token_for_test(native_token); + destroy_none(zro_token); + + assert!(called, 0); + called = false; + assert!(!called, 0); + + // call again with higher nonce state and a OApp configured send library + store::set_send_library(oapp, dst_eid, @22223333); + let (native_fee, zro_fee) = channels::quote_internal( + oapp, + dst_eid, + bytes32::from_address(@0x1234), + b"payload", + |msglib, send_packet| { + called = true; + assert!(msglib == @22223333, 1); + let (nonce, src_eid, sender, dst_eid, receiver, guid, message) = unpack_send_packet(send_packet); + assert!(nonce == 2, 2); + assert!(src_eid == 104, 3); + assert!(sender == bytes32::from_address(@123), 4); + assert!(dst_eid == 102, 5); + assert!(receiver == bytes32::from_address(@0x1234), 6); + assert!(message == b"payload", 7); + + let expected_guid = compute_guid( + 2, + 104, + bytes32::from_address(oapp), + dst_eid, + bytes32::from_address(@0x1234), + ); + + assert!(guid == expected_guid, 8); + (2000, 3000) + } + ); + + assert!(called, 0); + assert!(native_fee == 2000, 2); + assert!(zro_fee == 3000, 3); + } + + #[test] + fun test_register_receive_pathway() { + let oapp = @123; + let src_eid = 0x2; + let sender = bytes32::from_address(@0x1234); + store::init_module_for_test(); + + let oapp_signer = &create_signer_for_test(oapp); + register_oapp(oapp_signer, string::utf8(b"receiver")); + channels::register_receive_pathway(oapp, src_eid, sender); + assert!(was_event_emitted(&channels::receive_pathway_registered_event(oapp, src_eid, from_bytes32(sender))), 0); + assert!(channels::receive_pathway_registered(oapp, src_eid, sender), 0); + } + + #[test] + fun test_inbound() { + let oapp = @123; + let src_eid = 0x2; + let sender = bytes32::from_address(@0x1234); + let payload = b"payload"; + let payload_hash = bytes32::keccak256(payload); + store::init_module_for_test(); + let oapp_signer = &create_signer_for_test(oapp); + register_oapp(oapp_signer, string::utf8(b"receiver")); + channels::register_receive_pathway(oapp, src_eid, sender); + assert!(store::lazy_inbound_nonce(oapp, src_eid, sender) == 0, 0); + + // Inbound #1 + inbound(oapp, src_eid, sender, 1, payload_hash); + assert!(has_payload_hash(oapp, src_eid, sender, 1), 1); + assert!(get_payload_hash(oapp, src_eid, sender, 1) == payload_hash, 2); + + // Inbound #2 + inbound(oapp, src_eid, sender, 2, payload_hash); + assert!(has_payload_hash(oapp, src_eid, sender, 2), 3); + assert!(get_payload_hash(oapp, src_eid, sender, 2) == payload_hash, 4); + + // Inbound nonce should increment, but not lazy without clearing + assert!( + store::lazy_inbound_nonce(oapp, src_eid, sender) == 0, + 5, + ); // inbound does not increment lazy inbound + assert!(inbound_nonce(oapp, src_eid, sender) == 2, 6); // inbound_nonce is incremented + + // Only if cleared does the lazy increment to the inbound + clear_payload(oapp, src_eid, sender, 2, payload); + assert!( + store::lazy_inbound_nonce(oapp, src_eid, sender) == 2, + 5, + ); // inbound does not increment lazy inbound + assert!(inbound_nonce(oapp, src_eid, sender) == 2, 6); // inbound_nonce is incremented + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::channels::EEMPTY_PAYLOAD_HASH)] + fun test_inbound_should_not_accept_empty_payload_hash() { + let oapp = @123; + let src_eid = 0x2; + let sender = bytes32::from_address(@0x1234); + let nonce = 0x1; + let payload_hash = bytes32::zero_bytes32(); + store::init_module_for_test(); + let oapp_signer = &create_signer_for_test(oapp); + register_oapp(oapp_signer, string::utf8(b"receiver")); + channels::register_receive_pathway(oapp, src_eid, sender); + inbound(oapp, src_eid, sender, nonce, payload_hash); + } + + #[test] + fun test_inbound_nonce() { + let oapp = @123; + let src_eid = 0x2; + let sender = bytes32::from_address(@0x1234); + let payload = b"payload"; + let payload_hash = bytes32::keccak256(payload); + + store::init_module_for_test(); + let oapp_signer = &create_signer_for_test(oapp); + register_oapp(oapp_signer, string::utf8(b"receiver")); + channels::register_receive_pathway(oapp, src_eid, sender); + assert!(inbound_nonce(oapp, src_eid, sender) == 0, 0); + inbound(oapp, src_eid, sender, 1, payload_hash); + assert!(inbound_nonce(oapp, src_eid, sender) == 1, 1); + inbound(oapp, src_eid, sender, 2, payload_hash); + assert!(inbound_nonce(oapp, src_eid, sender) == 2, 2); + } + + #[test] + fun test_skip() { + let oapp = @123; + let src_eid = 0x2; + let sender = bytes32::from_address(@0x1234); + store::init_module_for_test(); + let oapp_signer = &create_signer_for_test(oapp); + register_oapp(oapp_signer, string::utf8(b"receiver")); + channels::register_receive_pathway(oapp, src_eid, sender); + assert!(inbound_nonce(oapp, src_eid, sender) == 0, 0); + skip(oapp, src_eid, sender, 1); + assert!(inbound_nonce(oapp, src_eid, sender) == 1, 1); + assert!(was_event_emitted(&inbound_nonce_skipped_event(src_eid, from_bytes32(sender), oapp, 1)), 2); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::channels::EINVALID_NONCE)] + fun test_skip_invalid_nonce() { + let oapp = @123; + let src_eid = 0x2; + let sender = bytes32::from_address(@0x1234); + store::init_module_for_test(); + let oapp_signer = &create_signer_for_test(oapp); + register_oapp(oapp_signer, string::utf8(b"receiver")); + channels::register_receive_pathway(oapp, src_eid, sender); + assert!(inbound_nonce(oapp, src_eid, sender) == 0, 0); + skip(oapp, src_eid, sender, 2); // skip_to_nonce must be 1 to succeed + } + + #[test] + fun test_nilify() { + let oapp = @123; + let src_eid = 0x2; + let sender = bytes32::from_address(@0x1234); + let payload = b"payload"; + let payload_hash = bytes32::keccak256(payload); + store::init_module_for_test(); + let oapp_signer = &create_signer_for_test(oapp); + register_oapp(oapp_signer, string::utf8(b"receiver")); + channels::register_receive_pathway(oapp, src_eid, sender); + + // Inbound #1 + inbound(oapp, src_eid, sender, 1, payload_hash); + assert!(has_payload_hash(oapp, src_eid, sender, 1), 0); + assert!(get_payload_hash(oapp, src_eid, sender, 1) == payload_hash, 1); + + // Inbound #2 + inbound(oapp, src_eid, sender, 2, payload_hash); + assert!(has_payload_hash(oapp, src_eid, sender, 2), 2); + assert!(get_payload_hash(oapp, src_eid, sender, 2) == payload_hash, 3); + + assert!(inbound_nonce(oapp, src_eid, sender) == 2, 3); + + // Nilify + nilify(oapp, src_eid, sender, 1, payload_hash); + assert!(was_event_emitted( + &packet_nilified_event(src_eid, from_bytes32(sender), oapp, 1, from_bytes32(payload_hash)) + ), 4); + + assert!(inbound_nonce(oapp, src_eid, sender) == 2, 3); + assert!( + get_payload_hash(oapp, src_eid, sender, 1) == bytes32::to_bytes32( + x"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + ), + 3, + ); // 1 is nilified + assert!(has_payload_hash(oapp, src_eid, sender, 2), 2); // 2 is not nilified + } + + #[test] + fun test_nilify_for_non_verified() { + let oapp = @123; + let src_eid = 0x2; + let sender = bytes32::from_address(@0x1234); + let payload = b"payload"; + let payload_hash = bytes32::keccak256(payload); + store::init_module_for_test(); + let oapp_signer = &create_signer_for_test(oapp); + register_oapp(oapp_signer, string::utf8(b"receiver")); + channels::register_receive_pathway(oapp, src_eid, sender); + + // Inbound #1 + inbound(oapp, src_eid, sender, 1, payload_hash); + assert!(has_payload_hash(oapp, src_eid, sender, 1), 0); + assert!(get_payload_hash(oapp, src_eid, sender, 1) == payload_hash, 1); + + // Inbound #2 + inbound(oapp, src_eid, sender, 2, payload_hash); + assert!(has_payload_hash(oapp, src_eid, sender, 2), 2); + assert!(get_payload_hash(oapp, src_eid, sender, 2) == payload_hash, 3); + + assert!(inbound_nonce(oapp, src_eid, sender) == 2, 3); + + let empty_payload_hash = x"0000000000000000000000000000000000000000000000000000000000000000"; + // Nilify + nilify( + oapp, + src_eid, + sender, + 3, + to_bytes32(empty_payload_hash), + ); + assert!(was_event_emitted( + &packet_nilified_event(src_eid, from_bytes32(sender), oapp, 3, empty_payload_hash) + ), 4); + + // inbound nonce increments to be beyond the nilified nonce + assert!(inbound_nonce(oapp, src_eid, sender) == 3, 3); + assert!( + get_payload_hash(oapp, src_eid, sender, 3) == bytes32::to_bytes32( + x"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + ), + 3, + ); // 3 is nilified + } + + #[test] + fun test_nilify_for_non_verified_2() { + let oapp = @123; + let src_eid = 0x2; + let sender = bytes32::from_address(@0x1234); + let payload = b"payload"; + let payload_hash = bytes32::keccak256(payload); + store::init_module_for_test(); + let oapp_signer = &create_signer_for_test(oapp); + register_oapp(oapp_signer, string::utf8(b"receiver")); + channels::register_receive_pathway(oapp, src_eid, sender); + + // Inbound #1 (don't do second inbound so there is a gap) + inbound(oapp, src_eid, sender, 1, payload_hash); + assert!(has_payload_hash(oapp, src_eid, sender, 1), 0); + assert!(get_payload_hash(oapp, src_eid, sender, 1) == payload_hash, 1); + + assert!(inbound_nonce(oapp, src_eid, sender) == 1, 3); + + let empty_payload_hash = x"0000000000000000000000000000000000000000000000000000000000000000"; + // Nilify + nilify( + oapp, + src_eid, + sender, + 3, + to_bytes32(empty_payload_hash), + ); + assert!(was_event_emitted( + &packet_nilified_event(src_eid, from_bytes32(sender), oapp, 3, empty_payload_hash) + ), 4); + + // inbound nonce doesn't increment + assert!(inbound_nonce(oapp, src_eid, sender) == 1, 3); + assert!( + get_payload_hash(oapp, src_eid, sender, 3) == bytes32::to_bytes32( + x"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + ), + 3, + ); // 3 is nilified + } + + #[test] + fun test_can_skip_after_nillify() { + let oapp = @123; + let src_eid = 0x2; + let sender = bytes32::from_address(@0x1234); + let payload = b"payload"; + let payload_hash = bytes32::keccak256(payload); + store::init_module_for_test(); + let oapp_signer = &create_signer_for_test(oapp); + register_oapp(oapp_signer, string::utf8(b"receiver")); + channels::register_receive_pathway(oapp, src_eid, sender); + + inbound(oapp, src_eid, sender, 1, payload_hash); + inbound(oapp, src_eid, sender, 2, payload_hash); + + nilify(oapp, src_eid, sender, 2, payload_hash); + nilify(oapp, src_eid, sender, 1, payload_hash); + + skip(oapp, src_eid, sender, 3); + + assert!(inbound_nonce(oapp, src_eid, sender) == 3, 0); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::channels::EINVALID_NONCE)] + fun test_cannot_skip_to_non_next_nonce() { + let oapp = @123; + let src_eid = 0x2; + let sender = bytes32::from_address(@0x1234); + let payload = b"payload"; + let payload_hash = bytes32::keccak256(payload); + store::init_module_for_test(); + let oapp_signer = &create_signer_for_test(oapp); + register_oapp(oapp_signer, string::utf8(b"receiver")); + channels::register_receive_pathway(oapp, src_eid, sender); + + inbound(oapp, src_eid, sender, 1, payload_hash); + inbound(oapp, src_eid, sender, 2, payload_hash); + + assert!(inbound_nonce(oapp, src_eid, sender) == 2, 0); + + skip(oapp, src_eid, sender, 2); // skip_to_nonce must be 3 to succeed + } + + + #[test] + fun test_burn() { + let oapp = @123; + let src_eid = 0x2; + let sender = bytes32::from_address(@0x1234); + let payload = b"payload"; + let payload_hash = bytes32::keccak256(payload); + store::init_module_for_test(); + let oapp_signer = &create_signer_for_test(oapp); + register_oapp(oapp_signer, string::utf8(b"receiver")); + channels::register_receive_pathway(oapp, src_eid, sender); + + inbound(oapp, src_eid, sender, 1, payload_hash); + inbound(oapp, src_eid, sender, 2, payload_hash); + assert!(inbound_nonce(oapp, src_eid, sender) == 2, 0); + + skip(oapp, src_eid, sender, 3); + + burn(oapp, src_eid, sender, 1, payload_hash); + assert!( + was_event_emitted(&packet_burnt_event(src_eid, from_bytes32(sender), oapp, 1, from_bytes32(payload_hash))), + 1, + ); + + assert!(store::lazy_inbound_nonce(oapp, src_eid, sender) == 3, 2); + assert!(!has_payload_hash(oapp, src_eid, sender, 1), 3); // 1 is burnt + assert!(has_payload_hash(oapp, src_eid, sender, 2), 4); // 2 is not burnt + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::channels::EINVALID_NONCE)] + fun test_cannot_burn_a_nonce_after_lazy_inbound_nonce() { + let oapp = @123; + let src_eid = 0x2; + let sender = bytes32::from_address(@0x1234); + let payload = b"payload"; + let payload_hash = bytes32::keccak256(payload); + store::init_module_for_test(); + let oapp_signer = &create_signer_for_test(oapp); + register_oapp(oapp_signer, string::utf8(b"receiver")); + channels::register_receive_pathway(oapp, src_eid, sender); + + inbound(oapp, src_eid, sender, 1, payload_hash); + inbound(oapp, src_eid, sender, 2, payload_hash); + assert!(inbound_nonce(oapp, src_eid, sender) == 2, 0); + + assert!(store::lazy_inbound_nonce(oapp, src_eid, sender) == 0, 1); + burn(oapp, src_eid, sender, 1, payload_hash); // lazy inbound is still 0 + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::channels::ENO_PAYLOAD_HASH)] + fun test_cannot_clear_a_burnt_nonce() { + let oapp = @123; + let src_eid = 0x2; + let sender = bytes32::from_address(@0x1234); + let payload = b"payload"; + let payload_hash = bytes32::keccak256(payload); + store::init_module_for_test(); + let oapp_signer = &create_signer_for_test(oapp); + register_oapp(oapp_signer, string::utf8(b"receiver")); + channels::register_receive_pathway(oapp, src_eid, sender); + + inbound(oapp, src_eid, sender, 1, payload_hash); + inbound(oapp, src_eid, sender, 2, payload_hash); + assert!(inbound_nonce(oapp, src_eid, sender) == 2, 0); + + clear_payload(oapp, src_eid, sender, 2, payload); // clear 2 + assert!(!has_payload_hash(oapp, src_eid, sender, 2), 1); // 2 is cleared + assert!(has_payload_hash(oapp, src_eid, sender, 1), 2); // still has hash #1 + + burn(oapp, src_eid, sender, 1, payload_hash); // burn 1 + assert!(!has_payload_hash(oapp, src_eid, sender, 1), 3); // 1 is burnt + + clear_payload(oapp, src_eid, sender, 1, payload); // cannot clear 1 + } + + #[test] + fun test_clear_payload() { + let oapp = @123; + let src_eid = 0x2; + let sender = bytes32::from_address(@0x1234); + let payload = b"payload"; + let payload_hash = bytes32::keccak256(payload); + store::init_module_for_test(); + let oapp_signer = &create_signer_for_test(oapp); + register_oapp(oapp_signer, string::utf8(b"receiver")); + channels::register_receive_pathway(oapp, src_eid, sender); + + inbound(oapp, src_eid, sender, 1, payload_hash); + inbound(oapp, src_eid, sender, 2, payload_hash); + assert!(store::lazy_inbound_nonce(oapp, src_eid, sender) == 0, 1); + assert!(inbound_nonce(oapp, src_eid, sender) == 2, 0); + + // clear 2 + clear_payload(oapp, src_eid, sender, 2, payload); + + assert!(store::lazy_inbound_nonce(oapp, src_eid, sender) == 2, 1); + assert!(inbound_nonce(oapp, src_eid, sender) == 2, 0); + assert!(!has_payload_hash(oapp, src_eid, sender, 2), 2); + + inbound(oapp, src_eid, sender, 3, payload_hash); + inbound(oapp, src_eid, sender, 4, payload_hash); + + // clear 1, 4, 3 + clear_payload(oapp, src_eid, sender, 1, payload); + clear_payload(oapp, src_eid, sender, 4, payload); + clear_payload(oapp, src_eid, sender, 3, payload); + } + + #[test] + fun test_can_clear_out_of_order() { + let oapp = @123; + let src_eid = 0x2; + let sender = bytes32::from_address(@0x1234); + let payload = b"payload"; + let payload_hash = bytes32::keccak256(payload); + store::init_module_for_test(); + let oapp_signer = &create_signer_for_test(oapp); + register_oapp(oapp_signer, string::utf8(b"receiver")); + channels::register_receive_pathway(oapp, src_eid, sender); + + inbound(oapp, src_eid, sender, 1, payload_hash); + inbound(oapp, src_eid, sender, 2, payload_hash); + assert!(store::lazy_inbound_nonce(oapp, src_eid, sender) == 0, 1); + assert!(inbound_nonce(oapp, src_eid, sender) == 2, 0); + + clear_payload(oapp, src_eid, sender, 2, payload); + assert!(has_payload_hash(oapp, src_eid, sender, 1), 2); // still has hash #1 + assert!(!has_payload_hash(oapp, src_eid, sender, 2), 2); // hash #2 removed + + clear_payload(oapp, src_eid, sender, 1, payload); + assert!(!has_payload_hash(oapp, src_eid, sender, 1), 2); // now hash #1 removed + assert!(store::lazy_inbound_nonce(oapp, src_eid, sender) == 2, 1); + assert!(inbound_nonce(oapp, src_eid, sender) == 2, 0); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::channels::EPAYLOAD_HASH_DOES_NOT_MATCH)] + fun test_clear_payload_should_fail_with_invalid_payload() { + let oapp = @123; + let src_eid = 0x2; + let sender = bytes32::from_address(@0x1234); + let payload = b"payload"; + let payload_hash = bytes32::keccak256(payload); + let invalid_payload = b"invalid_payload"; + store::init_module_for_test(); + let oapp_signer = &create_signer_for_test(oapp); + register_oapp(oapp_signer, string::utf8(b"receiver")); + channels::register_receive_pathway(oapp, src_eid, sender); + + inbound(oapp, src_eid, sender, 1, payload_hash); + inbound(oapp, src_eid, sender, 2, payload_hash); + assert!(store::lazy_inbound_nonce(oapp, src_eid, sender) == 0, 1); + assert!(inbound_nonce(oapp, src_eid, sender) == 2, 0); + + clear_payload(oapp, src_eid, sender, 2, invalid_payload); + clear_payload(oapp, src_eid, sender, 1, invalid_payload); + } + + #[test] + fun test_verify() { + // packet details + let receiver_oapp = @123; + let src_eid = 0x2; + let dst_eid = 0x3; + let sender = bytes32::from_address(@0x1234); + let receiver = bytes32::from_address(receiver_oapp); + let payload = b"payload"; + let payload_hash = bytes32::keccak256(payload); + let packet_header = packet_v1_codec::new_packet_v1_header_only( + src_eid, + sender, + dst_eid, + receiver, + 1, + ); + universal_config::init_module_for_test(dst_eid); + store::init_module_for_test(); + + // register destination defaults + msglib_manager::register_library(@simple_msglib); + msglib_manager::set_default_receive_library(src_eid, @simple_msglib, 0); + + // register destination + registration::register_oapp(receiver_oapp, string::utf8(b"receiver")); + channels::register_receive_pathway(receiver_oapp, src_eid, sender); // register pathway to receive from sender + + // initialize destination on simple_msglib + simple_msglib::msglib::initialize_for_test(); + + verify( + @simple_msglib, + packet_header, + payload_hash, + b"", + ); + + assert!(was_event_emitted(&packet_verified_event( + src_eid, + from_bytes32(sender), + 1, + bytes32::to_address(receiver), + from_bytes32(payload_hash), + )), 0); + } + + #[test] + fun test_verify_internal() { + // packet details + let receiver_oapp = @0x123; + let src_eid = 102; + let sender = bytes32::from_address(@0x1234); + let payload = b"payload"; + let payload_hash = bytes32::keccak256(payload); + let nonce = 11; + // use blocked message lib to ensure that it's not actually called when using the internal verify function, + // except the version() function, which is called upon library registration, but not in verify_internal() + let msglib = @blocked_msglib; + store::init_module_for_test(); + + // register destination defaults + msglib_manager::register_library(msglib); + msglib_manager::set_default_receive_library(src_eid, msglib, 0); + + // register destination + registration::register_oapp(receiver_oapp, string::utf8(b"receiver")); + channels::register_receive_pathway(receiver_oapp, src_eid, sender); // register pathway to receive from sender + + verify_internal( + msglib, + payload_hash, + receiver_oapp, + src_eid, + sender, + nonce, + ); + + assert!(was_event_emitted(&packet_verified_event( + src_eid, + from_bytes32(sender), + nonce, + receiver_oapp, + from_bytes32(payload_hash), + )), 0); + } + + + #[test] + fun test_verifiable() { + store::init_module_for_test(); + // returns false if pathway not registered + assert!(!channels::verifiable(@222, 1, bytes32::from_address(@123), 1), 0); + + // still returns false if registered but no pathway + registration::register_oapp(@123, string::utf8(b"receiver")); + assert!(!channels::verifiable(@222, 1, bytes32::from_address(@123), 1), 1); + + // return true if nonce (1) > lazy inbound(0) + channels::register_receive_pathway(@123, 1, bytes32::from_address(@222)); + assert!(channels::verifiable(@123, 1, bytes32::from_address(@222), 1), 1); + + // return false if nonce (0) <= lazy inbound(0) + assert!(!channels::verifiable(@123, 1, bytes32::from_address(@222), 0), 2); + + // return true if nonce (0) > lazy inbound(0) but has payload hash + channels::inbound(@123, 1, bytes32::from_address(@222), 0, bytes32::from_address(@999)); + assert!(channels::verifiable(@123, 1, bytes32::from_address(@222), 0), 2); + } + + #[test] + fun test_verify_allows_reverifying_if_not_executed() { + // packet details + let receiver_oapp = @123; + let src_eid = 0x2; + let dst_eid = 0x3; + let sender = bytes32::from_address(@0x1234); + let receiver = bytes32::from_address(receiver_oapp); + let payload = b"payload"; + let payload_hash = bytes32::keccak256(payload); + let packet_header = packet_v1_codec::new_packet_v1_header_only( + src_eid, + sender, + dst_eid, + receiver, + 1, + ); + store::init_module_for_test(); + universal_config::init_module_for_test(dst_eid); + + // register destination defaults + msglib_manager::register_library(@simple_msglib); + msglib_manager::set_default_receive_library(src_eid, @simple_msglib, 0); + + // register destination + registration::register_oapp(receiver_oapp, string::utf8(b"receiver")); + channels::register_receive_pathway(receiver_oapp, src_eid, sender); // register pathway to receive from sender + + // initialize destination on simple_msglib + simple_msglib::msglib::initialize_for_test(); + + verify( + @simple_msglib, + packet_header, + payload_hash, + b"", + ); + assert!(vector::length(&emitted_events()) == 1, 0); + verify( + @simple_msglib, + packet_header, + payload_hash, + b"", + ); + assert!(vector::length(&emitted_events()) == 2, 0); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::store::EUNREGISTERED_PATHWAY)] + fun test_verify_fails_if_unregistered_pathway() { + // packet details + let receiver_oapp = @123; + let src_eid = 0x2; + let dst_eid = 0x3; + let sender = bytes32::from_address(@0x1234); + let receiver = bytes32::from_address(receiver_oapp); + let payload = b"payload"; + let payload_hash = bytes32::keccak256(payload); + let packet_header = packet_v1_codec::new_packet_v1_header_only( + src_eid, + sender, + dst_eid, + receiver, + 1, + ); + store::init_module_for_test(); + universal_config::init_module_for_test(dst_eid); + + // register destination defaults + msglib_manager::register_library(@simple_msglib); + msglib_manager::set_default_receive_library(src_eid, @simple_msglib, 0); + + // register destination + registration::register_oapp(receiver_oapp, string::utf8(b"receiver")); + + // initialize destination on simple_msglib + simple_msglib::msglib::initialize_for_test(); + + verify( + @simple_msglib, + packet_header, + payload_hash, + b"", + ); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::channels::EINVALID_MSGLIB)] + fun test_verify_fails_receive_library_doesnt_match() { + // packet details + let receiver_oapp = @123; + let src_eid = 0x2; + let dst_eid = 0x3; + let sender = bytes32::from_address(@0x1234); + let receiver = bytes32::from_address(receiver_oapp); + let payload = b"payload"; + let payload_hash = bytes32::keccak256(payload); + let packet_header = packet_v1_codec::new_packet_v1_header_only( + src_eid, + sender, + dst_eid, + receiver, + 1, + ); + store::init_module_for_test(); + universal_config::init_module_for_test(dst_eid); + + // register destination + registration::register_oapp(receiver_oapp, string::utf8(b"receiver")); + channels::register_receive_pathway(receiver_oapp, src_eid, sender); // register pathway to receive from sender + + // register destination defaults + msglib_manager::register_library(@simple_msglib); + msglib_manager::register_library(@blocked_msglib); + msglib_manager::set_default_receive_library(src_eid, @simple_msglib, 0); + // oapp registers a different library + msglib_manager::set_receive_library(receiver_oapp, src_eid, @blocked_msglib, 0); + + + // initialize destination on simple_msglib + simple_msglib::msglib::initialize_for_test(); + + verify( + @simple_msglib, + packet_header, + payload_hash, + b"", + ); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::channels::ENOT_VERIFIABLE)] + fun test_verify_fails_if_nonce_already_processed() { + // packet details + let receiver_oapp = @123; + let src_eid = 0x2; + let dst_eid = 0x3; + let sender = bytes32::from_address(@0x1234); + let receiver = bytes32::from_address(receiver_oapp); + let payload = b"payload"; + let payload_hash = bytes32::keccak256(payload); + let packet_header = packet_v1_codec::new_packet_v1_header_only( + src_eid, + sender, + dst_eid, + receiver, + 1, + ); + store::init_module_for_test(); + universal_config::init_module_for_test(dst_eid); + + // register destination defaults + msglib_manager::register_library(@simple_msglib); + msglib_manager::set_default_receive_library(src_eid, @simple_msglib, 0); + + // register destination + registration::register_oapp(receiver_oapp, string::utf8(b"receiver")); + channels::register_receive_pathway(receiver_oapp, src_eid, sender); // register pathway to receive from sender + + // initialize destination on simple_msglib + simple_msglib::msglib::initialize_for_test(); + + skip(receiver_oapp, src_eid, sender, 1); + + verify( + @simple_msglib, + packet_header, + payload_hash, + b"", + ); + } +} diff --git a/packages/layerzero-v2/initia/contracts/endpoint_v2/tests/internal/messaging_composer_tests.move b/packages/layerzero-v2/initia/contracts/endpoint_v2/tests/internal/messaging_composer_tests.move new file mode 100644 index 00000000..45c8748f --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/endpoint_v2/tests/internal/messaging_composer_tests.move @@ -0,0 +1,52 @@ +#[test_only] +module endpoint_v2::messaging_composer_tests { + use std::string; + + use endpoint_v2::messaging_composer::{clear_compose, send_compose}; + use endpoint_v2::store; + use endpoint_v2_common::bytes32; + + #[test] + fun test_send_compose_and_clear() { + let oapp: address = @0x1; + let to = @0x2; + let index = 0; + let guid = bytes32::to_bytes32(b"................................"); + let message = b"message"; + store::init_module_for_test(); + store::register_composer(to, string::utf8(b"test")); + + send_compose(oapp, to, index, guid, message); + clear_compose(oapp, to, guid, index, message); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::messaging_composer::ECOMPOSE_NOT_FOUND)] + fun test_cannot_clear_before_send_compose() { + let oapp: address = @0x1; + let to = @0x2; + let index = 0; + let guid = bytes32::to_bytes32(b"................................"); + let message = b"message"; + store::init_module_for_test(); + store::register_composer(to, string::utf8(b"test")); + + clear_compose(oapp, to, guid, index, message); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::messaging_composer::ECOMPOSE_ALREADY_CLEARED)] + fun test_cannot_clear_same_message_twice() { + let oapp: address = @0x1; + let to = @0x2; + let index = 0; + let guid = bytes32::to_bytes32(b"................................"); + let message = b"message"; + store::init_module_for_test(); + store::register_composer(to, string::utf8(b"test")); + + send_compose(oapp, to, index, guid, message); + clear_compose(oapp, to, guid, index, message); + clear_compose(oapp, to, guid, index, message); + } +} diff --git a/packages/layerzero-v2/initia/contracts/endpoint_v2/tests/internal/messaging_receipt_tests.move b/packages/layerzero-v2/initia/contracts/endpoint_v2/tests/internal/messaging_receipt_tests.move new file mode 100644 index 00000000..8aaa72d9 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/endpoint_v2/tests/internal/messaging_receipt_tests.move @@ -0,0 +1,27 @@ +#[test_only] +module endpoint_v2::messaging_receipt_tests { + use endpoint_v2_common::bytes32; + + #[test] + fun test_messaging_receipt() { + let guid = bytes32::from_address(@0x12345678); + let nonce = 12; + let native_fee = 100; + let zro_fee = 10; + + let receipt = endpoint_v2::messaging_receipt::new_messaging_receipt(guid, nonce, native_fee, zro_fee); + + // check getters + assert!(endpoint_v2::messaging_receipt::get_guid(&receipt) == guid, 0); + assert!(endpoint_v2::messaging_receipt::get_nonce(&receipt) == nonce, 0); + assert!(endpoint_v2::messaging_receipt::get_native_fee(&receipt) == native_fee, 0); + assert!(endpoint_v2::messaging_receipt::get_zro_fee(&receipt) == zro_fee, 0); + + // check unpacked + let (guid_, nonce_, native_fee_, zro_fee_) = endpoint_v2::messaging_receipt::unpack_messaging_receipt(receipt); + assert!(guid_ == guid, 0); + assert!(nonce_ == nonce, 0); + assert!(native_fee_ == native_fee, 0); + assert!(zro_fee_ == zro_fee, 0); + } +} diff --git a/packages/layerzero-v2/initia/contracts/endpoint_v2/tests/internal/msglib_manager_tests.move b/packages/layerzero-v2/initia/contracts/endpoint_v2/tests/internal/msglib_manager_tests.move new file mode 100644 index 00000000..27aecf4f --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/endpoint_v2/tests/internal/msglib_manager_tests.move @@ -0,0 +1,806 @@ +#[test_only] +module endpoint_v2::msglib_manager_tests { + use std::account::create_signer_for_test; + use std::event::was_event_emitted; + use std::string; + use std::timestamp; + + use endpoint_v2::endpoint::{get_effective_send_library, get_registered_libraries, is_registered_library}; + use endpoint_v2::msglib_manager::{ + default_receive_library_set_event, + default_receive_library_timeout_set_event, + default_send_library_set_event, + get_default_receive_library, + get_default_send_library, + get_effective_receive_library, + is_valid_receive_library_for_oapp, + library_registered_event, + matches_default_receive_library, + receive_library_set_event, + receive_library_timeout_set_event, + register_library, + send_library_set_event, + set_default_receive_library, + set_default_receive_library_timeout, + set_default_send_library, + set_receive_library, + set_receive_library_timeout, + set_send_library, + }; + use endpoint_v2::store; + use endpoint_v2::timeout; + use endpoint_v2::timeout_test_helpers::{Self, set_block_height}; + use uln_302::configuration_tests::enable_receive_eid_for_test; + + const OAPP_ADDRESS: address = @0x1234; + + #[test_only] + /// Initialize the test environment + fun init_for_test() { + let native_framework = &create_signer_for_test(@std); + // start the clock + timestamp::set_time_has_started_for_testing(native_framework); + timeout_test_helpers::setup_for_timeouts(); + // initialize + endpoint_v2_common::universal_config::init_module_for_test(100); + store::init_module_for_test(); + // register some libraries + register_library(@blocked_msglib); + register_library(@simple_msglib); + register_library(@uln_302); + uln_302::msglib::initialize_for_test();// register an OApp + store::register_oapp(OAPP_ADDRESS, string::utf8(b"test")); + } + + // ================================================= Registration ================================================= + + #[test] + fun test_register_library() { + store::init_module_for_test(); + + // Register a new library + register_library(@blocked_msglib); + assert!(was_event_emitted(&library_registered_event(@blocked_msglib)), 2); + assert!(is_registered_library(@blocked_msglib), 0); + assert!(!is_registered_library(@simple_msglib), 1); + + // Register another library + register_library(@simple_msglib); + assert!(was_event_emitted(&library_registered_event(@simple_msglib)), 2); + assert!(is_registered_library(@blocked_msglib), 3); + assert!(is_registered_library(@simple_msglib), 4); + + let msglibs = get_registered_libraries(0, 10); + assert!(msglibs == vector[@blocked_msglib, @simple_msglib], 5); + + let msglibs = get_registered_libraries(0, 1); + assert!(msglibs == vector[@blocked_msglib], 6); + + let msglibs = get_registered_libraries(1, 5); + assert!(msglibs == vector[@simple_msglib], 7); + + let msglibs = get_registered_libraries(4, 5); + assert!(msglibs == vector[], 8); + + let msglibs = get_registered_libraries(1, 0); + assert!(msglibs == vector[], 8); + } + + #[test] + #[expected_failure(abort_code = router_node_1::router_node::ENOT_IMPLEMENTED)] + fun test_register_library_fails_if_unroutable() { + store::init_module_for_test(); + + // Register an unroutable library + register_library(@0x9874123); + } + + // ================================================ Send Libraries ================================================ + + #[test] + fun test_set_default_send_library() { + init_for_test(); + let dst_eid = 1; + + // Set the default send library + set_default_send_library(dst_eid, @blocked_msglib); + assert!(was_event_emitted(&default_send_library_set_event(dst_eid, @blocked_msglib)), 1); + assert!(store::get_default_send_library(dst_eid) == @blocked_msglib, 0); + + // Update the default send library + set_default_send_library(dst_eid, @simple_msglib); + assert!(was_event_emitted(&default_send_library_set_event(dst_eid, @simple_msglib)), 2); + assert!(store::get_default_send_library(dst_eid) == @simple_msglib, 1); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::msglib_manager::EATTEMPTED_TO_SET_CURRENT_LIBRARY)] + fun test_set_default_send_library_fails_if_setting_to_current_value() { + init_for_test(); + let dst_eid = 1; + + // Set the default send library twice to same value + set_default_send_library(dst_eid, @blocked_msglib); + set_default_send_library(dst_eid, @blocked_msglib); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::msglib_manager::EUNREGISTERED_MSGLIB)] + fun test_set_default_send_library_fails_if_library_not_registered() { + store::init_module_for_test(); + register_library(@blocked_msglib); + + // Set the default send library to an unregistered library + set_default_send_library(1, @simple_msglib); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::msglib_manager::EUNSUPPORTED_DST_EID)] + fun test_set_default_send_library_fails_if_library_does_not_support_eid() { + init_for_test(); + let dst_eid = 1; + // only enable receive side, not send side + uln_302::configuration_tests::enable_receive_eid_for_test(dst_eid); + + // Set the default send library to a library that does not support the EID + set_default_send_library(dst_eid, @uln_302); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::msglib_manager::EUNREGISTERED_MSGLIB)] + fun test_set_default_send_library_fails_if_attempting_to_unset() { + init_for_test(); + + // Attempt to unset + set_default_send_library(1, @0x0); + } + + #[test] + fun test_set_send_library() { + init_for_test(); + let dst_eid = 1; + uln_302::configuration_tests::enable_send_eid_for_test(dst_eid); + + // Set the default send library + set_default_send_library(dst_eid, @blocked_msglib); + assert!(was_event_emitted(&default_send_library_set_event(dst_eid, @blocked_msglib)), 3); + assert!(get_default_send_library(dst_eid) == @blocked_msglib, 0); + let (lib, is_default) = get_effective_send_library(OAPP_ADDRESS, dst_eid); + assert!(lib == @blocked_msglib, 1); + assert!(is_default, 2); + + // Set the OApp send library + set_send_library(OAPP_ADDRESS, dst_eid, @simple_msglib); + assert!(was_event_emitted(&send_library_set_event(OAPP_ADDRESS, dst_eid, @simple_msglib)), 4); + let (lib, is_default) = get_effective_send_library(OAPP_ADDRESS, dst_eid); + assert!(lib == @simple_msglib, 3); + assert!(!is_default, 4); + + // Update the OApp send library + set_send_library(OAPP_ADDRESS, dst_eid, @uln_302); + assert!(was_event_emitted(&send_library_set_event(OAPP_ADDRESS, dst_eid, @uln_302)), 7); + let (lib, is_default) = get_effective_send_library(OAPP_ADDRESS, dst_eid); + assert!(lib == @uln_302, 5); + assert!(!is_default, 6); + + // Unset the OApp send library + set_send_library(OAPP_ADDRESS, dst_eid, @0x0); + assert!(was_event_emitted(&send_library_set_event(OAPP_ADDRESS, dst_eid, @0x0)), 9); + let (lib, is_default) = get_effective_send_library(OAPP_ADDRESS, dst_eid); + assert!(lib == @blocked_msglib, 7); + assert!(is_default, 8); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::msglib_manager::EUNREGISTERED_MSGLIB)] + fun test_set_send_library_fails_if_library_not_registered() { + store::init_module_for_test(); + register_library(@blocked_msglib); + + // Set the OApp send library to an unregistered library + set_send_library(OAPP_ADDRESS, 1, @simple_msglib); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::msglib_manager::EATTEMPTED_TO_SET_CURRENT_LIBRARY)] + fun test_set_send_library_fails_if_setting_to_current_value() { + init_for_test(); + let dst_eid = 1; + set_default_send_library(dst_eid, @simple_msglib); + + // Set the OApp send library twice to same value + set_send_library(OAPP_ADDRESS, dst_eid, @simple_msglib); + set_send_library(OAPP_ADDRESS, dst_eid, @simple_msglib); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::msglib_manager::EUNSUPPORTED_DST_EID)] + fun test_set_send_library_fails_if_library_does_not_support_eid() { + init_for_test(); + let dst_eid = 1; + // only enable receive side, not send side + uln_302::configuration_tests::enable_receive_eid_for_test(dst_eid); + + // Set the OApp send library to a library that does not support the EID + set_send_library(OAPP_ADDRESS, dst_eid, @uln_302); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::msglib_manager::EOAPP_SEND_LIB_NOT_SET)] + fun test_set_send_library_fails_if_trying_to_unset_library_that_isnt_set() { + init_for_test(); + let dst_eid = 1; + + // Attempt to unset without having already set + set_send_library(OAPP_ADDRESS, dst_eid, @0x0); + } + + // Requires Manual Verification: + // * set_default_send_library_fails_if_attempting_to_set_to_receive_only_library + // * set_send_library_fails_if_attempting_to_set_to_receive_only_library + + // =============================================== Receive Libraries ============================================== + + #[test] + fun test_set_default_receive_library() { + init_for_test(); + let src_eid = 1; + + // Set the default receive library + set_default_receive_library(src_eid, @blocked_msglib, 0); + assert!(was_event_emitted(&default_receive_library_set_event(src_eid, @blocked_msglib)), 0); + assert!(get_default_receive_library(src_eid) == @blocked_msglib, 0); + assert!(matches_default_receive_library(src_eid, @blocked_msglib), 0); + + // Update the default receive library (no timeout) + set_default_receive_library(src_eid, @simple_msglib, 0); + assert!(was_event_emitted(&default_receive_library_set_event(src_eid, @simple_msglib)), 1); + assert!(get_default_receive_library(src_eid) == @simple_msglib, 1); + assert!(matches_default_receive_library(src_eid, @simple_msglib), 0); + assert!(!matches_default_receive_library(src_eid, @blocked_msglib), 0); + + // Update the default receive library (with timeout) + enable_receive_eid_for_test(src_eid); + set_default_receive_library(src_eid, @uln_302, 10); + assert!(was_event_emitted(&default_receive_library_set_event(src_eid, @uln_302)), 2); + assert!(was_event_emitted(&default_receive_library_timeout_set_event(src_eid, @simple_msglib, 10)), 3); + assert!(get_default_receive_library(src_eid) == @uln_302, 2); + // matches both + assert!(matches_default_receive_library(src_eid, @simple_msglib), 1); + assert!(matches_default_receive_library(src_eid, @uln_302), 1); + + // Update the default receive library (no timeout) + set_default_receive_library(src_eid, @blocked_msglib, 0); + assert!(was_event_emitted(&default_receive_library_set_event(src_eid, @blocked_msglib)), 4); + assert!(was_event_emitted(&default_receive_library_timeout_set_event(src_eid, @0x0, 0)), 5); + assert!(get_default_receive_library(src_eid) == @blocked_msglib, 3); + // does not preserve old timemout library or prior library + assert!(matches_default_receive_library(src_eid, @blocked_msglib), 2); + assert!(!matches_default_receive_library(src_eid, @simple_msglib), 2); + assert!(!matches_default_receive_library(src_eid, @uln_302), 2); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::msglib_manager::EUNREGISTERED_MSGLIB)] + fun test_set_default_receive_libraries_fails_if_not_registered() { + store::init_module_for_test(); + register_library(@blocked_msglib); + + // Set the default receive library to an unregistered library + set_default_receive_library(1, @simple_msglib, 0); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::msglib_manager::EATTEMPTED_TO_SET_CURRENT_LIBRARY)] + fun test_set_default_receive_libraries_fails_if_setting_to_current_value() { + init_for_test(); + let src_eid = 1; + + // Set the default receive library twice to same value + set_default_receive_library(src_eid, @simple_msglib, 0); + set_default_receive_library(src_eid, @simple_msglib, 0); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::msglib_manager::EUNSUPPORTED_SRC_EID)] + fun test_set_default_receive_libraries_fails_if_library_does_not_support_eid() { + init_for_test(); + let src_eid = 1; + // only enable send side, not receive side + uln_302::configuration_tests::enable_send_eid_for_test(src_eid); + + // Set the default receive library to a library that does not support the EID + set_default_receive_library(src_eid, @uln_302, 0); + } + + #[test] + fun test_set_receive_library() { + init_for_test(); + + // Set the OApp receive library + set_default_receive_library(1, @blocked_msglib, 0); + let (lib, is_default) = get_effective_receive_library(OAPP_ADDRESS, 1); + assert!(lib == @blocked_msglib, 0); + assert!(is_default, 0); + + set_receive_library(OAPP_ADDRESS, 1, @simple_msglib, 0); + assert!(was_event_emitted(&receive_library_set_event(OAPP_ADDRESS, 1, @simple_msglib)), 0); + assert!(was_event_emitted(&receive_library_timeout_set_event(OAPP_ADDRESS, 1, @0x0, 0)), 4); + let (lib, is_default) = get_effective_receive_library(OAPP_ADDRESS, 1); + assert!(lib == @simple_msglib, 0); + assert!(!is_default, 0); + + // Use a timeout + set_receive_library(OAPP_ADDRESS, 1, @blocked_msglib, 10); + assert!(was_event_emitted(&receive_library_set_event(OAPP_ADDRESS, 1, @blocked_msglib)), 1); + assert!(was_event_emitted(&receive_library_timeout_set_event(OAPP_ADDRESS, 1, @simple_msglib, 10)), 2); + let (lib, is_default) = get_effective_receive_library(OAPP_ADDRESS, 1); + assert!(lib == @blocked_msglib, 0); + assert!(!is_default, 0); + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @simple_msglib), 0); + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @blocked_msglib), 0); + + // Setting without a timeout removes the old timeout + uln_302::configuration_tests::enable_receive_eid_for_test(1); + set_receive_library(OAPP_ADDRESS, 1, @uln_302, 0); + assert!(was_event_emitted(&receive_library_set_event(OAPP_ADDRESS, 1, @uln_302)), 3); + assert!(was_event_emitted(&receive_library_timeout_set_event(OAPP_ADDRESS, 1, @blocked_msglib, 0)), 4); + let (lib, is_default) = get_effective_receive_library(OAPP_ADDRESS, 1); + assert!(lib == @uln_302, 0); + assert!(!is_default, 0); + assert!(!is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @simple_msglib), 0); + assert!(!is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @blocked_msglib), 0); + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @uln_302), 0); + + // Setting with timeout + set_receive_library(OAPP_ADDRESS, 1, @simple_msglib, 10); + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @uln_302), 0); + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @simple_msglib), 0); + + // Unsetting removes the timeout also + set_receive_library(OAPP_ADDRESS, 1, @0x0, 0); + assert!(was_event_emitted(&receive_library_set_event(OAPP_ADDRESS, 1, @0x0)), 5); + assert!(was_event_emitted(&receive_library_timeout_set_event(OAPP_ADDRESS, 1, @simple_msglib, 0)), 6); + assert!(!is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @uln_302), 0); + assert!(!is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @simple_msglib), 0); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::msglib_manager::EUNREGISTERED_MSGLIB)] + fun test_set_receive_library_fails_if_not_registered() { + store::init_module_for_test(); + register_library(@blocked_msglib); + + // Set the OApp receive library to an unregistered library + set_receive_library(OAPP_ADDRESS, 1, @simple_msglib, 0); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::msglib_manager::EATTEMPTED_TO_SET_CURRENT_LIBRARY)] + fun test_set_receive_library_fails_if_setting_to_current_value() { + init_for_test(); + let src_eid = 1; + set_default_receive_library(src_eid, @simple_msglib, 0); + + // Set the OApp receive library twice to same value + set_receive_library(OAPP_ADDRESS, src_eid, @simple_msglib, 0); + set_receive_library(OAPP_ADDRESS, src_eid, @simple_msglib, 0); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::msglib_manager::EUNSUPPORTED_SRC_EID)] + fun test_set_receive_library_fails_if_library_does_not_support_eid() { + init_for_test(); + let src_eid = 1; + // only enable send side, not receive side + uln_302::configuration_tests::enable_send_eid_for_test(src_eid); + + // Set the OApp receive library to a library that does not support the EID + // Note this would fail in either case, because the default is not set (and could not be set, because it would + // fail for the same reason) + set_receive_library(OAPP_ADDRESS, src_eid, @uln_302, 0); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::msglib_manager::ERECEIVE_LIB_NOT_SET)] + fun test_set_receive_library_fails_if_trying_to_unset_library_that_is_not_set() { + init_for_test(); + let src_eid = 1; + set_default_receive_library(src_eid, @simple_msglib, 0); + + // Attempt to unset without having already set + set_receive_library(OAPP_ADDRESS, src_eid, @0x0, 0); + } + + #[test] + #[expected_failure( + abort_code = endpoint_v2::msglib_manager::ECANNOT_SET_GRACE_PERIOD_ON_RECEIVE_LIBRARY_UNSET + )] + fun test_set_receive_library_aborts_if_provided_a_grace_period_time_for_unset() { + init_for_test(); + let src_eid = 1; + set_default_receive_library(src_eid, @simple_msglib, 0); + + set_receive_library(OAPP_ADDRESS, src_eid, @simple_msglib, 0); + + // Attempt to unset with a grace period + set_receive_library(OAPP_ADDRESS, src_eid, @0x0, 10); + } + + #[test] + fun test_matches_default_receive_library() { + init_for_test(); + + let src_eid = 2; + uln_302::configuration_tests::enable_receive_eid_for_test(src_eid); + set_default_receive_library(src_eid, @simple_msglib, 0); + assert!(was_event_emitted(&default_receive_library_set_event(src_eid, @simple_msglib)), 0); + + // No OApp specific config set + assert!(matches_default_receive_library(src_eid, @simple_msglib), 0); + + // Update default without grace period + set_default_receive_library(src_eid, @uln_302, 0); + assert!(was_event_emitted(&default_receive_library_set_event(src_eid, @uln_302)), 1); + assert!(!matches_default_receive_library(src_eid, @simple_msglib), 1); + assert!(matches_default_receive_library(src_eid, @uln_302), 2); + + // Update default with grace period + set_default_receive_library(src_eid, @blocked_msglib, 10); + assert!(was_event_emitted(&default_receive_library_set_event(src_eid, @blocked_msglib)), 2); + assert!(was_event_emitted(&default_receive_library_timeout_set_event(src_eid, @uln_302, 10)), 3); + set_block_height(9); + assert!(matches_default_receive_library(src_eid, @uln_302), 3); + assert!(matches_default_receive_library(src_eid, @blocked_msglib), 4); + + // Grace period expired + set_block_height(11); + assert!(!matches_default_receive_library(src_eid, @uln_302), 5); + assert!(matches_default_receive_library(src_eid, @blocked_msglib), 6); + } + + #[test] + fun test_is_valid_receive_library() { + init_for_test(); + uln_302::configuration_tests::enable_receive_eid_for_test(1); + + set_default_receive_library(1, @blocked_msglib, 0); + assert!(was_event_emitted(&default_receive_library_set_event(1, @blocked_msglib)), 0); + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @blocked_msglib), 0); + + // Set a new library for the OApp - if there is no grace period, the other is immediately invalid + set_receive_library(OAPP_ADDRESS, 1, @simple_msglib, 0); + assert!(was_event_emitted(&receive_library_set_event(OAPP_ADDRESS, 1, @simple_msglib)), 1); + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @simple_msglib), 0); + assert!(!is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @blocked_msglib), 0); + + // Updating with a grace period, leaves the prior one valid up to the grace period end time + set_receive_library(OAPP_ADDRESS, 1, @uln_302, 10); + assert!(was_event_emitted(&receive_library_set_event(OAPP_ADDRESS, 1, @uln_302)), 2); + assert!(was_event_emitted(&receive_library_timeout_set_event(OAPP_ADDRESS, 1, @simple_msglib, 10)), 3); + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @uln_302), 0); + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @simple_msglib), 0); + set_block_height(9); + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @uln_302), 0); + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @simple_msglib), 0); + + // After the grace period, only the new library is valid + set_block_height(10); + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @uln_302), 0); + assert!(!is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @simple_msglib), 0); + + // Unsetting for oapp + set_receive_library(OAPP_ADDRESS, 1, @0x0, 0); + assert!(was_event_emitted(&receive_library_set_event(OAPP_ADDRESS, 1, @0x0)), 4); + assert!(!is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @uln_302), 0); // prior immediately invalid + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @blocked_msglib), 0); // default also valid + + // Set again + set_receive_library(OAPP_ADDRESS, 1, @uln_302, 0); + assert!(was_event_emitted(&receive_library_set_event(OAPP_ADDRESS, 1, @uln_302)), 5); + assert!(!is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @blocked_msglib), 0); + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @uln_302), 0); + + // Unset again + set_receive_library(OAPP_ADDRESS, 1, @0x0, 0); + assert!(was_event_emitted(&receive_library_set_event(OAPP_ADDRESS, 1, @0x0)), 6); + assert!(!is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @uln_302), 0); // prior not valid + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, 1, @blocked_msglib), 0); // default valid + } + + #[test] + fun test_set_default_receive_library_timeout() { + init_for_test(); + + let src_eid = 2; + uln_302::configuration_tests::enable_receive_eid_for_test(src_eid); + + // t = 0 blocks + set_default_receive_library(src_eid, @blocked_msglib, 0); + assert!(was_event_emitted(&default_receive_library_set_event(src_eid, @blocked_msglib)), 0); + assert!(was_event_emitted(&default_receive_library_timeout_set_event(src_eid, @0x0, 0)), 3); + + // Set the grace period to 10 blocks + set_default_receive_library(src_eid, @simple_msglib, 10); + assert!(was_event_emitted(&default_receive_library_set_event(src_eid, @simple_msglib)), 1); + + // Grace period should be active + assert!(matches_default_receive_library(src_eid, @blocked_msglib), 0); + assert!(matches_default_receive_library(src_eid, @simple_msglib), 1); + let grace_period_config = store::get_default_receive_library_timeout(src_eid); + assert!(timeout::is_active(&grace_period_config), 2); + + // Reset the expiry to 20 blocks + set_default_receive_library_timeout(src_eid, @blocked_msglib, 20); + assert!(was_event_emitted(&default_receive_library_timeout_set_event(src_eid, @blocked_msglib, 20)), 3); + assert!(matches_default_receive_library(src_eid, @blocked_msglib), 2); + assert!(matches_default_receive_library(src_eid, @simple_msglib), 3); + grace_period_config = store::get_default_receive_library_timeout(src_eid); + assert!(timeout::is_active(&grace_period_config), 2); + + // t = 19 blocks: grace period should still be active + set_block_height(19); + assert!(matches_default_receive_library(src_eid, @blocked_msglib), 4); + assert!(matches_default_receive_library(src_eid, @simple_msglib), 5); + let grace_period_config = store::get_default_receive_library_timeout(src_eid); + assert!(timeout::is_active(&grace_period_config), 2); + + // t = 20 blocks (at the expiry): grace period should be expired + set_block_height(21); + assert!(!matches_default_receive_library(src_eid, @blocked_msglib), 6); + assert!(matches_default_receive_library(src_eid, @simple_msglib), 7); + let grace_period_config = store::get_default_receive_library_timeout(src_eid); + assert!(!timeout::is_active(&grace_period_config), 8); + + // Adding timeout at any point can revive the grace period and the fallback can be updated to any library + set_block_height(100); + set_default_receive_library_timeout(src_eid, @uln_302, 101); + assert!(was_event_emitted(&default_receive_library_timeout_set_event(src_eid, @uln_302, 101)), 9); + assert!(matches_default_receive_library(src_eid, @uln_302), 9); + assert!(matches_default_receive_library(src_eid, @simple_msglib), 10); + let grace_period_config = store::get_default_receive_library_timeout(src_eid); + assert!(timeout::is_active(&grace_period_config), 11); + + // After the timeout ends, the fallback should be invlid + set_block_height(1002); + assert!(!matches_default_receive_library(src_eid, @uln_302), 12); + assert!(matches_default_receive_library(src_eid, @simple_msglib), 13); + let grace_period_config = store::get_default_receive_library_timeout(src_eid); + assert!(!timeout::is_active(&grace_period_config), 14); + + // Clear the grace period config + // This should not fail despite the lib being unknown + set_default_receive_library_timeout(src_eid, @0x9999, 0); + assert!(was_event_emitted(&default_receive_library_timeout_set_event(src_eid, @0x9999, 0)), 15); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::msglib_manager::EEXPIRY_IS_IN_PAST)] + fun test_set_default_receive_library_timeout_fails_if_in_the_past() { + init_for_test(); + let src_eid = 2; + uln_302::configuration_tests::enable_receive_eid_for_test(src_eid); + set_default_receive_library(src_eid, @simple_msglib, 0); + + set_block_height(20); + + // Attempt to set a grace period that is in the past + set_default_receive_library_timeout(src_eid, @simple_msglib, 10); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::msglib_manager::EUNSUPPORTED_SRC_EID)] + fun test_set_default_receive_library_timeout_fails_if_msglib_doesnt_support_eid() { + init_for_test(); + let src_eid = 2; + // only enable send side, not receive side + uln_302::configuration_tests::enable_send_eid_for_test(src_eid); + + // Set the grace period to 10 blocks + set_default_receive_library(src_eid, @uln_302, 10); + + // Reset the expiry to 20 blocks + set_default_receive_library_timeout(src_eid, @uln_302, 20); + } + + #[test] + fun test_set_receive_library_timeout() { + init_for_test(); + + let src_eid = 2; + uln_302::configuration_tests::enable_receive_eid_for_test(src_eid); + + set_default_receive_library(src_eid, @simple_msglib, 0); + + // Both default and receive should be valid after setting the oapp lib with a timeout + set_receive_library(OAPP_ADDRESS, src_eid, @uln_302, 0); + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, src_eid, @uln_302), 0); + assert!(!is_valid_receive_library_for_oapp(OAPP_ADDRESS, src_eid, @simple_msglib), 1); + // unrelated msglib + assert!(!is_valid_receive_library_for_oapp(OAPP_ADDRESS, src_eid, @blocked_msglib), 2); + + // Reset the grace period to 20 blocks from current block (0) and to a different lib + set_receive_library_timeout(OAPP_ADDRESS, src_eid, @blocked_msglib, 20); + assert!(was_event_emitted(&receive_library_timeout_set_event(OAPP_ADDRESS, src_eid, @blocked_msglib, 20)), 3); + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, src_eid, @blocked_msglib), 2); + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, src_eid, @uln_302), 3); + // old grace period msglib + assert!(!is_valid_receive_library_for_oapp(OAPP_ADDRESS, src_eid, @simple_msglib), 4); + + // t = 19 blocks (before the expiry): grace period should still be active + set_block_height(19); + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, src_eid, @blocked_msglib), 5); + + // t = 20 blocks (at the expiry): grace period should be expired + set_block_height(20); + assert!(!is_valid_receive_library_for_oapp(OAPP_ADDRESS, src_eid, @blocked_msglib), 6); + + // set a new timout + set_receive_library_timeout(OAPP_ADDRESS, src_eid, @simple_msglib, 30); + assert!(was_event_emitted(&receive_library_timeout_set_event(OAPP_ADDRESS, src_eid, @simple_msglib, 30)), 7); + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, src_eid, @simple_msglib), 8); + + // unset the timeout + set_receive_library_timeout(OAPP_ADDRESS, src_eid, @1234567, 0); + assert!(was_event_emitted(&receive_library_timeout_set_event(OAPP_ADDRESS, src_eid, @0x0, 0)), 8); + assert!(!is_valid_receive_library_for_oapp(OAPP_ADDRESS, src_eid, @simple_msglib), 8); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::msglib_manager::EEXPIRY_IS_IN_PAST)] + fun test_set_receive_library_timeout_fails_if_in_the_past() { + init_for_test(); + let src_eid = 2; + uln_302::configuration_tests::enable_receive_eid_for_test(src_eid); + + set_default_receive_library(src_eid, @simple_msglib, 0); + set_receive_library(OAPP_ADDRESS, src_eid, @simple_msglib, 0); + set_block_height(20); + + // Attempt to set a grace period that is in the past + set_receive_library_timeout(OAPP_ADDRESS, src_eid, @simple_msglib, 10); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::msglib_manager::ERECEIVE_LIB_NOT_SET)] + fun test_set_receive_library_timeout_fails_if_trying_to_unset_library_that_is_not_set() { + init_for_test(); + let src_eid = 2; + set_default_receive_library(src_eid, @simple_msglib, 0); + + // Attempt to unset without having already set + set_receive_library_timeout(OAPP_ADDRESS, src_eid, @0x0, 10); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::msglib_manager::ENO_TIMEOUT_TO_DELETE)] + fun test_set_receive_library_timeout_fails_if_no_grace_period_config_to_delete() { + init_for_test(); + let src_eid = 2; + + set_default_receive_library(src_eid, @simple_msglib, 0); + set_receive_library(OAPP_ADDRESS, src_eid, @blocked_msglib, 0); + // Attempt to unset without having already set + set_receive_library_timeout(OAPP_ADDRESS, src_eid, @0x0, 0); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::msglib_manager::ERECEIVE_LIB_NOT_SET)] + fun test_set_receive_library_timeout_fails_if_no_oapp_receive_library_already_set() { + init_for_test(); + let src_eid = 2; + set_default_receive_library(src_eid, @simple_msglib, 0); + + // Attempt to set a grace period without having already set + set_receive_library_timeout(OAPP_ADDRESS, src_eid, @simple_msglib, 10); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::msglib_manager::EUNSUPPORTED_SRC_EID)] + fun test_set_receive_library_timeout_fails_if_msglib_doesnt_support_eid() { + init_for_test(); + let src_eid = 2; + // only enable send side, not receive side + uln_302::configuration_tests::enable_send_eid_for_test(src_eid); + + set_default_receive_library(src_eid, @simple_msglib, 0); + set_receive_library(OAPP_ADDRESS, src_eid, @simple_msglib, 0); + + // Attempt to set a grace period to a library that does not support the EID + set_receive_library_timeout(OAPP_ADDRESS, src_eid, @uln_302, 10); + } + + #[test] + fun test_get_effective_receive_library() { + init_for_test(); + + let src_eid = 2; + uln_302::configuration_tests::enable_receive_eid_for_test(src_eid); + + set_default_receive_library(src_eid, @simple_msglib, 0); + let (lib, is_default) = get_effective_receive_library(OAPP_ADDRESS, src_eid); + assert!(lib == @simple_msglib, 0); + assert!(is_default, 1); + + // set for oapp + set_receive_library( + OAPP_ADDRESS, + src_eid, + @uln_302, + 0, + ); + + let (lib, is_default) = get_effective_receive_library(OAPP_ADDRESS, src_eid); + assert!(lib == @uln_302, 2); + assert!(!is_default, 3); + + // remove the oapp setting + set_receive_library( + OAPP_ADDRESS, + src_eid, + @0x0, + 0, + ); + + let (lib, is_default) = get_effective_receive_library(OAPP_ADDRESS, src_eid); + assert!(lib == @simple_msglib, 4); + assert!(is_default, 5); + } + + #[test] + fun test_is_valid_receive_library_for_oapp() { + init_for_test(); + + let src_eid = 2; + uln_302::configuration_tests::enable_receive_eid_for_test(src_eid); + + // An unregistered library is not ever valid + assert!(!is_valid_receive_library_for_oapp(OAPP_ADDRESS, src_eid, @0x983721498743), 0); + + // The default is valid if one is not set for OApp + set_default_receive_library(src_eid, @simple_msglib, 0); + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, src_eid, @simple_msglib), 0); + assert!(!is_valid_receive_library_for_oapp(OAPP_ADDRESS, src_eid, @uln_302), 1); + + // set for oapp + set_receive_library( + OAPP_ADDRESS, + src_eid, + @uln_302, + 0, + ); + + // Only the oapp setting is valid + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, src_eid, @uln_302), 3); + assert!(!is_valid_receive_library_for_oapp(OAPP_ADDRESS, src_eid, @simple_msglib), 4); + + // Switch with a grace period + set_receive_library( + OAPP_ADDRESS, + src_eid, + @simple_msglib, + 10, + ); + + // Both are valid during grace period + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, src_eid, @simple_msglib), 5); + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, src_eid, @uln_302), 6); + + // After the grace period, only the new one is valid + set_block_height(11); + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, src_eid, @simple_msglib), 7); + assert!(!is_valid_receive_library_for_oapp(OAPP_ADDRESS, src_eid, @uln_302), 8); + + // remove the oapp setting + set_receive_library( + OAPP_ADDRESS, + src_eid, + @0x0, + 0, + ); + + // After unsetting, only the default is valid + assert!(is_valid_receive_library_for_oapp(OAPP_ADDRESS, src_eid, @simple_msglib), 3); + assert!(!is_valid_receive_library_for_oapp(OAPP_ADDRESS, src_eid, @uln_302), 4); + } +} \ No newline at end of file diff --git a/packages/layerzero-v2/initia/contracts/endpoint_v2/tests/internal/registration_tests.move b/packages/layerzero-v2/initia/contracts/endpoint_v2/tests/internal/registration_tests.move new file mode 100644 index 00000000..d3add2a2 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/endpoint_v2/tests/internal/registration_tests.move @@ -0,0 +1,81 @@ +#[test_only] +module endpoint_v2::registration_tests { + use std::event::was_event_emitted; + use std::string::utf8; + + use endpoint_v2::registration::{ + composer_registered_event, + is_registered_composer, + is_registered_oapp, + oapp_registered_event, + register_composer, + register_oapp + }; + use endpoint_v2::store; + + #[test] + fun test_register() { + store::init_module_for_test(); + register_oapp(@1234, utf8(b"test_receive")); + assert!(was_event_emitted(&oapp_registered_event(@1234, utf8(b"test_receive"))), 0); + assert!(is_registered_oapp(@1234), 1); + assert!(!is_registered_composer(@1234), 2); + let receiver_module = store::lz_receive_module(@1234); + assert!(receiver_module == utf8(b"test_receive"), 0); + + register_composer(@2234, utf8(b"test_compose")); + assert!(was_event_emitted(&composer_registered_event(@2234, utf8(b"test_compose"))), 1); + assert!(!is_registered_oapp(@2234), 2); + assert!(is_registered_composer(@2234), 3); + let composer_module = store::lz_compose_module(@2234); + assert!(composer_module == utf8(b"test_compose"), 1); + + // register oapp in same address as composer + register_oapp(@2234, utf8(b"test_receive")); + assert!(was_event_emitted(&oapp_registered_event(@2234, utf8(b"test_receive"))), 2); + assert!(is_registered_oapp(@2234), 3); + assert!(is_registered_composer(@2234), 4); + let receiver_module = store::lz_receive_module(@2234); + assert!(receiver_module == utf8(b"test_receive"), 2); + + // register composer in same address as oapp + register_composer(@1234, utf8(b"test_compose")); + assert!(was_event_emitted(&composer_registered_event(@1234, utf8(b"test_compose"))), 3); + assert!(is_registered_oapp(@1234), 4); + assert!(is_registered_composer(@1234), 5); + let composer_module = store::lz_compose_module(@1234); + assert!(composer_module == utf8(b"test_compose"), 3); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::registration::EALREADY_REGISTERED)] + fun test_register_oapp_fails_if_already_registered() { + store::init_module_for_test(); + register_oapp(@1234, utf8(b"test")); + register_oapp(@1234, utf8(b"test")); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::registration::EALREADY_REGISTERED)] + fun test_register_composer_fails_if_already_registered() { + store::init_module_for_test(); + register_oapp(@1234, utf8(b"test")); + register_composer(@1234, utf8(b"test")); + register_composer(@1234, utf8(b"test")); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::store::EEMPTY_MODULE_NAME)] + fun test_register_fails_if_empty_lz_receive_module() { + endpoint_v2::store::init_module_for_test(); + register_oapp(@1234, utf8(b"")); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2::store::EEMPTY_MODULE_NAME)] + fun test_register_fails_if_empty_lz_compose_module() { + endpoint_v2::store::init_module_for_test(); + register_oapp(@1234, utf8(b"receive")); + register_composer(@1234, utf8(b"")); + } +} diff --git a/packages/layerzero-v2/initia/contracts/endpoint_v2/tests/internal/timeout_test_helpers.move b/packages/layerzero-v2/initia/contracts/endpoint_v2/tests/internal/timeout_test_helpers.move new file mode 100644 index 00000000..5bbdf535 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/endpoint_v2/tests/internal/timeout_test_helpers.move @@ -0,0 +1,22 @@ +#[test_only] +module endpoint_v2::timeout_test_helpers { + #[test_only] + public fun setup_for_timeouts() { + let std = &std::account::create_account_for_test(@std); + std::block::initialize_for_test(std, 1_000_000); + std::reconfiguration::initialize_for_test(std); + let vm = &std::account::create_account_for_test(@0x0); + // genesis block + std::block::emit_writeset_block_event(vm, @1234); + } + + + #[test_only] + public fun set_block_height(target_height: u64) { + let vm = &std::account::create_signer_for_test(@0x0); + let start_height = std::block::get_current_block_height(); + for (i in start_height..target_height) { + std::block::emit_writeset_block_event(vm, @1234); + } + } +} diff --git a/packages/layerzero-v2/initia/contracts/endpoint_v2/tests/test_helpers.move b/packages/layerzero-v2/initia/contracts/endpoint_v2/tests/test_helpers.move new file mode 100644 index 00000000..7569265e --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/endpoint_v2/tests/test_helpers.move @@ -0,0 +1,143 @@ +#[test_only] +module endpoint_v2::test_helpers { + use std::account::create_signer_for_test; + use std::signer::address_of; + use std::timestamp; + + use endpoint_v2::admin; + use endpoint_v2_common::contract_identity::make_call_ref_for_test; + use endpoint_v2_common::native_token_test_helpers::initialize_native_token_for_test; + use msglib_types::worker_options::EXECUTOR_WORKER_ID; + use price_feed_module_0::price; + use price_feed_module_0::price::tag_price_with_eid; + use treasury::treasury; + use worker_common::worker_config; + + public fun setup_layerzero_for_test( + msglib_addr: address, // should be the address of whatever msglib we are trying to test + local_eid: u32, + remote_eid: u32, + ) { + let native_framework = &create_signer_for_test(@std); + let layerzero_admin = &create_signer_for_test(@layerzero_admin); + + // set global time + timestamp::set_time_has_started_for_testing(native_framework); + + // init + endpoint_v2_common::universal_config::init_module_for_test(local_eid); + admin::initialize_endpoint_for_test(); + simple_msglib::msglib::initialize_for_test(); + + // config/wire + endpoint_v2::msglib_manager::register_library(msglib_addr); + + // defaults + endpoint_v2::admin::set_default_send_library( + layerzero_admin, + remote_eid, + msglib_addr, + ); + endpoint_v2::admin::set_default_receive_library( + layerzero_admin, + remote_eid, + msglib_addr, + 0, + ); + } + + public fun setup_layerzero_for_test_uln(local_eid: u32, remote_eid: u32) { + initialize_native_token_for_test(); + let native_framework = &create_signer_for_test(@std); + let layerzero_admin = &create_signer_for_test(@layerzero_admin); + + // set global time + timestamp::set_time_has_started_for_testing(native_framework); + + // init + endpoint_v2_common::universal_config::init_module_for_test(local_eid); + admin::initialize_endpoint_for_test(); + uln_302::msglib::initialize_for_test(); + + // config/wire + endpoint_v2::msglib_manager::register_library(@uln_302); + + // uln + uln_302::configuration_tests::enable_send_eid_for_test(remote_eid); + uln_302::configuration_tests::enable_receive_eid_for_test(local_eid); + + // defaults + endpoint_v2::admin::set_default_send_library( + layerzero_admin, + remote_eid, + @uln_302, + ); + endpoint_v2::admin::set_default_receive_library( + layerzero_admin, + local_eid, + @uln_302, + 0, + ); + + let executor = @3002; + uln_302::admin::set_default_executor_config( + layerzero_admin, + remote_eid, + 100000, + executor, + ); + + // Executor config + worker_config::initialize_for_worker_test_only( + executor, + EXECUTOR_WORKER_ID(), + executor, + @13002, + vector[executor], + vector[@uln_302], + @executor_fee_lib_0, + ); + worker_config::set_executor_dst_config( + &make_call_ref_for_test(executor), + remote_eid, + 1000, + 1000, + 1000, + 1000, + 1000, + ); + // opt into using worker config for feelib routing + uln_302::msglib::set_worker_config_for_fee_lib_routing_opt_in(&create_signer_for_test(executor), true); + let feed_address = @1234; + worker_config::set_price_feed( + &make_call_ref_for_test(executor), + @price_feed_module_0, + feed_address, + ); + + // Treasury + treasury::init_module_for_test(); + + // Price feed + let feed_updater = &create_signer_for_test(@555); + price_feed_module_0::feeds::initialize(&create_signer_for_test(feed_address)); + price_feed_module_0::feeds::enable_feed_updater( + &create_signer_for_test(feed_address), + address_of(feed_updater) + ); + price_feed_module_0::feeds::set_price( + feed_updater, + feed_address, + price::serialize_eid_tagged_price_list(&vector[ + tag_price_with_eid( + remote_eid, + price::new_price(1, 1, 1), + ), + tag_price_with_eid( + local_eid, + price::new_price(1, 1, 1), + ), + ]), + ) + } +} diff --git a/packages/layerzero-v2/initia/contracts/endpoint_v2_common/Move.toml b/packages/layerzero-v2/initia/contracts/endpoint_v2_common/Move.toml new file mode 100644 index 00000000..97edeab4 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/endpoint_v2_common/Move.toml @@ -0,0 +1,21 @@ +[package] +name = "endpoint_v2_common" +version = "1.0.0" + +[addresses] +endpoint_v2_common = "_" +endpoint_v2 = "_" +layerzero_admin = "_" +layerzero_treasury_admin = "_" +native_token_metadata_address = "0x8e4733bdabcf7d4afc3d14f0dd46c9bf52fb0fce9e4b996c939e195b8bc891d9" + +[dev-addresses] +endpoint_v2_common = "0x9098" +endpoint_v2 = "0x12345678" +layerzero_admin = "0x18943124" +layerzero_treasury_admin = "0x123456231423" + +[dependencies.InitiaStdlib] +git = "https://github.com/initia-labs/move-natives.git" +rev = "77d5f3e140143bdaa41f850115b3035c134193e3" +subdir = "initia_stdlib" diff --git a/packages/layerzero-v2/initia/contracts/endpoint_v2_common/sources/assert_no_duplicates.move b/packages/layerzero-v2/initia/contracts/endpoint_v2_common/sources/assert_no_duplicates.move new file mode 100644 index 00000000..81be6e71 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/endpoint_v2_common/sources/assert_no_duplicates.move @@ -0,0 +1,19 @@ +module endpoint_v2_common::assert_no_duplicates { + use std::vector; + + /// Assert that there are no duplicate addresses in the given vector. + public fun assert_no_duplicates(items: &vector) { + for (i in 0..vector::length(items)) { + for (j in 0..i) { + if (vector::borrow(items, i) == vector::borrow(items, j)) { + abort EDUPLICATE_ITEM + } + } + } + } + + + // ================================================== Error Codes ================================================= + + const EDUPLICATE_ITEM: u64 = 1; +} \ No newline at end of file diff --git a/packages/layerzero-v2/initia/contracts/endpoint_v2_common/sources/bytes32.move b/packages/layerzero-v2/initia/contracts/endpoint_v2_common/sources/bytes32.move new file mode 100644 index 00000000..fb328552 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/endpoint_v2_common/sources/bytes32.move @@ -0,0 +1,62 @@ +/// This is a wrapper for vector that enforces a length of 32 bytes +module endpoint_v2_common::bytes32 { + use std::bcs; + use std::from_bcs; + use std::vector; + + public inline fun ZEROS_32_BYTES(): vector { + x"0000000000000000000000000000000000000000000000000000000000000000" + } + + struct Bytes32 has store, drop, copy { + bytes: vector + } + + /// Returns a Bytes32 with all bytes set to zero + public fun zero_bytes32(): Bytes32 { + Bytes32 { bytes: ZEROS_32_BYTES() } + } + + /// Returns a Bytes32 with all bytes set to 0xff + public fun ff_bytes32(): Bytes32 { + Bytes32 { bytes: x"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" } + } + + /// Returns true if the given Bytes32 is all zeros + public fun is_zero(bytes32: &Bytes32): bool { + bytes32.bytes == ZEROS_32_BYTES() + } + + /// Converts a vector of bytes to a Bytes32 + /// The vector must be exactly 32 bytes long + public fun to_bytes32(bytes: vector): Bytes32 { + assert!(vector::length(&bytes) == 32, EINVALID_LENGTH); + Bytes32 { bytes } + } + + /// Converts a Bytes32 to a vector of bytes + public fun from_bytes32(bytes32: Bytes32): vector { + bytes32.bytes + } + + /// Converts an address to a Bytes32 + public fun from_address(addr: address): Bytes32 { + let bytes = bcs::to_bytes(&addr); + to_bytes32(bytes) + } + + /// Converts a Bytes32 to an address + public fun to_address(bytes32: Bytes32): address { + from_bcs::to_address(bytes32.bytes) + } + + /// Get the keccak256 hash of the given bytes + public fun keccak256(bytes: vector): Bytes32 { + let hash = std::aptos_hash::keccak256(bytes); + to_bytes32(hash) + } + + // ================================================== Error Codes ================================================= + + const EINVALID_LENGTH: u64 = 1; +} diff --git a/packages/layerzero-v2/initia/contracts/endpoint_v2_common/sources/contract_identity.move b/packages/layerzero-v2/initia/contracts/endpoint_v2_common/sources/contract_identity.move new file mode 100644 index 00000000..dbb0cdb9 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/endpoint_v2_common/sources/contract_identity.move @@ -0,0 +1,100 @@ +/// This module defines structs for contracts to authenticate themselves and prove authorization for specific actions +/// +/// The *ContractSigner* is stored by the contract after producing it in init_module(). It can only be generated one +/// time per address to defend against impersonation attacks. It is copiable however, so that it can be explicitly +/// shared if required. +/// +/// This can be used to generate a *CallRef* which is passed to the callee. There is a generic CallRef struct, +/// in which the `Target` should be substituted with a struct defined in the callee module that represents the +/// authorization type granted by the caller. +/// +/// The *DynamicCallRef* is used to pass the target contract address and an authorization byte-vector to the callee. +/// This is useful when the target contract address is not known at compile time. Upon receiving a DynamicCallRef, +/// the callee should use the `get_dynamic_call_ref_caller` function to identify the caller and verify the authorization +/// matches the expected authorization for the call. +/// +/// The Target and the (address, authorization) pair are useful to mitigate the risk of a call ref being used to perform +/// an action that was not intended by the caller. +module endpoint_v2_common::contract_identity { + use std::signer::address_of; + + struct SignerCreated has key {} + + /// Struct to persist the contract identity + /// Access to the contract signer provides universal access to the contract's authority and should be protected + struct ContractSigner has store, copy { contract_address: address } + + /// A Static call reference that can be used to identify the caller and statically validate the authorization + struct CallRef has drop { contract_address: address } + + /// A Dynamic call reference that can be used to identify the caller and validate the intended target contract + /// and authorization + struct DynamicCallRef has drop { contract_address: address, target_contract: address, authorization: vector } + + /// Creates a ContractSigner for the contract to store and use for generating ContractCallRefs + /// Make a record of the creation to prevent future contract signer creation + public fun create_contract_signer(account: &signer): ContractSigner { + assert!(!exists(address_of(account)), ECONTRACT_SIGNER_ALREADY_EXISTS); + move_to(account, SignerCreated {}); + ContractSigner { contract_address: address_of(account) } + } + + /// Destroys the contract signer - once destroyed the contract signer cannot be recreated using + /// `create_contract_signer`; however, any copies of the contract signer will continue to exist + public fun irrecoverably_destroy_contract_signer(contract_signer: ContractSigner) { + let ContractSigner { contract_address: _ } = contract_signer; + } + + /// Make a static call ref from a ContractSigner + /// Generally the target does not have to be specified as it can be inferred from the function signature it is + /// used with + public fun make_call_ref(contract: &ContractSigner): CallRef { + CallRef { contract_address: contract.contract_address } + } + + /// Get the calling contract address from a static CallRef + public fun get_call_ref_caller(call_ref: &CallRef): address { + call_ref.contract_address + } + + /// This function is used to create a ContractCallRef from a ContractSigner + public fun make_dynamic_call_ref( + contract: &ContractSigner, + target_contract: address, + authorization: vector, + ): DynamicCallRef { + DynamicCallRef { contract_address: contract.contract_address, target_contract, authorization } + } + + /// This function is used to get the calling contract address, while asserting that the recipient is the correct + /// receiver contract + public fun get_dynamic_call_ref_caller( + call_ref: &DynamicCallRef, + receiver_to_assert: address, + authorization_to_assert: vector, + ): address { + assert!(call_ref.target_contract == receiver_to_assert, ETARGET_CONTRACT_MISMATCH); + assert!(call_ref.authorization == authorization_to_assert, EAUTHORIZATION_MISMATCH); + call_ref.contract_address + } + + #[test_only] + public fun make_call_ref_for_test(contract_address: address): CallRef { + CallRef { contract_address } + } + + #[test_only] + public fun make_dynamic_call_ref_for_test( + contract_address: address, + target_contract: address, + authorization: vector, + ): DynamicCallRef { + DynamicCallRef { contract_address, target_contract, authorization } + } + + // ================================================== Error Codes ================================================= + + const EAUTHORIZATION_MISMATCH: u64 = 1; + const ECONTRACT_SIGNER_ALREADY_EXISTS: u64 = 2; + const ETARGET_CONTRACT_MISMATCH: u64 = 2; +} diff --git a/packages/layerzero-v2/initia/contracts/endpoint_v2_common/sources/guid.move b/packages/layerzero-v2/initia/contracts/endpoint_v2_common/sources/guid.move new file mode 100644 index 00000000..bffca9a3 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/endpoint_v2_common/sources/guid.move @@ -0,0 +1,15 @@ +/// This module provides the function to compute the GUID for a message +module endpoint_v2_common::guid { + use endpoint_v2_common::bytes32::{Self, Bytes32}; + use endpoint_v2_common::serde; + + public fun compute_guid(nonce: u64, src_eid: u32, sender: Bytes32, dst_eid: u32, receiver: Bytes32): Bytes32 { + let guid_bytes = vector[]; + serde::append_u64(&mut guid_bytes, nonce); + serde::append_u32(&mut guid_bytes, src_eid); + serde::append_bytes32(&mut guid_bytes, sender); + serde::append_u32(&mut guid_bytes, dst_eid); + serde::append_bytes32(&mut guid_bytes, receiver); + bytes32::keccak256(guid_bytes) + } +} diff --git a/packages/layerzero-v2/initia/contracts/endpoint_v2_common/sources/native_token.move b/packages/layerzero-v2/initia/contracts/endpoint_v2_common/sources/native_token.move new file mode 100644 index 00000000..acf44f04 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/endpoint_v2_common/sources/native_token.move @@ -0,0 +1,22 @@ +/// This module provides a function to withdraw the gas token from an account's balance using FungibleAsset methods. +/// +/// On some chains, it is necessary to withdraw the gas token using coin-based functions and have the balance converted +/// to FungibleAsset. For those chains, this module should be used. +/// +/// For chains that have a fully FungibleAsset-based, this conversion is not necessary. In these cases, this module can +/// be used in place of the native_token.move module. +module endpoint_v2_common::native_token { + use std::fungible_asset::{FungibleAsset, Metadata}; + use std::object; + use std::primary_fungible_store; + + public fun balance(account: address): u64 { + let metadata = object::address_to_object(@native_token_metadata_address); + primary_fungible_store::balance(account, metadata) + } + + public fun withdraw(account: &signer, amount: u64): FungibleAsset { + let metadata = object::address_to_object(@native_token_metadata_address); + primary_fungible_store::withdraw(move account, metadata, amount) + } +} diff --git a/packages/layerzero-v2/initia/contracts/endpoint_v2_common/sources/packet_raw.move b/packages/layerzero-v2/initia/contracts/endpoint_v2_common/sources/packet_raw.move new file mode 100644 index 00000000..4f5c00be --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/endpoint_v2_common/sources/packet_raw.move @@ -0,0 +1,36 @@ +/// This module defines a semantic wrapper for raw packet (and packet header) bytes +/// This format is agnostic to the codec used in the Message Library, but it is valuable to provide clarity and type +/// safety for Packets and headers in the codebase +module endpoint_v2_common::packet_raw { + use std::vector; + + struct RawPacket has drop, copy, store { + packet: vector, + } + + /// Create a vector from a RawPacket + public fun bytes_to_raw_packet(packet_bytes: vector): RawPacket { + RawPacket { packet: packet_bytes } + } + + /// Borrow the packet bytes from a RawPacket + public fun borrow_packet_bytes(raw_packet: &RawPacket): &vector { + &raw_packet.packet + } + + /// Borrow the packet bytes mutably from a RawPacket + public fun borrow_packet_bytes_mut(raw_packet: &mut RawPacket): &mut vector { + &mut raw_packet.packet + } + + /// Move the packet bytes from a RawPacket + public fun get_packet_bytes(raw_packet: RawPacket): vector { + let RawPacket { packet } = raw_packet; + packet + } + + /// Get the packet length of a RawPacket + public fun length(raw_packet: &RawPacket): u64 { + vector::length(&raw_packet.packet) + } +} diff --git a/packages/layerzero-v2/initia/contracts/endpoint_v2_common/sources/packet_v1_codec.move b/packages/layerzero-v2/initia/contracts/endpoint_v2_common/sources/packet_v1_codec.move new file mode 100644 index 00000000..24770ada --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/endpoint_v2_common/sources/packet_v1_codec.move @@ -0,0 +1,179 @@ +module endpoint_v2_common::packet_v1_codec { + use std::vector; + + use endpoint_v2_common::bytes32::{Self, Bytes32}; + use endpoint_v2_common::packet_raw::{Self, borrow_packet_bytes, bytes_to_raw_packet, RawPacket}; + use endpoint_v2_common::serde; + + const PACKET_VERSION: u8 = 1; + + // Header Offsets + const VERSION_OFFSET: u64 = 0; + const NONCE_OFFSET: u64 = 1; + const SRC_EID_OFFSET: u64 = 9; + const SENDER_OFFSET: u64 = 13; + const DST_EID_OFFSET: u64 = 45; + const RECEIVER_OFFSET: u64 = 49; + const HEADER_LENGTH: u64 = 81; + + // Message Offsets + const GUID_OFFSET: u64 = 81; + const MESSAGE_OFFSET: u64 = 113; + + /// Build a new packet with the given parameters + public fun new_packet_v1( + src_eid: u32, + sender: Bytes32, + dst_eid: u32, + receiver: Bytes32, + nonce: u64, + guid: Bytes32, + message: vector, + ): RawPacket { + let bytes = new_packet_v1_header_only_bytes(src_eid, sender, dst_eid, receiver, nonce); + serde::append_bytes32(&mut bytes, guid); + vector::append(&mut bytes, message); + bytes_to_raw_packet(bytes) + } + + /// Build a new packet (header only) with the given parameters + public fun new_packet_v1_header_only( + src_eid: u32, + sender: Bytes32, + dst_eid: u32, + receiver: Bytes32, + nonce: u64, + ): RawPacket { + bytes_to_raw_packet( + new_packet_v1_header_only_bytes(src_eid, sender, dst_eid, receiver, nonce) + ) + } + + /// Build a new packet (header only) with the given parameters and output byte form + public fun new_packet_v1_header_only_bytes( + src_eid: u32, + sender: Bytes32, + dst_eid: u32, + receiver: Bytes32, + nonce: u64, + ): vector { + let bytes = vector[]; + serde::append_u8(&mut bytes, PACKET_VERSION); + serde::append_u64(&mut bytes, nonce); + serde::append_u32(&mut bytes, src_eid); + serde::append_bytes32(&mut bytes, sender); + serde::append_u32(&mut bytes, dst_eid); + serde::append_bytes32(&mut bytes, receiver); + bytes + } + + /// Extract only the Packet header part from a RawPacket + public fun extract_header(raw_packet: &RawPacket): RawPacket { + if (packet_raw::length(raw_packet) > GUID_OFFSET) { + let packet_header_bytes = vector::slice(borrow_packet_bytes(raw_packet), 0, GUID_OFFSET); + bytes_to_raw_packet(packet_header_bytes) + } else { + *raw_packet + } + } + + /// Check that the packet is the expected version for this codec (version 1) + public fun is_valid_version(raw_packet: &RawPacket): bool { + get_version(raw_packet) == PACKET_VERSION + } + + /// Get the version of the packet + public fun get_version(raw_packet: &RawPacket): u8 { + let packet_bytes = packet_raw::borrow_packet_bytes(raw_packet); + serde::extract_u8(packet_bytes, &mut VERSION_OFFSET) + } + + /// Get the nonce of the packet + public fun get_nonce(raw_packet: &RawPacket): u64 { + let packet_bytes = packet_raw::borrow_packet_bytes(raw_packet); + serde::extract_u64(packet_bytes, &mut NONCE_OFFSET) + } + + /// Get the source EID of the packet + public fun get_src_eid(raw_packet: &RawPacket): u32 { + let packet_bytes = packet_raw::borrow_packet_bytes(raw_packet); + serde::extract_u32(packet_bytes, &mut SRC_EID_OFFSET) + } + + /// Get the sender of the packet + public fun get_sender(raw_packet: &RawPacket): Bytes32 { + let packet_bytes = packet_raw::borrow_packet_bytes(raw_packet); + serde::extract_bytes32(packet_bytes, &mut SENDER_OFFSET) + } + + /// Get the destination EID of the packet + public fun get_dst_eid(raw_packet: &RawPacket): u32 { + let packet_bytes = packet_raw::borrow_packet_bytes(raw_packet); + serde::extract_u32(packet_bytes, &mut DST_EID_OFFSET) + } + + /// Get the receiver of the packet + public fun get_receiver(raw_packet: &RawPacket): Bytes32 { + let packet_bytes = packet_raw::borrow_packet_bytes(raw_packet); + serde::extract_bytes32(packet_bytes, &mut RECEIVER_OFFSET) + } + + /// Get the GUID of the packet + public fun get_guid(raw_packet: &RawPacket): Bytes32 { + let packet_bytes = packet_raw::borrow_packet_bytes(raw_packet); + serde::extract_bytes32(packet_bytes, &mut GUID_OFFSET) + } + + /// Get the length of the message in the packet + public fun get_message_length(raw_packet: &RawPacket): u64 { + let packet_length = packet_raw::length(raw_packet); + packet_length - MESSAGE_OFFSET + } + + /// Get the message of the packet + public fun get_message(raw_packet: &RawPacket): vector { + let packet_bytes = packet_raw::borrow_packet_bytes(raw_packet); + serde::extract_bytes_until_end(packet_bytes, &mut MESSAGE_OFFSET) + } + + /// Get the payload of the packet + public fun get_payload_hash(packet: &RawPacket): Bytes32 { + let guid = get_guid(packet); + let message = get_message(packet); + compute_payload_hash(guid, message) + } + + /// Compute the payload of the packet + public fun compute_payload(guid: Bytes32, message: vector): vector { + let payload = vector[]; + vector::append(&mut payload, bytes32::from_bytes32(guid)); + vector::append(&mut payload, message); + payload + } + + /// Compute the payload hash of the packet + public fun compute_payload_hash(guid: Bytes32, message: vector): Bytes32 { + bytes32::keccak256(compute_payload(guid, message)) + } + + /// Assert that the packet is a valid packet for the local EID + public fun assert_receive_header(packet_header: &RawPacket, local_eid: u32) { + let packet_bytes = packet_raw::borrow_packet_bytes(packet_header); + assert!(vector::length(packet_bytes) == HEADER_LENGTH, EINVALID_PACKET_HEADER); + assert!(is_valid_version(packet_header), EINVALID_PACKET_VERSION); + assert!(get_dst_eid(packet_header) == local_eid, EINVALID_EID) + } + + public fun is_receive_header_valid(packet_header: &RawPacket, local_eid: u32): bool { + let packet_bytes = packet_raw::borrow_packet_bytes(packet_header); + vector::length(packet_bytes) == HEADER_LENGTH + && is_valid_version(packet_header) + && get_dst_eid(packet_header) == local_eid + } + + // ================================================== Error Codes ================================================= + + const EINVALID_EID: u64 = 1; + const EINVALID_PACKET_HEADER: u64 = 2; + const EINVALID_PACKET_VERSION: u64 = 3; +} \ No newline at end of file diff --git a/packages/layerzero-v2/initia/contracts/endpoint_v2_common/sources/send_packet.move b/packages/layerzero-v2/initia/contracts/endpoint_v2_common/sources/send_packet.move new file mode 100644 index 00000000..5756bc44 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/endpoint_v2_common/sources/send_packet.move @@ -0,0 +1,89 @@ +/// This module defines the internal send packet structure. This is the packet structure that is used internally by the +/// endpoint_v2 to communicate with the message libraries. Unlike the packet_v1_codec, which is may be upgraded for +/// use with a future message library, the internal packet is a fixed structure and is unchangable even with future +/// message libraries +module endpoint_v2_common::send_packet { + use std::vector; + + use endpoint_v2_common::bytes32::Bytes32; + use endpoint_v2_common::guid; + + struct SendPacket has copy, drop, store { + nonce: u64, + src_eid: u32, + sender: Bytes32, + dst_eid: u32, + receiver: Bytes32, + guid: Bytes32, + message: vector, + } + + /// Create a new send packet + public fun new_send_packet( + nonce: u64, + src_eid: u32, + sender: Bytes32, + dst_eid: u32, + receiver: Bytes32, + message: vector, + ): SendPacket { + let guid = guid::compute_guid(nonce, src_eid, sender, dst_eid, receiver); + SendPacket { + nonce, + src_eid, + sender, + dst_eid, + receiver, + guid, + message, + } + } + + /// Unpack the send packet into its components + public fun unpack_send_packet( + packet: SendPacket, + ): (u64, u32, Bytes32, u32, Bytes32, Bytes32, vector) { + let SendPacket { + nonce, + src_eid, + sender, + dst_eid, + receiver, + guid, + message, + } = packet; + (nonce, src_eid, sender, dst_eid, receiver, guid, message) + } + + public fun get_nonce(packet: &SendPacket): u64 { + packet.nonce + } + + public fun get_src_eid(packet: &SendPacket): u32 { + packet.src_eid + } + + public fun get_sender(packet: &SendPacket): Bytes32 { + packet.sender + } + + public fun get_dst_eid(packet: &SendPacket): u32 { + packet.dst_eid + } + + public fun get_receiver(packet: &SendPacket): Bytes32 { + packet.receiver + } + + public fun get_guid(packet: &SendPacket): Bytes32 { + packet.guid + } + + public fun borrow_message(packet: &SendPacket): &vector { + &packet.message + } + + public fun get_message_length(packet: &SendPacket): u64 { + vector::length(&packet.message) + } +} diff --git a/packages/layerzero-v2/initia/contracts/endpoint_v2_common/sources/serde.move b/packages/layerzero-v2/initia/contracts/endpoint_v2_common/sources/serde.move new file mode 100644 index 00000000..9aa73c4a --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/endpoint_v2_common/sources/serde.move @@ -0,0 +1,200 @@ +/// Serialization and deserialization utilities +module endpoint_v2_common::serde { + use std::bcs::to_bytes; + use std::from_bcs::to_address; + use std::vector; + + use endpoint_v2_common::bytes32::{Self, Bytes32}; + + /// Extract a uint from a vector of bytes starting at `position`, up to 8 bytes (u64). + /// Position will be incremented to the position after the end of the read + /// This decodes in big-endian format, with the most significant byte first + public fun extract_uint(input: &vector, position: &mut u64, bytes: u8): u64 { + let result: u64 = 0; + for (i in 0..bytes) { + let byte: u8 = *vector::borrow(input, *position); + result = result + ((byte as u64) << ((bytes - i - 1) * 8)); + *position = *position + 1; + }; + result + } + + /// Extract a u8 from a vector of bytes starting at `position` (position will be updated to the end of read) + public inline fun extract_u8(input: &vector, position: &mut u64): u8 { + (extract_uint(input, position, 1) as u8) + } + + /// Extract a u16 from a vector of bytes starting at `position` (position will be updated to the end of read) + public inline fun extract_u16(input: &vector, position: &mut u64): u16 { + (extract_uint(input, position, 2) as u16) + } + + /// Extract a u32 from a vector of bytes starting at `position` (position will be updated to the end of read) + public inline fun extract_u32(input: &vector, position: &mut u64): u32 { + (extract_uint(input, position, 4) as u32) + } + + /// Extract a u64 from a vector of bytes starting at `position` (position will be updated to the end of read) + public inline fun extract_u64(input: &vector, position: &mut u64): u64 { + extract_uint(input, position, 8) + } + + /// Extract a u128 from a vector of bytes starting at `position` (position will be updated to the end of read) + /// This function does not use extract_uint because it is more efficient to handle u128 as a special case + public fun extract_u128(input: &vector, position: &mut u64): u128 { + let result: u128 = 0; + for (i in 0..16) { + let byte: u8 = *vector::borrow(input, *position); + result = result + ((byte as u128) << ((16 - i - 1) * 8)); + *position = *position + 1; + }; + result + } + + /// Extract a u256 from a vector of bytes starting at `position` (position will be updated to the end of read) + /// This function does not use extract_uint because it is more efficient to handle u256 as a special case + public fun extract_u256(input: &vector, position: &mut u64): u256 { + let result: u256 = 0; + for (i in 0..32) { + let byte: u8 = *vector::borrow(input, *position); + result = result + ((byte as u256) << ((32 - i - 1) * 8)); + *position = *position + 1; + }; + result + } + + /// Extract a vector of bytes from a vector starting at `position` and ending at `position + len` + /// This will update the position to `position + len` + public fun extract_fixed_len_bytes(input: &vector, position: &mut u64, len: u64): vector { + let result = vector::slice(input, *position, *position + len); + *position = *position + len; + result + } + + /// Extract a vector of bytes from a vector starting at `position` and ending at the end of the vector + public fun extract_bytes_until_end(input: &vector, position: &mut u64): vector { + let len = vector::length(input); + let result = vector::slice(input, *position, len); + *position = len; + result + } + + /// Extract an address from a vector of bytes + public fun extract_address(input: &vector, position: &mut u64): address { + let bytes = vector::slice(input, *position, *position + 32); + *position = *position + 32; + to_address(bytes) + } + + /// Append a uint to a vector of bytes, up to 8 bytes (u64) + public fun append_uint(target: &mut vector, value: u64, bytes: u8) { + for (i in 0..bytes) { + let byte: u8 = (((value >> (8 * (bytes - i - 1))) & 0xFF) as u8); + vector::push_back(target, byte); + }; + } + + /// Append a u8 to a vector of bytes + public inline fun append_u8(target: &mut vector, value: u8) { append_uint(target, (value as u64), 1); } + + /// Append a u16 to a vector of bytes + public inline fun append_u16(target: &mut vector, value: u16) { append_uint(target, (value as u64), 2); } + + /// Append a u32 to a vector of bytes + public inline fun append_u32(target: &mut vector, value: u32) { append_uint(target, (value as u64), 4); } + + /// Append a u64 to a vector of bytes + public inline fun append_u64(target: &mut vector, value: u64) { append_uint(target, value, 8); } + + + /// Append a u128 to a vector of bytes + /// This function does not use append_uint because it is more efficient to handle u128 as a special case + public fun append_u128(target: &mut vector, value: u128) { + for (i in 0..16) { + let byte: u8 = (((value >> (8 * (16 - i - 1))) & 0xFF) as u8); + vector::push_back(target, byte); + } + } + + /// Append a u256 to a vector of bytes + /// This function does not use append_uint because it is more efficient to handle u256 as a special case + public fun append_u256(target: &mut vector, value: u256) { + for (i in 0..32) { + let byte: u8 = (((value >> (8 * (32 - i - 1))) & 0xFF) as u8); + vector::push_back(target, byte); + } + } + + /// Get the remaining length of the byte-vector starting at `position` + public fun get_remaining_length(input: &vector, position: u64): u64 { + vector::length(input) - position + } + + /// Pad the bytes provided with zeros to the left make it the target size + /// This will throw if the length of the provided vector surpasses the target size + public fun pad_zero_left(bytes: vector, target_size: u64): vector { + let bytes_size = vector::length(&bytes); + assert!(target_size >= bytes_size, EINVALID_LENGTH); + let output = vector[]; + let padding_needed = target_size - bytes_size; + for (i in 0..padding_needed) { + vector::push_back(&mut output, 0); + }; + vector::append(&mut output, bytes); + output + } + + /// Append a byte vector to the of a buffer + public inline fun append_bytes(buf: &mut vector, bytes: vector) { + vector::append(buf, bytes); + } + + /// Append an address to a vector of bytes + public fun append_address(buf: &mut vector, addr: address) { + let bytes = to_bytes(&addr); + vector::append(buf, bytes); + } + + /// Append a bytes32 to a vector of bytes + public fun append_bytes32(buf: &mut vector, bytes32: Bytes32) { + vector::append(buf, bytes32::from_bytes32(bytes32)); + } + + /// Extract a bytes32 from a vector of bytes + public fun extract_bytes32(input: &vector, position: &mut u64): Bytes32 { + let bytes = vector::slice(input, *position, *position + 32); + *position = *position + 32; + bytes32::to_bytes32(bytes) + } + + /// This function flattens a vector of byte-vectors into a single byte-vector + public inline fun flatten(input: vector>): vector { + let result = vector[]; + vector::for_each(input, |element| { + vector::append(&mut result, element); + }); + result + } + + /// This function creates a vector of `Element` by applying the function `f` to each element in the range [0, count) + public inline fun map_count(count: u64, f: |u64|Element): vector { + let vec = vector[]; + for (i in 0..count) { + vector::push_back(&mut vec, f(i)); + }; + vec + } + + /// This function create a bytes vector by applying the function `f` to a &mut buffer empty vector + /// this is useful for directly creating a bytes vector from an append_*() function in a single line + /// for example: let eighteen = bytes_of(|buf| append_u8(buf, 0x12)) + public inline fun bytes_of(f: |&mut vector|()): vector { + let buf = vector[]; + f(&mut buf); + buf + } + + // ================================================== Error Codes ================================================= + + const EINVALID_LENGTH: u64 = 1; +} \ No newline at end of file diff --git a/packages/layerzero-v2/initia/contracts/endpoint_v2_common/sources/universal_config.move b/packages/layerzero-v2/initia/contracts/endpoint_v2_common/sources/universal_config.move new file mode 100644 index 00000000..43438be4 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/endpoint_v2_common/sources/universal_config.move @@ -0,0 +1,167 @@ +/// This module provides a groud truth for EID and ZRO Metadata address +module endpoint_v2_common::universal_config { + use std::event::emit; + use std::fungible_asset::{Self, FungibleAsset, Metadata}; + use std::object::{Self, Object}; + use std::option::{Self, Option}; + use std::signer::address_of; + + #[test_only] + use std::account::create_signer_for_test; + + #[test_only] + friend endpoint_v2_common::universal_config_tests; + + struct UniversalStore has key { + // The EID for this endpoint + eid: u32, + // The ZRO metadata if it has been set + zro_data: Option>, + // Whether the ZRO address is locked. Once locked the zro metadata cannot be changed + zro_locked: bool, + } + + /// Initialize the UniversalStore must be called by endpoint_v2_common + public entry fun initialize(admin: &signer, eid: u32) acquires UniversalStore { + assert_admin(address_of(move admin)); + assert!(universal_store().eid == 0, EALREADY_INITIALIZED); + universal_store_mut().eid = eid; + } + + fun init_module(account: &signer) { + move_to(account, UniversalStore { + eid: 0, + zro_data: option::none(), + zro_locked: false, + }); + } + + #[test_only] + public fun init_module_for_test(eid: u32) acquires UniversalStore { + init_module(&create_signer_for_test(@endpoint_v2_common)); + initialize(&create_signer_for_test(@layerzero_admin), eid); + } + + #[view] + /// Get the EID for the V2 Endpoint + public fun eid(): u32 acquires UniversalStore { + universal_store().eid + } + + /// Set the ZRO address + /// @param account: The layerzero admin account signer + /// @param zro_address: The address of the ZRO metadata (@0x0 to unset) + public entry fun set_zro_address(account: &signer, zro_address: address) acquires UniversalStore { + assert_admin(address_of(move account)); + assert!(!universal_store().zro_locked, EZRO_ADDRESS_LOCKED); + + if (zro_address == @0x0) { + // Unset the ZRO address + assert!(option::is_some(&universal_store().zro_data), ENO_CHANGE); + let zro_data_store = &mut universal_store_mut().zro_data; + *zro_data_store = option::none(); + } else { + // Set the ZRO address + assert!(object::object_exists(zro_address), EINVALID_ZRO_ADDRESS); + if (has_zro_metadata()) { + assert!(get_zro_address() != zro_address, ENO_CHANGE); + }; + let zro_metadata = object::address_to_object(zro_address); + let zro_data_store = &mut universal_store_mut().zro_data; + *zro_data_store = option::some(zro_metadata); + }; + + emit(ZroMetadataSet { zro_address }); + } + + /// Lock the ZRO address so it can no longer be set or unset + public entry fun lock_zro_address(account: &signer) acquires UniversalStore { + assert_admin(address_of(move account)); + assert!(!universal_store().zro_locked, ENO_CHANGE); + assert!(option::is_some(&universal_store().zro_data), EZRO_ADDRESS_NOT_SET); + + let locked_store = &mut universal_store_mut().zro_locked; + *locked_store = true; + + emit(ZroMetadataLocked {}); + } + + #[view] + /// Check if ZRO address is set + public fun has_zro_metadata(): bool acquires UniversalStore { + option::is_some(&universal_store().zro_data) + } + + #[view] + /// Get the ZRO address + public fun get_zro_address(): address acquires UniversalStore { + object::object_address(&get_zro_metadata()) + } + + #[view] + /// Get the ZRO metadata + public fun get_zro_metadata(): Object acquires UniversalStore { + assert_zro_metadata_set(); + *option::borrow(&universal_store().zro_data) + } + + /// Assert that the ZRO metadata is set + public fun assert_zro_metadata_set() acquires UniversalStore { + assert!(has_zro_metadata(), EINVALID_ZRO_ADDRESS); + } + + /// Check if the given FungibleAsset is the ZRO asset + public fun is_zro(fa: &FungibleAsset): bool acquires UniversalStore { + if (!has_zro_metadata()) { return false }; + let metadata = fungible_asset::asset_metadata(fa); + metadata == get_zro_metadata() + } + + /// Check if the given Metadata is the ZRO metadata + public fun is_zro_metadata(metadata: Object): bool acquires UniversalStore { + if (!has_zro_metadata()) { return false }; + metadata == get_zro_metadata() + } + + // ==================================================== Helpers =================================================== + + inline fun assert_admin(admin: address) { + assert!(admin == @layerzero_admin, EUNAUTHORIZED); + } + + inline fun universal_store(): &UniversalStore { borrow_global(@endpoint_v2_common) } + + inline fun universal_store_mut(): &mut UniversalStore { borrow_global_mut(@endpoint_v2_common) } + + #[test_only] + public fun change_eid_for_test(eid: u32) acquires UniversalStore { universal_store_mut().eid = eid; } + + // ==================================================== Events ==================================================== + + #[event] + struct ZroMetadataSet has drop, store { + zro_address: address, + } + + #[event] + struct ZroMetadataLocked has drop, store {} + + #[test_only] + public fun zro_metadata_set(zro_address: address): ZroMetadataSet { + ZroMetadataSet { zro_address } + } + + #[test_only] + public fun zro_metadata_locked(): ZroMetadataLocked { + ZroMetadataLocked {} + } + + // ================================================== Error Codes ================================================= + + const EALREADY_INITIALIZED: u64 = 1; + const EUNAUTHORIZED: u64 = 2; + const EINVALID_ZRO_ADDRESS: u64 = 3; + const ENO_CHANGE: u64 = 4; + const EZRO_ADDRESS_NOT_SET: u64 = 5; + const EZRO_ADDRESS_LOCKED: u64 = 6; +} diff --git a/packages/layerzero-v2/initia/contracts/endpoint_v2_common/tests/assert_no_duplicates_tests.move b/packages/layerzero-v2/initia/contracts/endpoint_v2_common/tests/assert_no_duplicates_tests.move new file mode 100644 index 00000000..ecc9680d --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/endpoint_v2_common/tests/assert_no_duplicates_tests.move @@ -0,0 +1,17 @@ +#[test_only] +module endpoint_v2_common::assert_no_duplicates_tests { + use endpoint_v2_common::assert_no_duplicates::assert_no_duplicates; + + #[test] + fun does_not_abort_if_no_duplicate_addresses() { + let addresses = vector
[@1, @2, @3, @4]; + assert_no_duplicates(&addresses); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2_common::assert_no_duplicates::EDUPLICATE_ITEM)] + fun aborts_if_duplicate_addresses() { + let addresses = vector
[@1, @2, @3, @1, @5, @6]; + assert_no_duplicates(&addresses); + } +} diff --git a/packages/layerzero-v2/initia/contracts/endpoint_v2_common/tests/bytes_32_tests.move b/packages/layerzero-v2/initia/contracts/endpoint_v2_common/tests/bytes_32_tests.move new file mode 100644 index 00000000..b6e735a9 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/endpoint_v2_common/tests/bytes_32_tests.move @@ -0,0 +1,33 @@ +#[test_only] +module endpoint_v2_common::bytes_32_tests { + use endpoint_v2_common::bytes32::{from_address, from_bytes32, is_zero, to_address, to_bytes32, zero_bytes32}; + + #[test] + fun test_zero_bytes32() { + let zero_bytes32 = zero_bytes32(); + assert!(is_zero(&zero_bytes32), 0); + + let zero_manual = to_bytes32(x"0000000000000000000000000000000000000000000000000000000000000000"); + assert!(is_zero(&zero_manual), 0); + + let non_zero = to_bytes32(x"0000000000000000000000000000000000000000000000000000000000000001"); + assert!(!is_zero(&non_zero), 0); + } + + #[test] + fun test_from_to_address() { + let addr = @0x12345; + let bytes32 = from_address(addr); + assert!(from_bytes32(bytes32) == x"0000000000000000000000000000000000000000000000000000000000012345", 0); + + let addr2 = to_address(bytes32); + assert!(addr == addr2, 0); + } + + #[test] + fun test_to_from_bytes32() { + let bytes = x"1234560000000000000000000000000000000000000000000000000000123456"; + let bytes32 = to_bytes32(bytes); + assert!(from_bytes32(bytes32) == bytes, 0); + } +} diff --git a/packages/layerzero-v2/initia/contracts/endpoint_v2_common/tests/contract_identity_tests.move b/packages/layerzero-v2/initia/contracts/endpoint_v2_common/tests/contract_identity_tests.move new file mode 100644 index 00000000..04af0c10 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/endpoint_v2_common/tests/contract_identity_tests.move @@ -0,0 +1,61 @@ +#[test_only] +module endpoint_v2_common::contract_identity_tests { + use std::account::create_signer_for_test; + + use endpoint_v2_common::contract_identity; + use endpoint_v2_common::contract_identity::irrecoverably_destroy_contract_signer; + + #[test] + fun test_contract_identity() { + let account = create_signer_for_test(@1234); + let contract_signer = contract_identity::create_contract_signer(&account); + let call_ref = contract_identity::make_dynamic_call_ref(&contract_signer, @5555, b"general"); + let caller = contract_identity::get_dynamic_call_ref_caller(&call_ref, @5555, b"general"); + assert!(caller == @1234, 0); + irrecoverably_destroy_contract_signer(contract_signer); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2_common::contract_identity::ECONTRACT_SIGNER_ALREADY_EXISTS)] + fun test_contract_identity_fails_if_already_exists() { + let account = create_signer_for_test(@1234); + let cs1 = contract_identity::create_contract_signer(&account); + let cs2 = contract_identity::create_contract_signer(&account); + irrecoverably_destroy_contract_signer(cs1); + irrecoverably_destroy_contract_signer(cs2); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2_common::contract_identity::ETARGET_CONTRACT_MISMATCH)] + fun test_get_caller_fails_if_incorrect_receiver() { + let account = create_signer_for_test(@1234); + let contract_signer = contract_identity::create_contract_signer(&account); + let call_ref = contract_identity::make_dynamic_call_ref(&contract_signer, @5555, b"general"); + // shoud be @5555 + contract_identity::get_dynamic_call_ref_caller(&call_ref, @6666, b"general"); + irrecoverably_destroy_contract_signer(contract_signer); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2_common::contract_identity::EAUTHORIZATION_MISMATCH)] + fun test_get_caller_fails_if_incorrect_authorization() { + let account = create_signer_for_test(@1234); + let contract_signer = contract_identity::create_contract_signer(&account); + let call_ref = contract_identity::make_dynamic_call_ref(&contract_signer, @5555, b"general"); + let caller = contract_identity::get_dynamic_call_ref_caller(&call_ref, @5555, b"other"); + assert!(caller == @1234, 0); + irrecoverably_destroy_contract_signer(contract_signer); + } + + struct TestTarget {} + + #[test] + fun test_get_call_ref() { + let account = create_signer_for_test(@3333); + let contract_signer = contract_identity::create_contract_signer(&account); + let call_ref = contract_identity::make_call_ref(&contract_signer); + let caller = contract_identity::get_call_ref_caller(&call_ref); + assert!(caller == @3333, 0); + irrecoverably_destroy_contract_signer(contract_signer); + } +} diff --git a/packages/layerzero-v2/initia/contracts/endpoint_v2_common/tests/native_token_test_helpers.move b/packages/layerzero-v2/initia/contracts/endpoint_v2_common/tests/native_token_test_helpers.move new file mode 100644 index 00000000..27454078 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/endpoint_v2_common/tests/native_token_test_helpers.move @@ -0,0 +1,22 @@ +#[test_only] +module endpoint_v2_common::native_token_test_helpers { + use std::aptos_coin; + use std::fungible_asset; + use std::fungible_asset::FungibleAsset; + use std::primary_fungible_store::ensure_primary_store_exists; + + public fun initialize_native_token_for_test() { + let fa = aptos_coin::mint_apt_fa_for_test(0); + fungible_asset::destroy_zero(fa); + } + + public fun mint_native_token_for_test(amount: u64): FungibleAsset { + aptos_coin::mint_apt_fa_for_test(amount) + } + + public fun burn_token_for_test(token: FungibleAsset) { + let metadata = fungible_asset::asset_metadata(&token); + let burn_loc = ensure_primary_store_exists(@0x0, metadata); + fungible_asset::deposit(burn_loc, token); + } +} diff --git a/packages/layerzero-v2/initia/contracts/endpoint_v2_common/tests/packet_tests.move b/packages/layerzero-v2/initia/contracts/endpoint_v2_common/tests/packet_tests.move new file mode 100644 index 00000000..12cd5e75 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/endpoint_v2_common/tests/packet_tests.move @@ -0,0 +1,76 @@ +#[test_only] +module endpoint_v2_common::packet_tests { + use std::vector; + + use endpoint_v2_common::bytes32; + use endpoint_v2_common::guid::compute_guid; + use endpoint_v2_common::packet_raw::get_packet_bytes; + use endpoint_v2_common::packet_v1_codec; + use endpoint_v2_common::packet_v1_codec::{ + compute_payload, + extract_header, + get_dst_eid, + get_guid, + get_message, + get_nonce, + get_receiver, + get_sender, get_src_eid, new_packet_v1, + }; + + #[test] + fun test_encode_and_extract_packet() { + let src_eid = 1; + let sender = bytes32::from_address(@0x3); + let dst_eid = 2; + let receiver = bytes32::from_address(@0x4); + let nonce = 0x1234; + let message = vector[9, 8, 7, 6, 5, 4]; + let guid = compute_guid(nonce, src_eid, sender, dst_eid, receiver); + + let packet = new_packet_v1(src_eid, sender, dst_eid, receiver, nonce, guid, message); + + // test header extraction and assertion + let packet_header = extract_header(&packet); + packet_v1_codec::assert_receive_header(&packet_header, 2); + + // test textual decoders + assert!(get_src_eid(&packet) == 1, 2); + + assert!(get_sender(&packet) == sender, 3); + assert!(get_dst_eid(&packet) == 2, 4); + assert!(get_receiver(&packet) == receiver, 5); + assert!(get_nonce(&packet) == 0x1234, 6); + assert!(get_message(&packet) == message, 7); + assert!(get_guid(&packet) == guid, 8); + + // construct expected serialized packet + let expected = vector[ + 1, // version + 0, 0, 0, 0, 0, 0, 18, 52, // nonce + 0, 0, 0, 1, // src_eid + 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, 3, // sender + 0, 0, 0, 2, // dst_eid + 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, 4, // receiver + ]; + vector::append(&mut expected, bytes32::from_bytes32(guid)); + vector::append(&mut expected, vector[9, 8, 7, 6, 5, 4]); + + let serialized = get_packet_bytes(packet); + // test whole + assert!(serialized == expected, 1); + } + + #[test] + fun test_compute_payload() { + let guid = bytes32::to_bytes32(b"................................"); + let message = vector[18, 19, 20]; + let payload = compute_payload(guid, message); + + let expected = vector[ + 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, + 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, // 32 periods + 18, 19, 20, // message + ]; + assert!(payload == expected, 1); + } +} diff --git a/packages/layerzero-v2/initia/contracts/endpoint_v2_common/tests/packet_v1_codec_tests.move b/packages/layerzero-v2/initia/contracts/endpoint_v2_common/tests/packet_v1_codec_tests.move new file mode 100644 index 00000000..f7db0e96 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/endpoint_v2_common/tests/packet_v1_codec_tests.move @@ -0,0 +1,56 @@ +#[test_only] +module endpoint_v2_common::packet_v1_codec_tests { + use std::vector; + + use endpoint_v2_common::bytes32; + use endpoint_v2_common::packet_raw; + use endpoint_v2_common::packet_v1_codec::{assert_receive_header, new_packet_v1_header_only}; + + #[test] + fun test_assert_receive_header() { + let header = new_packet_v1_header_only( + 1, + bytes32::from_address(@0x3), + 2, + bytes32::from_address(@0x4), + 0x1234, + ); + assert_receive_header(&header, 2); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2_common::packet_v1_codec::EINVALID_PACKET_HEADER)] + fun test_assert_receive_header_fails_if_invalid_length() { + let header = packet_raw::bytes_to_raw_packet(b"1234"); + assert_receive_header(&header, 1); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2_common::packet_v1_codec::EINVALID_PACKET_VERSION)] + fun test_assert_receive_header_fails_if_invalid_version() { + let header = new_packet_v1_header_only( + 1, + bytes32::from_address(@0x3), + 2, + bytes32::from_address(@0x4), + 0x1234, + ); + let bytes = packet_raw::borrow_packet_bytes_mut(&mut header); + *vector::borrow_mut(bytes, 0) = 0x02; + assert_receive_header(&header, 2); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2_common::packet_v1_codec::EINVALID_EID)] + fun test_assert_receive_header_fails_if_invalid_dst_eid() { + let header = new_packet_v1_header_only( + 1, + bytes32::from_address(@0x3), + 2, + bytes32::from_address(@0x4), + 0x1234, + ); + // dst_eid (not src_eid) should match local_eid + assert_receive_header(&header, 1); + } +} diff --git a/packages/layerzero-v2/initia/contracts/endpoint_v2_common/tests/serde_tests.move b/packages/layerzero-v2/initia/contracts/endpoint_v2_common/tests/serde_tests.move new file mode 100644 index 00000000..355d1749 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/endpoint_v2_common/tests/serde_tests.move @@ -0,0 +1,146 @@ +#[test_only] +module endpoint_v2_common::serde_tests { + use endpoint_v2_common::bytes32; + use endpoint_v2_common::serde::{ + append_address, + append_bytes, + append_bytes32, + append_u128, + append_u16, + append_u256, + append_u32, + append_u64, + append_u8, + append_uint, + bytes_of, + extract_address, + extract_bytes32, + extract_bytes_until_end, + extract_u128, + extract_u16, + extract_u256, + extract_u32, + extract_u64, + extract_u8, + extract_uint, + map_count + }; + + #[test] + fun test_extract_uint() { + let data: vector = x"99999999123456789999999999999999999999"; + let position = 4; + let result: u32 = (extract_uint(&data, &mut position, 2) as u32); + assert!(result == 0x1234, (result as u64)); // little endian + assert!(position == 6, 0); + } + + #[test] + fun test_append_uint() { + let data: vector = x"99999999"; + append_uint(&mut data, 0x1234, 2); + assert!(data == x"999999991234", 0); + } + + #[test] + fun test_append_then_extract_uint() { + let data: vector = x"99999999"; + append_uint(&mut data, 0x1234, 2); + let position = 4; + let result: u32 = (extract_uint(&data, &mut position, 2) as u32); + assert!(result == 0x1234, 0); + assert!(position == 6, 0); + } + + #[test] + fun test_append_bytes() { + let data: vector = x"4444"; + append_bytes(&mut data, x"1234567890"); + assert!(data == x"44441234567890", 0); + } + + #[test] + fun test_extract_bytes_until_end() { + let data: vector = x"444400000000001234567890"; + let position = 2; + let result: vector = extract_bytes_until_end(&data, &mut position); + assert!(result == x"00000000001234567890", 0); + assert!(position == 12, 0); + } + + #[test] + fun test_append_address() { + let data: vector = x"4444"; + append_address(&mut data, @0x12345678); + assert!(data == x"44440000000000000000000000000000000000000000000000000000000012345678", 0); + } + + #[test] + fun test_extract_address() { + let data: vector = x"44440000000000000000000000000000000000000000000000000000000087654321"; + let position = 2; + let result: address = extract_address(&data, &mut position); + assert!(result == @0x87654321, 0); + assert!(position == 34, 0); + } + + #[test] + fun test_various() { + let buf: vector = x"4444"; + append_u8(&mut buf, 0x12); // 1 byte + append_u16(&mut buf, 0x1234); // 2 bytes + append_u32(&mut buf, 0x12345678); // 4 bytes + append_u64(&mut buf, 0x1234567890); // 8 bytes + append_u128(&mut buf, 0x12345678901234567890); // 16 bytes + append_u256(&mut buf, 0x1234567890123456789012345678901234567890123456789012345678901234); // 32 bytes + + let pos = 2; // start after the initial junk data + assert!(extract_u8(&buf, &mut pos) == 0x12, 0); + assert!(extract_u16(&buf, &mut pos) == 0x1234, 0); + assert!(extract_u32(&buf, &mut pos) == 0x12345678, 0); + assert!(extract_u64(&buf, &mut pos) == 0x1234567890, 0); + assert!(extract_u128(&buf, &mut pos) == 0x12345678901234567890, 0); + assert!(extract_u256(&buf, &mut pos) == 0x1234567890123456789012345678901234567890123456789012345678901234, 0); + // 2 initial bytes + 63 bytes in closure = 65 + assert!(pos == 65, 0); + } + + #[test] + fun test_append_bytes32() { + let data: vector = x"4444"; + let b32 = bytes32::to_bytes32(x"5555555555555555555555555555555555555555555555555555555555555555"); + append_bytes32(&mut data, b32); + assert!(data == x"44445555555555555555555555555555555555555555555555555555555555555555", 0); + } + + #[test] + fun test_extract_bytes32() { + let data = x"444455555555555555555555555555555555555555555555555555555555555555551234"; + let pos = 2; + let result = extract_bytes32(&data, &mut pos); + assert!(result == bytes32::to_bytes32(x"5555555555555555555555555555555555555555555555555555555555555555"), 0); + } + + #[test] + fun test_map_count() { + let data = x"00010002000300040005"; + let pos = 0; + let vec = map_count(4, |_i| extract_u16(&data, &mut pos)); + let expected = vector[1, 2, 3, 4]; + assert!(vec == expected, 0); + } + + #[test] + fun test_map_count_using_i() { + let vec = map_count(5, |i| (i as u8)); + let expected = vector[0, 1, 2, 3, 4]; + assert!(vec == expected, 0); + } + + #[test] + fun test_bytes_of() { + let data = bytes_of(|buf| append_address(buf, @0x12345678)); + let expected = x"0000000000000000000000000000000000000000000000000000000012345678"; + assert!(data == expected, 0); + } +} diff --git a/packages/layerzero-v2/initia/contracts/endpoint_v2_common/tests/universal_config_tests.move b/packages/layerzero-v2/initia/contracts/endpoint_v2_common/tests/universal_config_tests.move new file mode 100644 index 00000000..6634acd5 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/endpoint_v2_common/tests/universal_config_tests.move @@ -0,0 +1,136 @@ +#[test_only] +module endpoint_v2_common::universal_config_tests { + use std::account::create_signer_for_test; + use std::fungible_asset; + + use endpoint_v2_common::native_token_test_helpers::burn_token_for_test; + use endpoint_v2_common::universal_config::{ + assert_zro_metadata_set, eid, get_zro_address, get_zro_metadata, has_zro_metadata, init_module_for_test, + initialize, is_zro, is_zro_metadata, lock_zro_address, set_zro_address, + }; + use endpoint_v2_common::zro_test_helpers::create_fa; + + #[test] + fun test_eid() { + let ep_common = &create_signer_for_test(@layerzero_admin); + init_module_for_test(0); // Initializing to 0 = not initialized state + initialize(ep_common, 55); + assert!(eid() == 55, 1); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2_common::universal_config::EUNAUTHORIZED)] + fun test_eid_should_fail_if_not_layerzero_admin() { + let lz_admin = &create_signer_for_test(@endpoint_v2_common); + init_module_for_test(0); // Initializing to 0 = not initialized state + initialize(lz_admin, 55); + } + + #[test] + fun test_set_zro_address() { + let (other_addr, other_metadata, _) = create_fa(b"OTHER"); + let (zro_addr, zro_metadata, _) = create_fa(b"ZRO"); + init_module_for_test(55); + let lz_admin = &create_signer_for_test(@layerzero_admin); + assert!(!has_zro_metadata(), 0); + + set_zro_address(lz_admin, other_addr); + assert!(get_zro_address() == other_addr, 0); + assert!(has_zro_metadata(), 0); + assert!(get_zro_metadata() == other_metadata, 1); + assert!(has_zro_metadata(), 0); + // can change setting if not locked + set_zro_address(lz_admin, zro_addr); + assert!(has_zro_metadata(), 0); + // can unset if not locked + set_zro_address(lz_admin, @0x0); + assert!(!has_zro_metadata(), 0); + + set_zro_address(lz_admin, zro_addr); + assert!(get_zro_address() == zro_addr, 0); + assert!(get_zro_metadata() == zro_metadata, 0); + assert_zro_metadata_set(); + assert!(has_zro_metadata(), 0); + + let fa = fungible_asset::zero(zro_metadata); + let fa_other = fungible_asset::zero(other_metadata); + + assert!(is_zro_metadata(zro_metadata), 0); + assert!(!is_zro_metadata(other_metadata), 0); + + assert!(is_zro(&fa), 0); + assert!(!is_zro(&fa_other), 0); + + lock_zro_address(lz_admin); + + burn_token_for_test(fa); + burn_token_for_test(fa_other); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2_common::universal_config::EUNAUTHORIZED)] + fun test_set_zro_address_should_fail_if_not_admin() { + init_module_for_test(55); + let lz = &create_signer_for_test(@layerzero_treasury_admin); + + let (zro_addr, _, _) = create_fa(b"ZRO"); + set_zro_address(lz, zro_addr); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2_common::universal_config::EZRO_ADDRESS_LOCKED)] + fun test_set_zro_address_fails_if_locked_when_setting() { + let (zro_addr, _, _) = create_fa(b"ZRO"); + init_module_for_test(55); + + let lz_admin = &create_signer_for_test(@layerzero_admin); + set_zro_address(lz_admin, zro_addr); + lock_zro_address(lz_admin); + set_zro_address(lz_admin, zro_addr); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2_common::universal_config::ENO_CHANGE)] + fun test_lock_zro_address_fails_if_no_change() { + let (zro_addr, _, _) = create_fa(b"ZRO"); + init_module_for_test(55); + + let lz_admin = &create_signer_for_test(@layerzero_admin); + set_zro_address(lz_admin, zro_addr); + lock_zro_address(lz_admin); + lock_zro_address(lz_admin); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2_common::universal_config::EZRO_ADDRESS_LOCKED)] + fun test_set_zro_address_fails_if_locked_when_unsetting() { + let (zro_addr, _, _) = create_fa(b"ZRO"); + init_module_for_test(55); + + let lz_admin = &create_signer_for_test(@layerzero_admin); + set_zro_address(lz_admin, zro_addr); + lock_zro_address(lz_admin); + set_zro_address(lz_admin, @0x0); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2_common::universal_config::ENO_CHANGE)] + fun test_set_zro_address_fails_if_no_change() { + let (zro_addr, _, _) = create_fa(b"ZRO"); + init_module_for_test(55); + + let lz_admin = &create_signer_for_test(@layerzero_admin); + set_zro_address(lz_admin, zro_addr); + set_zro_address(lz_admin, zro_addr); + } + + + #[test] + #[expected_failure(abort_code = endpoint_v2_common::universal_config::ENO_CHANGE)] + fun test_set_zro_address_fails_if_no_change_unsetting() { + init_module_for_test(55); + + let lz_admin = &create_signer_for_test(@layerzero_admin); + set_zro_address(lz_admin, @0x0); + } +} diff --git a/packages/layerzero-v2/initia/contracts/endpoint_v2_common/tests/zro_test_helpers.move b/packages/layerzero-v2/initia/contracts/endpoint_v2_common/tests/zro_test_helpers.move new file mode 100644 index 00000000..36656bd0 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/endpoint_v2_common/tests/zro_test_helpers.move @@ -0,0 +1,29 @@ +#[test_only] +module endpoint_v2_common::zro_test_helpers { + use std::account::create_signer_for_test; + use std::fungible_asset; + use std::fungible_asset::{Metadata, MintRef}; + use std::object; + use std::object::{address_from_constructor_ref, Object, object_from_constructor_ref}; + use std::option; + use std::primary_fungible_store::create_primary_store_enabled_fungible_asset; + use std::string::utf8; + + public fun create_fa(name: vector): (address, Object, MintRef) { + let lz = &create_signer_for_test(@layerzero_treasury_admin); + let constructor = object::create_named_object(lz, name); + create_primary_store_enabled_fungible_asset( + &constructor, + option::none(), + utf8(b"ZRO"), + utf8(b"ZRO"), + 8, + utf8(b""), + utf8(b""), + ); + let mint_ref = fungible_asset::generate_mint_ref(&constructor); + let addr = address_from_constructor_ref(&constructor); + let metadata = object_from_constructor_ref(&constructor); + (addr, metadata, mint_ref) + } +} diff --git a/packages/layerzero-v2/initia/contracts/layerzero_views/Move.toml b/packages/layerzero-v2/initia/contracts/layerzero_views/Move.toml new file mode 100644 index 00000000..eed4cbac --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/layerzero_views/Move.toml @@ -0,0 +1,69 @@ +[package] +name = "layerzero_views" +version = "1.0.0" + +[addresses] +layerzero_views = "_" +dvn = "_" +router_node_0 = "_" +zro = "_" +simple_msglib = "_" +blocked_msglib = "_" +uln_302 = "_" +router_node_1 = "_" +endpoint_v2_common = "_" +endpoint_v2 = "_" +layerzero_admin = "_" +layerzero_treasury_admin = "_" +treasury = "_" +msglib_types = "_" +worker_common = "_" +price_feed_router_0 = "_" +price_feed_router_1 = "_" +price_feed_module_0 = "_" +executor_fee_lib_router_0 = "_" +executor_fee_lib_router_1 = "_" +dvn_fee_lib_router_0 = "_" +dvn_fee_lib_router_1 = "_" +executor_fee_lib_0 = "_" +dvn_fee_lib_0 = "_" + +[dev-addresses] +layerzero_views = "0x1010123" +dvn = "0x3001" +router_node_0 = "0x9001" +zro = "0x1112" +simple_msglib = "0x9002" +blocked_msglib = "0x9003" +uln_302 = "0x9005" +router_node_1 = "0x9007" +endpoint_v2_common = "0x9098" +endpoint_v2 = "0x12345678" +layerzero_admin = "0x18943124" +layerzero_treasury_admin = "0x1894312499" +treasury = "0x123432432" +msglib_types = "0x97324123" +worker_common = "0x3999" +price_feed_router_0 = "0x65DD71A" +price_feed_router_1 = "0x65DD71AB" +price_feed_module_0 = "0x65DD71" +executor_fee_lib_router_0 = "0x30001" +executor_fee_lib_router_1 = "0x30002" +dvn_fee_lib_router_0 = "0x30001a" +dvn_fee_lib_router_1 = "0x30002a" +executor_fee_lib_0 = "0x3000" +dvn_fee_lib_0 = "0x3000a" + +[dependencies.InitiaStdlib] +git = "https://github.com/initia-labs/move-natives.git" +rev = "77d5f3e140143bdaa41f850115b3035c134193e3" +subdir = "initia_stdlib" + +[dependencies] +endpoint_v2_common = { local = "../endpoint_v2_common" } +endpoint_v2 = { local = "../endpoint_v2" } +uln_302 = { local = "../msglib/libs/uln_302" } + +[dev-dependencies] +dvn = { local = "../workers/dvn" } +treasury = { local = "../treasury" } \ No newline at end of file diff --git a/packages/layerzero-v2/initia/contracts/layerzero_views/sources/endpoint_views.move b/packages/layerzero-v2/initia/contracts/layerzero_views/sources/endpoint_views.move new file mode 100644 index 00000000..775fbf71 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/layerzero_views/sources/endpoint_views.move @@ -0,0 +1,79 @@ +module layerzero_views::endpoint_views { + use endpoint_v2::endpoint; + use endpoint_v2_common::bytes32; + + // EXECUTABLE STATES + const STATE_NOT_EXECUTABLE: u8 = 0; + const STATE_VERIFIED_BUT_NOT_EXECUTABLE: u8 = 1; + const STATE_EXECUTABLE: u8 = 2; + const STATE_EXECUTED: u8 = 3; + + #[view] + public fun initializable( + src_eid: u32, + sender: vector, + receiver: address, + ): bool { + endpoint::initializable(src_eid, sender, receiver) + } + + #[view] + /// View function to check if a message is verifiable (can commit_verification on the uln via endpoint.verify()) + public fun verifiable( + src_eid: u32, + sender: vector, + nonce: u64, + receiver: address, + receive_lib: address, + ): bool { + if ( + !endpoint::is_valid_receive_library_for_oapp(receiver, src_eid, receive_lib) || + !endpoint::verifiable_view(src_eid, sender, nonce, receiver) + ) { + return false + }; + + true + } + + #[view] + public fun executable( + src_eid: u32, + sender: vector, + nonce: u64, + receiver: address, + ): u8 { + let sender_bytes32 = bytes32::to_bytes32(sender); + let has_payload_hash = endpoint::has_payload_hash( + src_eid, + sender_bytes32, + nonce, + receiver, + ); + + if (!has_payload_hash && nonce <= endpoint::get_lazy_inbound_nonce(receiver, src_eid, sender)) { + return STATE_EXECUTED + }; + + if (has_payload_hash) { + let payload_hash = endpoint::payload_hash( + receiver, + src_eid, + sender_bytes32, + nonce, + ); + if ( + !bytes32::is_zero(&payload_hash) && nonce <= endpoint::get_inbound_nonce( + receiver, src_eid, sender, + )) { + return STATE_EXECUTABLE + }; + + if (payload_hash != bytes32::zero_bytes32()) { + return STATE_VERIFIED_BUT_NOT_EXECUTABLE + } + }; + + return STATE_NOT_EXECUTABLE + } +} diff --git a/packages/layerzero-v2/initia/contracts/layerzero_views/sources/uln_302.move b/packages/layerzero-v2/initia/contracts/layerzero_views/sources/uln_302.move new file mode 100644 index 00000000..3de4e685 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/layerzero_views/sources/uln_302.move @@ -0,0 +1,94 @@ +module layerzero_views::uln_302 { + use endpoint_v2::endpoint; + use endpoint_v2_common::bytes32::{Self, Bytes32}; + use endpoint_v2_common::packet_raw; + use endpoint_v2_common::packet_v1_codec; + use endpoint_v2_common::universal_config; + use layerzero_views::endpoint_views; + + // VERIFICATION STATES + const STATE_VERIFYING: u8 = 0; + const STATE_VERIFIABLE: u8 = 1; + const STATE_VERIFIED: u8 = 2; + const STATE_NOT_INITIALIZABLE: u8 = 3; + + #[view] + /// View function to check if a message is verifiable (can commit_verification on the uln via endpoint.verify()) + public fun verifiable( + packet_header_bytes: vector, + payload_hash: vector, + ): u8 { + // extract data from packet + let packet_header = packet_raw::bytes_to_raw_packet(packet_header_bytes); + let src_eid = packet_v1_codec::get_src_eid(&packet_header); + let sender = packet_v1_codec::get_sender(&packet_header); + let nonce = packet_v1_codec::get_nonce(&packet_header); + let receiver = bytes32::to_address( + packet_v1_codec::get_receiver(&packet_header) + ); + + packet_v1_codec::assert_receive_header( + &packet_header, + universal_config::eid() + ); + + // check endpoint initializable + if (!endpoint_views::initializable( + src_eid, + bytes32::from_bytes32(sender), + receiver, + )) { + return STATE_NOT_INITIALIZABLE + }; + + // check endpoint verifiable + if (!endpoint_verifiable( + src_eid, + sender, + nonce, + receiver, + payload_hash, + )) { + return STATE_VERIFIED + }; + + // check uln verifiable + if (uln_302::msglib::verifiable(packet_header_bytes, payload_hash)) { + return STATE_VERIFIABLE + }; + STATE_VERIFYING + } + + fun endpoint_verifiable( + src_eid: u32, + sender: Bytes32, + nonce: u64, + receiver: address, + payload_hash: vector, + ): bool { + let (receive_lib, _) = endpoint_v2::endpoint::get_effective_receive_library(receiver, src_eid); + let sender_vec = bytes32::from_bytes32(sender); + // check if commit_verification is possible via endpoint::verify() + if (!endpoint_views::verifiable( + src_eid, + sender_vec, + nonce, + receiver, + receive_lib, + )) { + return false + }; + + if ( + endpoint::has_payload_hash_view( + src_eid, + bytes32::from_bytes32(sender), + nonce, + receiver, + ) && endpoint_v2::endpoint::get_payload_hash(receiver, src_eid, sender_vec, nonce) == + payload_hash) { + return false + }; + true + } +} diff --git a/packages/layerzero-v2/initia/contracts/layerzero_views/tests/layerzero_views_tests.move b/packages/layerzero-v2/initia/contracts/layerzero_views/tests/layerzero_views_tests.move new file mode 100644 index 00000000..94aca712 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/layerzero_views/tests/layerzero_views_tests.move @@ -0,0 +1,289 @@ +#[test_only] +module layerzero_views::layerzero_views_tests { + use std::account::{create_account_for_test, create_signer_for_test}; + use std::event::was_event_emitted; + use std::fungible_asset::{Self, FungibleAsset, Metadata}; + use std::object::address_to_object; + use std::option; + use std::primary_fungible_store; + + use endpoint_v2::channels::{ + packet_delivered_event, + packet_sent_event, + packet_verified_event, + }; + use endpoint_v2::endpoint::{Self, register_oapp, wrap_guid}; + use endpoint_v2_common::bytes32::{Self, from_bytes32}; + use endpoint_v2_common::contract_identity::{Self, irrecoverably_destroy_contract_signer, make_call_ref_for_test}; + use endpoint_v2_common::guid::compute_guid; + use endpoint_v2_common::native_token_test_helpers::{ + burn_token_for_test, + mint_native_token_for_test + }; + use endpoint_v2_common::packet_raw::{Self, get_packet_bytes}; + use endpoint_v2_common::packet_v1_codec::{Self, new_packet_v1}; + use executor_fee_lib_0::executor_option::{append_executor_options, new_executor_options, new_lz_receive_option}; + use layerzero_views::endpoint_views; + use layerzero_views::uln_302; + use msglib_types::worker_options; + use worker_common::worker_config; + + #[test] + fun test_verifiable() { + // account setup + let src_eid = 1; + let dst_eid = 1; + let oapp_address = @9112; + let account = &create_signer_for_test(oapp_address); + let sender = bytes32::from_address(oapp_address); + let receiver = bytes32::from_address(oapp_address); + let nonce = 1u64; + let guid = compute_guid( + nonce, + src_eid, + sender, + dst_eid, + receiver, + ); + + // init test context + endpoint_v2::test_helpers::setup_layerzero_for_test_uln(dst_eid, src_eid); + + // DVN Config + let pubkey1 = x"618d6c6d3b7bd345563636f27db50a39a7fe80c712d22d92aa3485264c9d8446b68db4661092205507a2a39cf3008c23e416cc4f41d87f4431a7cbc0d516f1a4"; + dvn::dvn::init_module_for_test(); + dvn::dvn::initialize( + &create_account_for_test(@dvn), + @dvn, + vector[@111], + vector[pubkey1], + 1, + vector[@uln_302], + @dvn_fee_lib_0, + ); + worker_config::set_dvn_dst_config( + &make_call_ref_for_test(@dvn), + src_eid, + 1000, + 1000, + 1000, + ); + // opt into using worker config for dvn feelib routing + uln_302::msglib::set_worker_config_for_fee_lib_routing_opt_in(&create_signer_for_test(@dvn), true); + + let feed_address = @1234; + worker_config::set_price_feed( + &make_call_ref_for_test(@dvn), + @price_feed_module_0, + feed_address, + ); + + let contract_signer = contract_identity::create_contract_signer(account); + let call_ref = &contract_identity::make_call_ref(&contract_signer); + register_oapp( + account, + std::string::utf8(b"test_oapp") + ); + endpoint::register_receive_pathway(call_ref, dst_eid, receiver); + + let options = worker_options::new_empty_type_3_options(); + append_executor_options( + &mut options, + &new_executor_options( + vector[new_lz_receive_option(100, 0)], + vector[], + vector[], + false, + ) + ); + + let message = vector[1, 2, 3, 4]; + + let (fee_in_native, _) = endpoint::quote( + oapp_address, + dst_eid, + bytes32::from_address(oapp_address), + message, + options, + false // pay_in_zro, + ); + + let packet = new_packet_v1( + src_eid, + sender, + dst_eid, + receiver, + nonce, + guid, + message, + ); + let packet_header = packet_v1_codec::extract_header(&packet); + let payload_hash = packet_v1_codec::get_payload_hash(&packet); + + // send test + + // deposit into account + let fee = mint_native_token_for_test(fee_in_native); + primary_fungible_store::ensure_primary_store_exists( + oapp_address, + fungible_asset::asset_metadata(&fee) + ); + burn_token_for_test(fee); + let zro_fee = option::none(); + + let native_metadata = address_to_object(@native_token_metadata_address); + let native_fee = primary_fungible_store::withdraw( + account, + native_metadata, + fee_in_native, + ); + + let sending_call_ref = &contract_identity::make_call_ref(&contract_signer); + assert!( + uln_302::verifiable( + packet_raw::get_packet_bytes(packet_header), + bytes32::from_bytes32(payload_hash) + ) == 0, // VERIFYING + 1, + ); + assert!( + endpoint_views::executable( + src_eid, + bytes32::from_bytes32(sender), + nonce, + bytes32::to_address(receiver) + ) == 0, // NOT_EXECUTABLE + 1, + ); + // send(oapp, dst_eid, type, options, native_fee); + endpoint::send( + sending_call_ref, + dst_eid, + receiver, + message, + options, + &mut native_fee, + &mut zro_fee, + ); + burn_token_for_test(native_fee); + option::destroy_none(zro_fee); + + assert!( + was_event_emitted( + &packet_sent_event(packet, options, @uln_302) + ), + 0, + ); + assert!( + uln_302::verifiable( + packet_raw::get_packet_bytes(packet_header), + bytes32::from_bytes32(payload_hash) + ) == 0, // VERIFYING + 1, + ); + assert!( + endpoint_views::executable( + src_eid, + bytes32::from_bytes32(sender), + nonce, + bytes32::to_address(receiver) + ) == 0, // NOT_EXECUTABLE + 1, + ); + + let admin = &create_signer_for_test(@111); + dvn::dvn::verify( + admin, + packet_raw::get_packet_bytes(packet_header), + bytes32::from_bytes32(payload_hash), + 100, + @uln_302, + 123456789123, + x"a13c94e82fc009f71f152f137bed7fb799fa7d75a91a0e3a4ed2000fd408ba052743f3b91ee00cf6a5e98cd4d12b3b2e4984213c0c1c5a251b4e98eeec54f7a800", + ); + + assert!( + uln_302::verifiable( + packet_raw::get_packet_bytes(packet_header), + bytes32::from_bytes32(payload_hash) + ) == 1, // VERIFIABLE + 2, + ); + assert!( + endpoint_views::executable( + src_eid, + bytes32::from_bytes32(sender), + nonce, + bytes32::to_address(receiver) + ) == 0, // NOT_EXECUTABLE + 1, + ); + + // - verify packet + endpoint_v2::endpoint::verify( + @uln_302, + get_packet_bytes(packet_header), + bytes32::from_bytes32(payload_hash), + b"", + ); + + assert!( + was_event_emitted( + &packet_verified_event( + src_eid, + from_bytes32(sender), + nonce, + oapp_address, + from_bytes32(payload_hash), + ) + ), + 1, + ); + + assert!( + uln_302::verifiable( + packet_raw::get_packet_bytes(packet_header), + bytes32::from_bytes32(payload_hash) + ) == 2, // VERIFIED + 3, + ); + assert!( + endpoint_views::executable( + src_eid, + bytes32::from_bytes32(sender), + nonce, + bytes32::to_address(receiver) + ) == 2, // NOT_EXECUTABLE + 1, + ); + + let receiving_call_ref = &contract_identity::make_call_ref(&contract_signer); + // - execute packet + // - check that counter has incremented + // doesn't matter who the caller of lz_receive + endpoint::clear( + receiving_call_ref, + src_eid, + sender, + nonce, + wrap_guid(guid), + message, + ); + assert!( + was_event_emitted( + &packet_delivered_event(src_eid, from_bytes32(sender), nonce, oapp_address) + ), + 2, + ); + assert!( + endpoint_views::executable( + src_eid, + bytes32::from_bytes32(sender), + nonce, + bytes32::to_address(receiver) + ) == 3, // NOT_EXECUTABLE + 1, + ); + irrecoverably_destroy_contract_signer(contract_signer); + } +} diff --git a/packages/layerzero-v2/initia/contracts/layerzero_views/tests/uln_302_tests.move b/packages/layerzero-v2/initia/contracts/layerzero_views/tests/uln_302_tests.move new file mode 100644 index 00000000..ca33e849 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/layerzero_views/tests/uln_302_tests.move @@ -0,0 +1,360 @@ +#[test_only] +module uln_302::uln_302_tests { + use std::account::{create_account_for_test, create_signer_for_test}; + use std::event::was_event_emitted; + use std::fungible_asset::{Self, FungibleAsset, Metadata}; + use std::object::address_to_object; + use std::option; + use std::primary_fungible_store; + + use endpoint_v2::channels::{ + packet_delivered_event, + packet_sent_event, + packet_verified_event, + }; + use endpoint_v2::endpoint::{Self, register_oapp, wrap_guid}; + use endpoint_v2_common::bytes32::{Self, from_bytes32}; + use endpoint_v2_common::contract_identity::{Self, irrecoverably_destroy_contract_signer, make_call_ref_for_test}; + use endpoint_v2_common::guid::compute_guid; + use endpoint_v2_common::native_token_test_helpers::{burn_token_for_test, mint_native_token_for_test}; + use endpoint_v2_common::packet_raw::{Self, get_packet_bytes}; + use endpoint_v2_common::packet_v1_codec::{Self, new_packet_v1}; + use endpoint_v2_common::zro_test_helpers::create_fa; + use executor_fee_lib_0::executor_option::{append_executor_options, new_executor_options, new_lz_receive_option}; + use msglib_types::worker_options; + use worker_common::worker_config; + + #[test] + fun test_verifiable() { + // account setup + let src_eid = 1; + let dst_eid = 1; + let oapp_address = @9112; + let account = &create_signer_for_test(oapp_address); + let sender = bytes32::from_address(oapp_address); + let receiver = bytes32::from_address(oapp_address); + let nonce = 1u64; + let guid = compute_guid(nonce, src_eid, sender, dst_eid, receiver); + + // init test context + endpoint_v2::test_helpers::setup_layerzero_for_test_uln(dst_eid, src_eid); + + // DVN Config + let pubkey1 = x"618d6c6d3b7bd345563636f27db50a39a7fe80c712d22d92aa3485264c9d8446b68db4661092205507a2a39cf3008c23e416cc4f41d87f4431a7cbc0d516f1a4"; + dvn::dvn::init_module_for_test(); + dvn::dvn::initialize( + &create_account_for_test(@dvn), + @dvn, + vector[@111], + vector[pubkey1], + 1, + vector[@uln_302], + @dvn_fee_lib_0, + ); + worker_config::set_dvn_dst_config( + &make_call_ref_for_test(@dvn), + src_eid, + 1000, + 1000, + 1000, + ); + // opt into using worker config for dvn feelib routing + uln_302::msglib::set_worker_config_for_fee_lib_routing_opt_in(&create_signer_for_test(@dvn), true); + + let feed_address = @1234; + worker_config::set_price_feed( + &make_call_ref_for_test(@dvn), + @price_feed_module_0, + feed_address, + ); + + let contract_signer = contract_identity::create_contract_signer(account); + let call_ref = &contract_identity::make_call_ref(&contract_signer); + register_oapp(account, std::string::utf8(b"test_oapp")); + endpoint::register_receive_pathway( + call_ref, + dst_eid, + receiver, + ); + + let options = worker_options::new_empty_type_3_options(); + append_executor_options(&mut options, &new_executor_options( + vector[ + new_lz_receive_option(100, 0), + ], + vector[], + vector[], + false, + )); + + let message = vector[1, 2, 3, 4]; + + let (fee_in_native, _) = endpoint::quote( + oapp_address, + dst_eid, + bytes32::from_address(oapp_address), + message, + options, + false, // pay_in_zro + ); + + let packet = new_packet_v1( + src_eid, + sender, + dst_eid, + receiver, + nonce, + guid, + message, + ); + + let payload_hash = packet_v1_codec::get_payload_hash(&packet); + + // send test + + // deposit into account + let fee = mint_native_token_for_test(fee_in_native); + primary_fungible_store::ensure_primary_store_exists(oapp_address, fungible_asset::asset_metadata(&fee)); + burn_token_for_test(fee); + let zro_fee = option::none(); + + let native_metadata = address_to_object(@native_token_metadata_address); + let native_fee = primary_fungible_store::withdraw( + account, + native_metadata, + fee_in_native, + ); + + let sending_call_ref = &contract_identity::make_call_ref(&contract_signer); + // send(oapp, dst_eid, type, options, native_fee); + endpoint::send( + sending_call_ref, + dst_eid, + receiver, + message, + options, + &mut native_fee, + &mut zro_fee, + ); + burn_token_for_test(native_fee); + option::destroy_none(zro_fee); + + assert!(was_event_emitted(&packet_sent_event(packet, options, @uln_302)), 0); + let admin = &create_signer_for_test(@111); + dvn::dvn::verify( + admin, + packet_raw::get_packet_bytes(packet_v1_codec::extract_header(&packet)), + bytes32::from_bytes32(payload_hash), + 100, + @uln_302, + 123456789123, + x"a13c94e82fc009f71f152f137bed7fb799fa7d75a91a0e3a4ed2000fd408ba052743f3b91ee00cf6a5e98cd4d12b3b2e4984213c0c1c5a251b4e98eeec54f7a800", + ); + + // - verify packet + let packet_header = packet_v1_codec::extract_header(&packet); + endpoint_v2::endpoint::verify( + @uln_302, + get_packet_bytes(packet_header), + bytes32::from_bytes32(payload_hash), + b"", + ); + + assert!(was_event_emitted( + &packet_verified_event(src_eid, from_bytes32(sender), nonce, oapp_address, from_bytes32(payload_hash)) + ), 1); + + let receiving_call_ref = &contract_identity::make_call_ref(&contract_signer); + // - execute packet + // - check that counter has incremented + // doesn't matter who the caller of lz_receive + endpoint::clear( + receiving_call_ref, + src_eid, + sender, + nonce, + wrap_guid(guid), + message, + ); + assert!(was_event_emitted(&packet_delivered_event(src_eid, from_bytes32(sender), nonce, oapp_address)), 2); + irrecoverably_destroy_contract_signer(contract_signer) + } + + #[test] + fun test_verifiable_with_zro() { + // account setup + let src_eid = 1; + let dst_eid = 1; + let oapp_address = @9112; + let account = &create_signer_for_test(oapp_address); + let sender = bytes32::from_address(oapp_address); + let receiver = bytes32::from_address(oapp_address); + let nonce = 1u64; + let guid = compute_guid(nonce, src_eid, sender, dst_eid, receiver); + + // init test context + endpoint_v2::test_helpers::setup_layerzero_for_test_uln(dst_eid, src_eid); + + // DVN Config + let pubkey1 = x"618d6c6d3b7bd345563636f27db50a39a7fe80c712d22d92aa3485264c9d8446b68db4661092205507a2a39cf3008c23e416cc4f41d87f4431a7cbc0d516f1a4"; + dvn::dvn::init_module_for_test(); + dvn::dvn::initialize( + &create_account_for_test(@dvn), + @dvn, + vector[@111], + vector[pubkey1], + 1, + vector[@uln_302], + @dvn_fee_lib_0, + ); + worker_config::set_dvn_dst_config( + &make_call_ref_for_test(@dvn), + src_eid, + 1000, + 1000, + 1000, + ); + // opt into using worker config for dvn feelib routing + uln_302::msglib::set_worker_config_for_fee_lib_routing_opt_in(&create_signer_for_test(@dvn), true); + + let feed_address = @1234; + worker_config::set_price_feed( + &make_call_ref_for_test(@dvn), + @price_feed_module_0, + feed_address, + ); + + let contract_signer = contract_identity::create_contract_signer(account); + let call_ref = &contract_identity::make_call_ref(&contract_signer); + register_oapp(account, std::string::utf8(b"test_oapp")); + endpoint::register_receive_pathway( + call_ref, + dst_eid, + receiver, + ); + + + let options = worker_options::new_empty_type_3_options(); + append_executor_options(&mut options, &new_executor_options( + vector[ + new_lz_receive_option(100, 0), + ], + vector[], + vector[], + false, + )); + + let message = vector[1, 2, 3, 4]; + + let (zro_metadata_addr, _zro_metadata, zro_mint_ref) = create_fa(b"ZRO"); + + endpoint_v2_common::universal_config::set_zro_address( + &create_signer_for_test(@layerzero_admin), + zro_metadata_addr, + ); + + endpoint_v2_common::universal_config::lock_zro_address(&create_signer_for_test(@layerzero_admin)); + + treasury::treasury::set_zro_enabled( + &create_signer_for_test(@layerzero_treasury_admin), + true, + ); + + treasury::treasury::set_zro_fee( + &create_signer_for_test(@layerzero_treasury_admin), + 100, + ); + + let (fee_in_native, fee_in_zro) = endpoint::quote( + oapp_address, + dst_eid, + bytes32::from_address(oapp_address), + message, + options, + true, // pay_in_zro + ); + assert!(fee_in_zro == 100, 0); + + let packet = new_packet_v1( + src_eid, + sender, + dst_eid, + receiver, + nonce, + guid, + message, + ); + + let payload_hash = packet_v1_codec::get_payload_hash(&packet); + + // send test + + // deposit into account + let fee = mint_native_token_for_test(fee_in_native); + primary_fungible_store::ensure_primary_store_exists(oapp_address, fungible_asset::asset_metadata(&fee)); + burn_token_for_test(fee); + + let zro_fee = option::some(fungible_asset::mint(&zro_mint_ref, fee_in_zro)); + + let native_metadata = address_to_object(@native_token_metadata_address); + let native_fee = primary_fungible_store::withdraw( + account, + native_metadata, + fee_in_native, + ); + + let sending_call_ref = &contract_identity::make_call_ref(&contract_signer); + // send(oapp, dst_eid, type, options, native_fee); + endpoint::send( + sending_call_ref, + dst_eid, + receiver, + message, + options, + &mut native_fee, + &mut zro_fee, + ); + burn_token_for_test(native_fee); + option::destroy(zro_fee, |fa| burn_token_for_test(fa)); + + assert!(was_event_emitted(&packet_sent_event(packet, options, @uln_302)), 0); + let admin = &create_signer_for_test(@111); + dvn::dvn::verify( + admin, + packet_raw::get_packet_bytes(packet_v1_codec::extract_header(&packet)), + bytes32::from_bytes32(payload_hash), + 100, + @uln_302, + 123456789123, + x"a13c94e82fc009f71f152f137bed7fb799fa7d75a91a0e3a4ed2000fd408ba052743f3b91ee00cf6a5e98cd4d12b3b2e4984213c0c1c5a251b4e98eeec54f7a800", + ); + + // - verify packet + let packet_header = packet_v1_codec::extract_header(&packet); + endpoint_v2::endpoint::verify( + @uln_302, + get_packet_bytes(packet_header), + bytes32::from_bytes32(payload_hash), + b"", + ); + + assert!(was_event_emitted( + &packet_verified_event(src_eid, from_bytes32(sender), nonce, oapp_address, from_bytes32(payload_hash)) + ), 1); + + let receiving_call_ref = &contract_identity::make_call_ref(&contract_signer); + // - execute packet + // - check that counter has incremented + // doesn't matter who the caller of lz_receive + endpoint::clear( + receiving_call_ref, + src_eid, + sender, + nonce, + wrap_guid(guid), + message, + ); + assert!(was_event_emitted(&packet_delivered_event(src_eid, from_bytes32(sender), nonce, oapp_address)), 2); + irrecoverably_destroy_contract_signer(contract_signer) + } +} diff --git a/packages/layerzero-v2/initia/contracts/msglib/libs/blocked_msglib/Move.toml b/packages/layerzero-v2/initia/contracts/msglib/libs/blocked_msglib/Move.toml new file mode 100644 index 00000000..964f022a --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/msglib/libs/blocked_msglib/Move.toml @@ -0,0 +1,23 @@ +[package] +name = "blocked_msglib" +version = "1.0.0" +authors = [] + +[addresses] +blocked_msglib = "_" +endpoint_v2 = "_" +layerzero_treasury_admin = "_" +endpoint_v2_common = "_" +layerzero_admin = "_" + +[dev-addresses] +blocked_msglib = "0x9003" +endpoint_v2 = "0x12345678" +layerzero_treasury_admin = "0x1894312499" +endpoint_v2_common = "0x94535243" +layerzero_admin = "0x18943124" + +[dependencies] +endpoint_v2_common = { local = "../../../endpoint_v2_common" } + +[dev-dependencies] diff --git a/packages/layerzero-v2/initia/contracts/msglib/libs/blocked_msglib/sources/router_calls.move b/packages/layerzero-v2/initia/contracts/msglib/libs/blocked_msglib/sources/router_calls.move new file mode 100644 index 00000000..d2078ea5 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/msglib/libs/blocked_msglib/sources/router_calls.move @@ -0,0 +1,73 @@ +module blocked_msglib::router_calls { + use std::any::Any; + use std::fungible_asset::FungibleAsset; + use std::option::Option; + + use endpoint_v2_common::bytes32::Bytes32; + use endpoint_v2_common::contract_identity::DynamicCallRef; + use endpoint_v2_common::packet_raw::RawPacket; + use endpoint_v2_common::send_packet::SendPacket; + + const ENOT_IMPLEMENTED: u64 = 1; + + public fun quote( + _packet: SendPacket, + _options: vector, + _pay_in_zro: bool, + ): (u64, u64) { + abort ENOT_IMPLEMENTED + } + + public fun send( + _call_ref: &DynamicCallRef, + _packet: SendPacket, + _options: vector, + _native_token: &mut FungibleAsset, + _zro_token: &mut Option, + ): (u64, u64, RawPacket) { + abort ENOT_IMPLEMENTED + } + + public fun commit_verification( + _call_ref: &DynamicCallRef, + _packet_header: RawPacket, + _payload_hash: Bytes32, + _extra_data: vector, + ): (address, u32, Bytes32, u64) { + abort ENOT_IMPLEMENTED + } + + public fun dvn_verify(_call_ref: &DynamicCallRef, _params: Any) { + abort ENOT_IMPLEMENTED + } + + public fun set_config( + _call_ref: &DynamicCallRef, + _oapp: address, + _eid: u32, + _config_type: u32, + _config: vector + ) { + abort ENOT_IMPLEMENTED + } + + #[view] + public fun get_config(_oapp: address, _eid: u32, _config_type: u32): vector { + abort ENOT_IMPLEMENTED + } + + #[view] + public fun version(): (u64 /*major*/, u8 /*minor*/, u8 /*endpoint_version*/) { + (0xffffffffffffffff, 0xff, 2) + } + + #[view] + public fun is_supported_send_eid(_eid: u32): bool { + true + } + + #[view] + public fun is_supported_receive_eid(_eid: u32): bool { + true + } +} diff --git a/packages/layerzero-v2/initia/contracts/msglib/libs/simple_msglib/Move.toml b/packages/layerzero-v2/initia/contracts/msglib/libs/simple_msglib/Move.toml new file mode 100644 index 00000000..15318045 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/msglib/libs/simple_msglib/Move.toml @@ -0,0 +1,26 @@ +[package] +name = "simple_msglib" +version = "1.0.0" +authors = [] + +[addresses] +simple_msglib = "_" +endpoint_v2 = "_" +layerzero_treasury_admin = "_" +endpoint_v2_common = "_" +layerzero_admin = "_" + +[dev-addresses] +simple_msglib = "0x9002" +endpoint_v2 = "0x12345678" +layerzero_treasury_admin = "0x1894312499" +endpoint_v2_common = "0x9098" +layerzero_admin = "0x18943124" + +[dependencies.InitiaStdlib] +git = "https://github.com/initia-labs/move-natives.git" +rev = "77d5f3e140143bdaa41f850115b3035c134193e3" +subdir = "initia_stdlib" + +[dependencies] +endpoint_v2_common = { local = "../../../endpoint_v2_common" } \ No newline at end of file diff --git a/packages/layerzero-v2/initia/contracts/msglib/libs/simple_msglib/sources/msglib.move b/packages/layerzero-v2/initia/contracts/msglib/libs/simple_msglib/sources/msglib.move new file mode 100644 index 00000000..b9917780 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/msglib/libs/simple_msglib/sources/msglib.move @@ -0,0 +1,39 @@ +module simple_msglib::msglib { + const ADMIN: address = @layerzero_admin; + + // Simple message lib has globally fixed fees + struct SimpleMessageLibConfig has key { + native_fee: u64, + zro_fee: u64, + } + + fun init_module(account: &signer) { + move_to(move account, SimpleMessageLibConfig { native_fee: 0, zro_fee: 0 }); + } + + #[test_only] + public fun initialize_for_test() { + init_module(&std::account::create_signer_for_test(@simple_msglib)); + } + + public entry fun set_messaging_fee( + account: &signer, + native_fee: u64, + zro_fee: u64, + ) acquires SimpleMessageLibConfig { + assert!(std::signer::address_of(move account) == ADMIN, EUNAUTHORIZED); + let msglib_config = borrow_global_mut(@simple_msglib); + msglib_config.native_fee = native_fee; + msglib_config.zro_fee = zro_fee; + } + + #[view] + public fun get_messaging_fee(): (u64, u64) acquires SimpleMessageLibConfig { + let msglib_config = borrow_global(@simple_msglib); + (msglib_config.native_fee, msglib_config.zro_fee) + } + + // ================================================== Error Codes ================================================= + + const EUNAUTHORIZED: u64 = 1; +} diff --git a/packages/layerzero-v2/initia/contracts/msglib/libs/simple_msglib/sources/router_calls.move b/packages/layerzero-v2/initia/contracts/msglib/libs/simple_msglib/sources/router_calls.move new file mode 100644 index 00000000..7a60fce7 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/msglib/libs/simple_msglib/sources/router_calls.move @@ -0,0 +1,113 @@ +module simple_msglib::router_calls { + use std::any::Any; + use std::fungible_asset::FungibleAsset; + use std::option::{Self, Option}; + + use endpoint_v2_common::bytes32::{Self, Bytes32}; + use endpoint_v2_common::contract_identity::{DynamicCallRef, get_dynamic_call_ref_caller}; + use endpoint_v2_common::packet_raw::RawPacket; + use endpoint_v2_common::packet_v1_codec; + use endpoint_v2_common::send_packet::{Self, SendPacket}; + use endpoint_v2_common::universal_config; + use simple_msglib::msglib; + + public fun quote(_packet: SendPacket, _options: vector, pay_in_zro: bool): (u64, u64) { + let (native_fee, zro_fee) = msglib::get_messaging_fee(); + if (pay_in_zro) (0, zro_fee) else (native_fee, 0) + } + + public fun send( + call_ref: &DynamicCallRef, + packet: SendPacket, + _options: vector, + _native_token: &mut FungibleAsset, + zro_token: &mut Option, + ): (u64, u64, RawPacket) { + assert!(get_dynamic_call_ref_caller(call_ref, @simple_msglib, b"send") == @endpoint_v2, EINVALID_CALLER); + + let ( + nonce, + src_eid, + sender, + dst_eid, + receiver, + guid, + message, + ) = send_packet::unpack_send_packet(packet); + + let (native_fee, zro_fee) = msglib::get_messaging_fee(); + let packet = packet_v1_codec::new_packet_v1( + src_eid, + sender, + dst_eid, + receiver, + nonce, + guid, + message, + ); + if (option::is_some(zro_token)) (0, zro_fee, packet) else (native_fee, 0, packet) + } + + public fun commit_verification( + call_ref: &DynamicCallRef, + packet_header: RawPacket, + _payload_hash: Bytes32, + _extra_data: vector, + ): (address, u32, Bytes32, u64) { + assert!( + get_dynamic_call_ref_caller(call_ref, @simple_msglib, b"commit_verification") == @endpoint_v2, + EINVALID_CALLER, + ); + + // Assert header + packet_v1_codec::assert_receive_header(&packet_header, universal_config::eid()); + + // No check for verifiable in simple message lib + + // Decode the header + let receiver = bytes32::to_address(packet_v1_codec::get_receiver(&packet_header)); + let src_eid = packet_v1_codec::get_src_eid(&packet_header); + let sender = packet_v1_codec::get_sender(&packet_header); + let nonce = packet_v1_codec::get_nonce(&packet_header); + (receiver, src_eid, sender, nonce) + } + + public fun dvn_verify(_call_ref: &DynamicCallRef, _params: Any) { + abort ENOT_IMPLEMENTED + } + + public fun set_config( + _call_ref: &DynamicCallRef, + __oapp: address, + _eid: u32, + _config_type: u32, + _config: vector + ) { + abort ENOT_IMPLEMENTED + } + + #[view] + public fun get_config(_oapp: address, _eid: u32, _config_type: u32): vector { + abort ENOT_IMPLEMENTED + } + + #[view] + public fun version(): (u64 /*major*/, u8 /*minor*/, u8 /*endpoint_version*/) { + (0, 0, 2) + } + + #[view] + public fun is_supported_send_eid(_eid: u32): bool { + true + } + + #[view] + public fun is_supported_receive_eid(_eid: u32): bool { + true + } + + // ================================================== Error Codes ================================================= + + const EINVALID_CALLER: u64 = 1; + const ENOT_IMPLEMENTED: u64 = 2; +} diff --git a/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/Move.toml b/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/Move.toml new file mode 100644 index 00000000..726332dd --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/Move.toml @@ -0,0 +1,53 @@ +[package] +name = "uln_302" +version = "1.0.0" +authors = [] + +[addresses] +uln_302 = "_" +endpoint_v2_common = "_" +endpoint_v2 = "_" +layerzero_admin = "_" +layerzero_treasury_admin = "_" + +msglib_types = "_" +treasury = "_" +worker_common = "_" +price_feed_router_0 = "_" +price_feed_router_1 = "_" +executor_fee_lib_0 = "_" +dvn_fee_lib_0 = "_" +executor_fee_lib_router_0 = "_" +executor_fee_lib_router_1 = "_" +dvn_fee_lib_router_0 = "_" +dvn_fee_lib_router_1 = "_" +dvn = "_" + +[dev-addresses] +uln_302 = "0x9005" +endpoint_v2_common = "0x9098" +endpoint_v2 = "0x12345678" +layerzero_admin = "0x18943124" +layerzero_treasury_admin = "0x1894312499" +msglib_types = "0x97329841" +treasury = "0x123432432" +worker_common = "0x3999" +price_feed_router_0 = "0x65DD71A" +price_feed_router_1 = "0x65DD71AB" +price_feed_module_0 = "0x65DD71" +executor_fee_lib_0 = "0x13245135" +dvn_fee_lib_0 = "0x13245135a" +executor_fee_lib_router_0 = "0x30001" +executor_fee_lib_router_1 = "0x30002" +dvn_fee_lib_router_0 = "0x30001a" +dvn_fee_lib_router_1 = "0x30002a" +dvn = "0x324241" + +[dependencies] +msglib_types = { local = "../../msglib_types" } +endpoint_v2_common = { local = "../../../endpoint_v2_common" } +treasury = { local = "../../../treasury" } +executor_fee_lib_router_0 = { local = "../../../worker_peripherals/fee_lib_routers/executor_fee_lib_router_0" } +dvn_fee_lib_router_0 = { local = "../../../worker_peripherals/fee_lib_routers/dvn_fee_lib_router_0" } + +[dev-dependencies] diff --git a/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/sources/admin.move b/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/sources/admin.move new file mode 100644 index 00000000..bdd57df2 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/sources/admin.move @@ -0,0 +1,78 @@ +/// Entrypoint for administrative functions for the ULN 302 module. The functions in this module can only be called by +/// ULN administrators +module uln_302::admin { + use std::signer::address_of; + + use msglib_types::configs_executor::new_executor_config; + use msglib_types::configs_uln::new_uln_config; + use uln_302::configuration; + + /// Sets the default send config for an EID + public entry fun set_default_uln_send_config( + account: &signer, + dst_eid: u32, + confirmations: u64, + optional_dvn_threshold: u8, + required_dvns: vector
, + optional_dvns: vector
, + ) { + assert_admin(address_of(move account)); + configuration::set_default_send_uln_config( + dst_eid, + new_uln_config( + confirmations, + optional_dvn_threshold, + required_dvns, + optional_dvns, + false, + false, + false, + ) + ) + } + + /// Sets the default receive config for an EID + public entry fun set_default_uln_receive_config( + account: &signer, + src_eid: u32, + confirmations: u64, + optional_dvn_threshold: u8, + required_dvns: vector
, + optional_dvns: vector
, + ) { + assert_admin(address_of(move account)); + configuration::set_default_receive_uln_config( + src_eid, + new_uln_config( + confirmations, + optional_dvn_threshold, + required_dvns, + optional_dvns, + false, + false, + false, + ) + ) + } + + /// Sets the default executor config for an EID + public entry fun set_default_executor_config( + account: &signer, + eid: u32, + max_message_size: u32, + executor_address: address, + ) { + assert_admin(address_of(move account)); + let config = new_executor_config(max_message_size, executor_address); + configuration::set_default_executor_config(eid, config) + } + + /// Internal function to assert that the caller is the admin + fun assert_admin(admin: address) { + assert!(admin == @layerzero_admin, ENOT_AUTHORIZED); + } + + // ================================================== Error Codes ================================================= + + const ENOT_AUTHORIZED: u64 = 1; +} diff --git a/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/sources/internal/assert_valid_default_uln_config.move b/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/sources/internal/assert_valid_default_uln_config.move new file mode 100644 index 00000000..9794e384 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/sources/internal/assert_valid_default_uln_config.move @@ -0,0 +1,51 @@ +module uln_302::assert_valid_default_uln_config { + use endpoint_v2_common::assert_no_duplicates; + use msglib_types::configs_uln::{Self, get_optional_dvn_threshold, UlnConfig}; + + friend uln_302::configuration; + + #[test_only] + friend uln_302::assert_valid_default_uln_config_tests; + + const MAX_DVNS: u64 = 127; + + public(friend) fun assert_valid_default_uln_config(config: &UlnConfig) { + let use_default_for_required_dvns = configs_uln::get_use_default_for_required_dvns(config); + let use_default_for_optional_dvns = configs_uln::get_use_default_for_optional_dvns(config); + let use_default_for_confirmations = configs_uln::get_use_default_for_confirmations(config); + assert!(!use_default_for_required_dvns, EREQUESTING_USE_DEFAULT_REQUIRED_DVNS_FOR_DEFAULT_CONFIG); + assert!(!use_default_for_optional_dvns, EREQUESTING_USE_DEFAULT_OPTIONAL_DVNS_FOR_DEFAULT_CONFIG); + assert!(!use_default_for_confirmations, EREQUESTING_USE_DEFAULT_CONFIRMATIONS_FOR_DEFAULT_CONFIG); + let required_dvn_count = configs_uln::get_required_dvn_count(config); + let optional_dvn_count = configs_uln::get_optional_dvn_count(config); + let optional_dvn_threshold = get_optional_dvn_threshold(config); + + // Make sure there is an effective DVN threshold (required + optional threshold) >= 1 + assert!(required_dvn_count > 0 || optional_dvn_threshold > 0, ENO_EFFECTIVE_DVN_THRESHOLD); + + // Optional threshold should not be greater than count of optional dvns + assert!((optional_dvn_threshold as u64) <= optional_dvn_count, EOPTIONAL_DVN_THRESHOLD_EXCEEDS_COUNT); + + // If there are optional dvns, there should be an effective threshold + assert!(optional_dvn_count == 0 || optional_dvn_threshold > 0, EINVALID_DVN_THRESHOLD); + + // Make sure there are no duplicates in the required and optional DVNs + // This does not check for duplicates across required and optional DVN lists because of unclear outcomes + // with partial default configurations. The admin should take care to avoid duplicates between lists + assert!(required_dvn_count <= MAX_DVNS, ETOO_MANY_REQUIRED_DVNS); + assert!(optional_dvn_count <= MAX_DVNS, ETOO_MANY_OPTIONAL_DVNS); + assert_no_duplicates::assert_no_duplicates(&configs_uln::get_required_dvns(config)); + assert_no_duplicates::assert_no_duplicates(&configs_uln::get_optional_dvns(config)); + } + + // ================================================== Error Codes ================================================= + + const EINVALID_DVN_THRESHOLD: u64 = 1; + const ENO_EFFECTIVE_DVN_THRESHOLD: u64 = 2; + const EOPTIONAL_DVN_THRESHOLD_EXCEEDS_COUNT: u64 = 3; + const EREQUESTING_USE_DEFAULT_CONFIRMATIONS_FOR_DEFAULT_CONFIG: u64 = 4; + const EREQUESTING_USE_DEFAULT_OPTIONAL_DVNS_FOR_DEFAULT_CONFIG: u64 = 5; + const EREQUESTING_USE_DEFAULT_REQUIRED_DVNS_FOR_DEFAULT_CONFIG: u64 = 6; + const ETOO_MANY_OPTIONAL_DVNS: u64 = 7; + const ETOO_MANY_REQUIRED_DVNS: u64 = 8; +} diff --git a/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/sources/internal/assert_valid_uln_config.move b/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/sources/internal/assert_valid_uln_config.move new file mode 100644 index 00000000..206dfd15 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/sources/internal/assert_valid_uln_config.move @@ -0,0 +1,65 @@ +module uln_302::assert_valid_uln_config { + use endpoint_v2_common::assert_no_duplicates; + use msglib_types::configs_uln::{Self, UlnConfig}; + + friend uln_302::configuration; + + #[test_only] + friend uln_302::assert_valid_oapp_uln_config_tests; + + const MAX_DVNS: u64 = 127; + + public(friend) fun assert_valid_uln_config(oapp_config: &UlnConfig, default_config: &UlnConfig) { + let use_default_for_required_dvns = configs_uln::get_use_default_for_required_dvns(oapp_config); + let use_default_for_optional_dvns = configs_uln::get_use_default_for_optional_dvns(oapp_config); + let use_default_for_confirmations = configs_uln::get_use_default_for_confirmations(oapp_config); + let required_dvn_count = configs_uln::get_required_dvn_count(oapp_config); + let optional_dvn_count = configs_uln::get_optional_dvn_count(oapp_config); + let optional_dvn_threshold = configs_uln::get_optional_dvn_threshold(oapp_config); + if (use_default_for_required_dvns) { + assert!(required_dvn_count == 0, ENONEMPTY_REQUIRED_DVNS_WITH_USE_DEFAULT); + }; + if (use_default_for_optional_dvns) { + assert!(optional_dvn_count == 0, ENONEMPTY_OPTIONAL_DVNS_WITH_USE_DEFAULT); + }; + if (use_default_for_confirmations) { + assert!(configs_uln::get_confirmations(oapp_config) == 0, + ENONZERO_CONFIRMATIONS_PROVIDED_FOR_DEFAULT_CONFIG, + ); + }; + + assert!(required_dvn_count <= MAX_DVNS, ETOO_MANY_REQUIRED_DVNS); + assert!(optional_dvn_count <= MAX_DVNS, ETOO_MANY_OPTIONAL_DVNS); + + // Make sure there are no duplicates in the required and optional DVNs + // The admin should take care to avoid duplicates between lists, which is not checked here + assert_no_duplicates::assert_no_duplicates(&configs_uln::get_required_dvns(oapp_config)); + assert_no_duplicates::assert_no_duplicates(&configs_uln::get_optional_dvns(oapp_config)); + + // Optional threshold should not be greater than count of optional dvns + assert!((optional_dvn_threshold as u64) <= optional_dvn_count, EOPTIONAL_DVN_THRESHOLD_EXCEEDS_COUNT); + + // If there are optional dvns, there should be an effective threshold + assert!(optional_dvn_count == 0 || optional_dvn_threshold > 0, EINVALID_DVN_THRESHOLD); + + // make sure there is an effective DVN threshold (required + optional threshold) >= 1 + let effective_optional_threshold = if (!use_default_for_optional_dvns) optional_dvn_threshold else { + configs_uln::get_optional_dvn_threshold(default_config) + }; + let effective_required_count = if (!use_default_for_required_dvns) required_dvn_count else { + configs_uln::get_required_dvn_count(default_config) + }; + assert!(effective_optional_threshold > 0 || effective_required_count > 0, ENO_EFFECTIVE_DVN_THRESHOLD); + } + + // ================================================== Error Codes ================================================= + + const EINVALID_DVN_THRESHOLD: u64 = 1; + const ENONEMPTY_OPTIONAL_DVNS_WITH_USE_DEFAULT: u64 = 2; + const ENONEMPTY_REQUIRED_DVNS_WITH_USE_DEFAULT: u64 = 3; + const ENONZERO_CONFIRMATIONS_PROVIDED_FOR_DEFAULT_CONFIG: u64 = 4; + const ENO_EFFECTIVE_DVN_THRESHOLD: u64 = 5; + const EOPTIONAL_DVN_THRESHOLD_EXCEEDS_COUNT: u64 = 6; + const ETOO_MANY_OPTIONAL_DVNS: u64 = 7; + const ETOO_MANY_REQUIRED_DVNS: u64 = 8; +} \ No newline at end of file diff --git a/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/sources/internal/configuration.move b/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/sources/internal/configuration.move new file mode 100644 index 00000000..bf3e8939 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/sources/internal/configuration.move @@ -0,0 +1,304 @@ +/// This module handles the configuration of the ULN for both the send and receive sides +module uln_302::configuration { + use std::event::emit; + use std::option; + use std::vector; + + use endpoint_v2_common::serde; + use msglib_types::configs_executor::{Self, ExecutorConfig, new_executor_config}; + use msglib_types::configs_uln::{ + Self, get_confirmations, get_optional_dvn_threshold, get_optional_dvns, get_required_dvns, + get_use_default_for_confirmations, get_use_default_for_optional_dvns, get_use_default_for_required_dvns, + new_uln_config, UlnConfig, + }; + use uln_302::assert_valid_default_uln_config::assert_valid_default_uln_config; + use uln_302::assert_valid_uln_config::assert_valid_uln_config; + use uln_302::uln_302_store; + + friend uln_302::sending; + friend uln_302::verification; + friend uln_302::msglib; + friend uln_302::admin; + friend uln_302::router_calls; + + #[test_only] + friend uln_302::verification_tests; + + #[test_only] + friend uln_302::msglib_tests; + + #[test_only] + friend uln_302::configuration_tests; + + // This is needed for tests of inline functions in the sending module, which call friend functions in this module + #[test_only] + friend uln_302::sending_tests; + + // Configuration Types as used in this message library + public inline fun CONFIG_TYPE_EXECUTOR(): u32 { 1 } + + public inline fun CONFIG_TYPE_SEND_ULN(): u32 { 2 } + + public inline fun CONFIG_TYPE_RECV_ULN(): u32 { 3 } + + const ULN_SEND_SIDE: u8 = 0; + const ULN_RECEIVE_SIDE: u8 = 1; + + // ===================================================== OApp ===================================================== + + /// Gets the serialized configuration for an OApp eid and config type, returning the default if the OApp + /// configuration is not set + public(friend) fun get_config(oapp: address, eid: u32, config_type: u32): vector { + if (config_type == CONFIG_TYPE_SEND_ULN()) { + let config = get_send_uln_config(oapp, eid); + serde::bytes_of(|buf| configs_uln::append_uln_config(buf, config)) + } else if (config_type == CONFIG_TYPE_RECV_ULN()) { + let config = get_receive_uln_config(oapp, eid); + serde::bytes_of(|buf| configs_uln::append_uln_config(buf, config)) + } else if (config_type == CONFIG_TYPE_EXECUTOR()) { + let config = get_executor_config(oapp, eid); + serde::bytes_of(|buf| configs_executor::append_executor_config(buf, config)) + } else { + abort EUNKNOWN_CONFIG_TYPE + } + } + + public(friend) fun set_config(oapp: address, eid: u32, config_type: u32, config: vector) { + let pos = 0; + if (config_type == CONFIG_TYPE_SEND_ULN()) { + let uln_config = configs_uln::extract_uln_config(&config, &mut pos); + set_send_uln_config(oapp, eid, uln_config); + } else if (config_type == CONFIG_TYPE_RECV_ULN()) { + let uln_config = configs_uln::extract_uln_config(&config, &mut pos); + set_receive_uln_config(oapp, eid, uln_config); + } else if (config_type == CONFIG_TYPE_EXECUTOR()) { + let executor_config = configs_executor::extract_executor_config(&config, &mut pos); + set_executor_config(oapp, eid, executor_config); + } else { + abort EUNKNOWN_CONFIG_TYPE + }; + + // If the entire config was not consumed, the config is invalid + assert!(vector::length(&config) == pos, EINVALID_CONFIG_LENGTH); + } + + // ================================================= Receive Side ================================================= + + public(friend) fun supports_receive_eid(eid: u32): bool { + uln_302_store::has_default_receive_uln_config(eid) + } + + public(friend) fun set_default_receive_uln_config(eid: u32, config: UlnConfig) { + assert_valid_default_uln_config(&config); + uln_302_store::set_default_receive_uln_config(eid, config); + emit(DefaultUlnConfigSet { eid, config, send_or_receive: ULN_RECEIVE_SIDE }); + } + + public(friend) fun set_receive_uln_config(receiver: address, eid: u32, config: UlnConfig) { + let default = uln_302_store::get_default_receive_uln_config(eid); + assert!(option::is_some(&default), EEID_NOT_CONFIGURED); + assert_valid_uln_config(&config, option::borrow(&default)); + uln_302_store::set_receive_uln_config(receiver, eid, config); + emit(UlnConfigSet { oapp: receiver, eid, config, send_or_receive: ULN_RECEIVE_SIDE }); + } + + public(friend) fun get_receive_uln_config(receiver: address, eid: u32): UlnConfig { + let oapp_config = uln_302_store::get_receive_uln_config(receiver, eid); + let default_config = uln_302_store::get_default_receive_uln_config(eid); + + if (option::is_some(&oapp_config) && option::is_some(&default_config)) { + merge_uln_configs(option::borrow(&default_config), option::borrow(&oapp_config)) + } else if (option::is_some(&default_config)) { + option::extract(&mut default_config) + } else { + abort EEID_NOT_CONFIGURED + } + } + + // =================================================== Send Side ================================================== + + public(friend) fun supports_send_eid(eid: u32): bool { + uln_302_store::has_default_send_uln_config(eid) + } + + public(friend) fun set_default_send_uln_config(eid: u32, config: UlnConfig) { + assert_valid_default_uln_config(&config); + uln_302_store::set_default_send_uln_config(eid, config); + emit(DefaultUlnConfigSet { eid, config, send_or_receive: ULN_SEND_SIDE }); + } + + public(friend) fun set_send_uln_config(oapp: address, eid: u32, config: UlnConfig) { + let default = uln_302_store::get_default_send_uln_config(eid); + assert!(option::is_some(&default), EEID_NOT_CONFIGURED); + assert_valid_uln_config(&config, option::borrow(&default)); + uln_302_store::set_send_uln_config(oapp, eid, config); + emit(UlnConfigSet { oapp, eid, config, send_or_receive: ULN_SEND_SIDE }); + } + + public(friend) fun get_send_uln_config(sender: address, eid: u32): UlnConfig { + let oapp_config = uln_302_store::get_send_uln_config(sender, eid); + let default_config = uln_302_store::get_default_send_uln_config(eid); + + if (option::is_some(&oapp_config) && option::is_some(&default_config)) { + merge_uln_configs(option::borrow(&default_config), option::borrow(&oapp_config)) + } else if (option::is_some(&default_config)) { + option::extract(&mut default_config) + } else { + abort EEID_NOT_CONFIGURED + } + } + + // =================================================== Executor =================================================== + + public(friend) fun get_executor_config(sender: address, eid: u32): ExecutorConfig { + let oapp_config = uln_302_store::get_executor_config(sender, eid); + let default_config = uln_302_store::get_default_executor_config(eid); + + if (option::is_some(&oapp_config) && option::is_some(&default_config)) { + // get correct merged config if oapp and default are both set + merge_executor_configs(option::borrow(&default_config), option::borrow(&oapp_config)) + } else if (option::is_some(&default_config)) { + option::extract(&mut default_config) + } else { + abort EEID_NOT_CONFIGURED + } + } + + public(friend) fun set_default_executor_config(eid: u32, config: ExecutorConfig) { + assert_valid_default_executor_config(&config); + uln_302_store::set_default_executor_config(eid, config); + emit(DefaultExecutorConfigSet { eid, config }); + } + + public(friend) fun set_executor_config(sender: address, eid: u32, config: ExecutorConfig) { + uln_302_store::set_executor_config(sender, eid, config); + emit(ExecutorConfigSet { oapp: sender, eid, config }); + } + + public(friend) fun merge_executor_configs( + default_config: &ExecutorConfig, + oapp_config: &ExecutorConfig, + ): ExecutorConfig { + let default_max_message_size = configs_executor::get_max_message_size(default_config); + let default_executor_address = configs_executor::get_executor_address(default_config); + let oapp_max_message_size = configs_executor::get_max_message_size(oapp_config); + let oapp_executor_address = configs_executor::get_executor_address(oapp_config); + + new_executor_config( + if (oapp_max_message_size > 0) { oapp_max_message_size } else { default_max_message_size }, + if (oapp_executor_address != @0x0) { oapp_executor_address } else { default_executor_address } + ) + } + + public(friend) fun assert_valid_default_executor_config(config: &ExecutorConfig) { + let max_message_size = configs_executor::get_max_message_size(config); + let executor_address = configs_executor::get_executor_address(config); + assert!(max_message_size > 0, EMAX_MESSAGE_SIZE_ZERO); + assert!(executor_address != @0x0, EEXECUTOR_ADDRESS_IS_ZERO); + } + + // =================================================== Internal =================================================== + + /// Merges parts of the oapp config with the default config, where specified + public(friend) fun merge_uln_configs(default_config: &UlnConfig, oapp_config: &UlnConfig): UlnConfig { + let default_for_confirmations = get_use_default_for_confirmations(oapp_config); + let default_for_required = get_use_default_for_required_dvns(oapp_config); + let default_for_optional = get_use_default_for_optional_dvns(oapp_config); + + // handle situations where there the configuration requests a fallback to default + let optional_dvn_threshold = if (!default_for_optional) { + get_optional_dvn_threshold(oapp_config) + } else { + get_optional_dvn_threshold(default_config) + }; + let optional_dvns = if (!default_for_optional) { + get_optional_dvns(oapp_config) + } else { + get_optional_dvns(default_config) + }; + let required_dvns = if (!default_for_required) { + get_required_dvns(oapp_config) + } else { + get_required_dvns(default_config) + }; + + // Check that there is at least one DVN configured. This is also checked on setting the configuration, but this + // could cease to be the case if the default configuration is changed after the OApp configuration is set. + assert!((optional_dvn_threshold as u64) + vector::length(&required_dvns) > 0, EINSUFFICIENT_DVNS_CONFIGURED); + + let confirmations = if (!default_for_confirmations) { + get_confirmations(oapp_config) + } else { + get_confirmations(default_config) + }; + + new_uln_config( + confirmations, + optional_dvn_threshold, + required_dvns, + optional_dvns, + default_for_confirmations, + default_for_required, + default_for_optional, + ) + } + + // ==================================================== Events ==================================================== + + #[event] + struct DefaultUlnConfigSet has store, drop { + eid: u32, + config: UlnConfig, + send_or_receive: u8 + } + + #[event] + struct DefaultExecutorConfigSet has store, drop { + eid: u32, + config: ExecutorConfig, + } + + #[event] + struct UlnConfigSet has store, drop { + oapp: address, + eid: u32, + config: UlnConfig, + send_or_receive: u8 + } + + #[event] + struct ExecutorConfigSet has store, drop { + oapp: address, + eid: u32, + config: ExecutorConfig, + } + + #[test_only] + public fun default_uln_config_set_event(eid: u32, config: UlnConfig, send_or_receive: u8): DefaultUlnConfigSet { + DefaultUlnConfigSet { eid, config, send_or_receive } + } + + #[test_only] + public fun default_executor_config_set_event(eid: u32, config: ExecutorConfig): DefaultExecutorConfigSet { + DefaultExecutorConfigSet { eid, config } + } + + #[test_only] + public fun uln_config_set_event(oapp: address, eid: u32, config: UlnConfig, send_or_receive: u8): UlnConfigSet { + UlnConfigSet { oapp, eid, config, send_or_receive } + } + + #[test_only] + public fun executor_config_set_event(oapp: address, eid: u32, config: ExecutorConfig): ExecutorConfigSet { + ExecutorConfigSet { oapp, eid, config } + } + + // ================================================== Error Codes ================================================= + + const EEID_NOT_CONFIGURED: u64 = 1; + const EEXECUTOR_ADDRESS_IS_ZERO: u64 = 2; + const EINVALID_CONFIG_LENGTH: u64 = 3; + const EINSUFFICIENT_DVNS_CONFIGURED: u64 = 4; + const EMAX_MESSAGE_SIZE_ZERO: u64 = 5; + const EUNKNOWN_CONFIG_TYPE: u64 = 6; +} diff --git a/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/sources/internal/for_each_dvn.move b/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/sources/internal/for_each_dvn.move new file mode 100644 index 00000000..0a4859ab --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/sources/internal/for_each_dvn.move @@ -0,0 +1,30 @@ +module uln_302::for_each_dvn { + use std::vector; + + friend uln_302::verification; + friend uln_302::sending; + + #[test_only] + friend uln_302::for_each_dvn_tests; + + /// Loop through each dvn and provide the dvn and the dvn index + /// If a required dvn is duplicated as an optional dvn, it will be called twice + public(friend) inline fun for_each_dvn( + required_dvns: &vector
, + optional_dvns: &vector
, + f: |address, u64|(), + ) { + let count_required = vector::length(required_dvns); + let count_optional = vector::length(optional_dvns); + + for (i in 0..(count_required + count_optional)) { + let dvn = if (i < count_required) { + vector::borrow(required_dvns, i) + } else { + vector::borrow(optional_dvns, i - count_required) + }; + + f(*dvn, i) + } + } +} diff --git a/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/sources/internal/sending.move b/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/sources/internal/sending.move new file mode 100644 index 00000000..a460107f --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/sources/internal/sending.move @@ -0,0 +1,506 @@ +module uln_302::sending { + use std::event; + use std::fungible_asset::{Self, FungibleAsset}; + use std::option::{Self, Option}; + use std::primary_fungible_store; + use std::vector; + + use dvn_fee_lib_router_0::dvn_fee_lib_router::get_dvn_fee as fee_lib_router_get_dvn_fee; + use endpoint_v2_common::bytes32::{Self, Bytes32}; + use endpoint_v2_common::packet_raw::{Self, RawPacket}; + use endpoint_v2_common::packet_v1_codec; + use endpoint_v2_common::send_packet::{Self, SendPacket, unpack_send_packet}; + use executor_fee_lib_router_0::executor_fee_lib_router::get_executor_fee as fee_lib_router_get_executor_fee; + use msglib_types::configs_executor; + use msglib_types::configs_uln::{Self, unpack_uln_config}; + use msglib_types::worker_options::get_matching_options; + use treasury::treasury; + use uln_302::configuration; + use uln_302::for_each_dvn::for_each_dvn; + use uln_302::msglib::worker_config_for_fee_lib_routing_opt_in; + use worker_common::worker_config; + + friend uln_302::router_calls; + + #[test_only] + friend uln_302::sending_tests; + + // ==================================================== Sending =================================================== + + /// Makes payment to workers and triggers the packet send + public(friend) fun send( + packet: SendPacket, + options: vector, + native_token: &mut FungibleAsset, + zro_token: &mut Option, + ): (u64, u64, RawPacket) { + let dst_eid = send_packet::get_dst_eid(&packet); + let sender = bytes32::to_address(send_packet::get_sender(&packet)); + let message_length = send_packet::get_message_length(&packet); + send_internal( + packet, + options, + native_token, + zro_token, + // Get Executor Fee + |executor_address, executor_options| fee_lib_router_get_executor_fee( + @uln_302, + get_worker_fee_lib(executor_address), + executor_address, + dst_eid, + sender, + message_length, + executor_options, + ), + // Get DVN Fee + |dvn, confirmations, dvn_options, packet_header, payload_hash| fee_lib_router_get_dvn_fee( + @uln_302, + get_worker_fee_lib(dvn), + dvn, + dst_eid, + sender, + packet_raw::get_packet_bytes(packet_header), + bytes32::from_bytes32(payload_hash), + confirmations, + dvn_options, + ) + ) + } + + /// Internal function to send a packet. The fee library function calls are injected as parameters so that this + /// behavior can be mocked in tests + /// + /// @param packet: the packet details that will be sent + /// @param options: combined executor and DVN options for the packet + /// @param native_token: the native token fungible asset mutable ref to pay the workers + /// @param zro_token: optional ZRO token fungible asset mutable ref to pay the workers + /// @param get_executor_fee: function that gets the fee for the executor + /// |executor, executor_options| (fee, deposit_address) + /// @param get_dvn_fee: function that gets the fee for one DVN + /// |dvn, confirmations, dvn_option, packet_header, payload_hash| (fee, deposit_address) + public(friend) inline fun send_internal( + packet: SendPacket, + options: vector, + native_token: &mut FungibleAsset, + zro_token: &mut Option, + get_executor_fee: |address, vector| (u64, address), + get_dvn_fee: |address, u64, vector, RawPacket, Bytes32| (u64, address), + ): (u64 /*native*/, u64 /*zro*/, RawPacket /*encoded_packet*/) { + // Convert the Send Packet in the a Packet of the Codec V1 format + let (nonce, src_eid, sender, dst_eid, receiver, guid, message) = unpack_send_packet(packet); + let sender_address = bytes32::to_address(sender); + let raw_packet = packet_v1_codec::new_packet_v1( + src_eid, + sender, + dst_eid, + receiver, + nonce, + guid, + message, + ); + + // Calculate and Pay Worker Fees (DVNs and Executor) + let packet_header = packet_v1_codec::extract_header(&raw_packet); + let payload_hash = packet_v1_codec::get_payload_hash(&raw_packet); + let message_length = vector::length(&message); + let worker_native_fee = pay_workers( + sender_address, + dst_eid, + message_length, + options, + native_token, + // Get Executor Fee + |executor, executor_options| get_executor_fee(executor, executor_options), + // Get DVN Fee - Partially apply the parameters that are known in this scope + |dvn, confirmations, dvn_options| get_dvn_fee( + dvn, + confirmations, + dvn_options, + packet_header, + payload_hash, + ), + ); + + // Calculate and Pay Treasury Fee + let treasury_zro_fee = 0; + let treasury_native_fee = 0; + if (option::is_some(zro_token)) { + treasury_zro_fee = treasury::pay_fee(worker_native_fee, option::borrow_mut(zro_token)); + } else { + treasury_native_fee = treasury::pay_fee(worker_native_fee, native_token); + }; + + // Return the total Native and ZRO paid + let total_native_fee = worker_native_fee + treasury_native_fee; + (total_native_fee, treasury_zro_fee, raw_packet) + } + + // ==================================================== Quoting =================================================== + + // Quote the price of a send in native and ZRO tokens (if `pay_in_zro` is true) + public(friend) fun quote(packet: SendPacket, options: vector, pay_in_zro: bool): (u64, u64) { + let dst_eid = send_packet::get_dst_eid(&packet); + let sender = bytes32::to_address(send_packet::get_sender(&packet)); + let message_length = send_packet::get_message_length(&packet); + + quote_internal( + packet, + options, + pay_in_zro, + // Get Executor Fee + |executor_address, executor_options| fee_lib_router_get_executor_fee( + @uln_302, + get_worker_fee_lib(executor_address), + executor_address, + dst_eid, + sender, + message_length, + executor_options, + ), + // Get DVN Fee + |dvn, confirmations, dvn_options, packet_header, payload_hash| fee_lib_router_get_dvn_fee( + @uln_302, + get_worker_fee_lib(dvn), + dvn, + dst_eid, + sender, + packet_raw::get_packet_bytes(packet_header), + bytes32::from_bytes32(payload_hash), + confirmations, + dvn_options, + ) + ) + } + + /// Provides a quote for sending a packet + /// + /// @param packet: the packet to be sent + /// @param options: combined executor and DVN options for the packet + /// @param pay_in_zro: whether the fees should be paid in ZRO or native token + /// @param get_executor_fee: function that gets the fee for the executor + /// |executor, executor_option| (fee, deposit_address) + /// @param get_dvn_fee function: that gets the fee for one DVN + /// |dvn, confirmations, dvn_option, packet_header, payload_hash| (fee, deposit_address) + public(friend) inline fun quote_internal( + packet: SendPacket, + options: vector, + pay_in_zro: bool, + get_executor_fee: |address, vector| (u64, address), + get_dvn_fee: |address, u64, vector, RawPacket, Bytes32| (u64, address), + ): (u64, u64) { + let (nonce, src_eid, sender, dst_eid, receiver, guid, message) = unpack_send_packet(packet); + let sender_address = bytes32::to_address(sender); + + // Extract the packet header and payload hash + let packet_header = packet_v1_codec::new_packet_v1_header_only( + src_eid, + sender, + dst_eid, + receiver, + nonce, + ); + + // Get the Executor Configuration + let executor_config = uln_302::configuration::get_executor_config(sender_address, dst_eid); + let executor_address = configs_executor::get_executor_address(&executor_config); + let max_msg_size = configs_executor::get_max_message_size(&executor_config); + + // Split provided options into executor and DVN options + let (executor_options, all_dvn_options) = msglib_types::worker_options::extract_and_split_options(&options); + + // Assert message size is less than what is configured for the OApp + assert!(vector::length(&message) <= (max_msg_size as u64), err_EMESSAGE_SIZE_EXCEEDS_MAX()); + + // Get Executor Fee + let (executor_fee, _deposit_address) = get_executor_fee(executor_address, executor_options); + + // Get Fee for all DVNs + let payload_hash = packet_v1_codec::compute_payload_hash(guid, message); + let config = configuration::get_send_uln_config(sender_address, dst_eid); + let (confirmations, _, required_dvns, optional_dvns, _, _, _) = unpack_uln_config(config); + let (dvn_fee, _dvn_fees, _dvn_deposit_addresses) = get_all_dvn_fees( + &required_dvns, + &optional_dvns, + all_dvn_options, + // Get DVN Fee - Partially apply the parameters that are known in this scope + |dvn, dvn_options| get_dvn_fee( + dvn, + confirmations, + dvn_options, + packet_header, + payload_hash, + ), + ); + + let worker_native_fee = executor_fee + dvn_fee; + + // Calculate Treasury Fee + let treasury_fee = treasury::get_fee(worker_native_fee, pay_in_zro); + let treasury_zro_fee = if (pay_in_zro) { treasury_fee } else { 0 }; + let treasury_native_fee = if (!pay_in_zro) { treasury_fee } else { 0 }; + + // Return Native and ZRO Fees + let total_native_fee = worker_native_fee + treasury_native_fee; + (total_native_fee, treasury_zro_fee) + } + + // ==================================================== DVN Fees ================================================== + + /// Calculates the fees for all the DVNs + /// + /// @param required_dvns: list of required DVNs + /// @param optional_dvns: list of optional DVNs + /// @param validation_options: concatinated options for all DVNs + /// @param get_dvn_fee: function that gets the fee for one DVN + /// |dvn, dvn_option| (u64, address) + /// @return (total_fee, dvn_fees, dvn_deposit_addresses) + public(friend) inline fun get_all_dvn_fees( + required_dvns: &vector
, + optional_dvns: &vector
, + validation_options: vector, // options for all dvns, + get_dvn_fee: |address, vector| (u64, address), + ): (u64, vector, vector
) { + let index_option_pairs = msglib_types::worker_options::group_dvn_options_by_index(&validation_options); + + // Calculate the fee for each DVN and accumulate total DVN fee + let total_dvn_fee = 0; + let dvn_fees = vector[]; + let dvn_deposit_addresses = vector
[]; + + for_each_dvn(required_dvns, optional_dvns, |dvn, i| { + let options = get_matching_options(&index_option_pairs, (i as u8)); + let (fee, deposit_address) = get_dvn_fee(dvn, options); + vector::push_back(&mut dvn_fees, fee); + vector::push_back(&mut dvn_deposit_addresses, deposit_address); + total_dvn_fee = total_dvn_fee + fee; + }); + + (total_dvn_fee, dvn_fees, dvn_deposit_addresses) + } + + // ==================================================== Payment =================================================== + + /// Pay the workers for the message + /// + /// @param sender the address of the sender + /// @param dst_eid the destination endpoint id + /// @param message_length the length of the message + /// @param options the options for the workers + /// @param native_token the native token fungible asset to pay the workers + /// @param get_executor_fee function that gets the fee for the Executor + /// |executor, executor_option| (fee, deposit_address) + /// @param get_dvn_fee function that gets the fee for one DVN + /// |dvn, confirmations, dvn_options| (fee, deposit_address) + public(friend) inline fun pay_workers( + sender: address, + dst_eid: u32, + message_length: u64, + options: vector, + native_token: &mut FungibleAsset, + get_executor_fee: |address, vector| (u64, address), + get_dvn_fee: |address, u64, vector| (u64, address), + ): u64 /*native fee*/ { + // Split serialized options into separately concatenated executor options and dvn options + let (executor_options, dvn_options) = msglib_types::worker_options::extract_and_split_options(&options); + + // Pay Executor + let executor_native_fee = pay_executor_and_assert_size( + sender, + native_token, + dst_eid, + message_length, + executor_options, + // Get Executor Fee + |executor, options| get_executor_fee(executor, options), + ); + + // Pay Verifier + let verifier_native_fee = pay_verifier( + sender, + dst_eid, + native_token, + dvn_options, + // Get DVN Fee + |dvn, confirmations, options| get_dvn_fee(dvn, confirmations, options) + ); + + let total_native_fee = executor_native_fee + verifier_native_fee; + total_native_fee + } + + /// Pay the executor for the message and check the size against the configured maximum + /// + /// @param sender: the address of the sender + /// @param native_token: the native token fungible asset to pay the executor + /// @param dst_eid: the destination endpoint id + /// @param message_length: the length of the message + /// @param executor_options: the options for the executor + /// @param get_executor_fee: function that gets the fee for the Executor + /// |executor, executor_option| (fee, deposit_address) + /// @return the fee paid to the executor + inline fun pay_executor_and_assert_size( + sender: address, + native_token: &mut FungibleAsset, + dst_eid: u32, + message_length: u64, + executor_options: vector, + get_executor_fee: |address, vector| (u64, address), + ): u64 { + // Load config + let executor_config = configuration::get_executor_config(sender, dst_eid); + let max_msg_size = configs_executor::get_max_message_size(&executor_config); + let executor_address = configs_executor::get_executor_address(&executor_config); + + // Assert message size is less than what is configured for the OApp + assert!(message_length <= (max_msg_size as u64), err_EMESSAGE_SIZE_EXCEEDS_MAX()); + + // Extract and Deposit the Executor Fee + let (fee, deposit_address) = get_executor_fee(executor_address, executor_options); + let fee_fa = fungible_asset::extract(native_token, fee); + primary_fungible_store::deposit(deposit_address, fee_fa); + + // Emit a record of the fee paid to the executor + emit_executor_fee_paid(executor_address, deposit_address, fee); + + fee + } + + /// Pay the DVNs for verification + /// Emits a DvnFeePaidEvent to notify the DVNs that they have been paid + /// + /// @param sender: the address of the sender + /// @param dst_eid: the destination endpoint id + /// @param native_token: the native token fungible asset to pay the DVNs + /// @param dvn_options: the options for the DVNs + /// @param get_dvn_fee: function that gets the dvn fee for one DVN + /// |dvn, confirmations, dvn_option| (fee, deposit_address) + /// @return the total fee paid to all DVNs + inline fun pay_verifier( + sender: address, + dst_eid: u32, + native_token: &mut FungibleAsset, + dvn_options: vector, + get_dvn_fee: |address, u64, vector| (u64, address), + ): u64 /*native_fee*/ { + let config = configuration::get_send_uln_config(sender, dst_eid); + let required_dvns = configs_uln::get_required_dvns(&config); + let optional_dvns = configs_uln::get_optional_dvns(&config); + let confirmations = configs_uln::get_confirmations(&config); + + let (total_fee, dvn_fees, dvn_deposit_addresses) = get_all_dvn_fees( + &required_dvns, + &optional_dvns, + dvn_options, + // Get DVN Fee - Partially apply the parameters that are known in this scope + |dvn, options| get_dvn_fee( + dvn, + confirmations, + options, + ), + ); + + // Deposit the appropriate fee into each of the DVN deposit addresses + for_each_dvn(&required_dvns, &optional_dvns, |_dvn, i| { + let fee = vector::borrow(&dvn_fees, i); + let deposit_address = *vector::borrow(&dvn_deposit_addresses, i); + let fee_fa = fungible_asset::extract(native_token, *fee); + primary_fungible_store::deposit(deposit_address, fee_fa); + }); + + // Emit a record of the fees paid to the DVNs + emit_dvn_fee_paid(required_dvns, optional_dvns, dvn_fees, dvn_deposit_addresses); + + total_fee + } + + // ==================================================== Routing =================================================== + + /// Provides the feelib address for a worker + /// Defer to worker_common::worker_config if opting in to worker config. Otherwise, use the worker itself. + fun get_worker_fee_lib(worker: address): address { + if (worker_config_for_fee_lib_routing_opt_in(worker)) { + worker_config::get_worker_fee_lib(worker) + } else { + worker + } + } + + // ==================================================== Events ==================================================== + + #[event] + struct DvnFeePaid has store, copy, drop { + required_dvns: vector
, + optional_dvns: vector
, + dvn_deposit_addresses: vector
, + dvn_fees: vector, + } + + #[event] + struct ExecutorFeePaid has store, copy, drop { + executor_address: address, + executor_deposit_address: address, + executor_fee: u64, + } + + public(friend) fun emit_executor_fee_paid( + executor_address: address, + executor_deposit_address: address, + executor_fee: u64, + ) { + event::emit(ExecutorFeePaid { + executor_address, + executor_deposit_address, + executor_fee, + }); + } + + public(friend) fun emit_dvn_fee_paid( + required_dvns: vector
, + optional_dvns: vector
, + dvn_fees: vector, + dvn_deposit_addresses: vector
, + ) { + event::emit(DvnFeePaid { + required_dvns, + optional_dvns, + dvn_fees, + dvn_deposit_addresses, + }); + } + + #[test_only] + public fun dvn_fee_paid_event( + required_dvns: vector
, + optional_dvns: vector
, + dvn_fees: vector, + dvn_deposit_addresses: vector
, + ): DvnFeePaid { + DvnFeePaid { + required_dvns, + optional_dvns, + dvn_fees, + dvn_deposit_addresses, + } + } + + #[test_only] + public fun executor_fee_paid_event( + executor_address: address, + executor_deposit_address: address, + executor_fee: u64, + ): ExecutorFeePaid { + ExecutorFeePaid { + executor_address, + executor_deposit_address, + executor_fee, + } + } + + // ================================================== Error Codes ================================================= + + const EMESSAGE_SIZE_EXCEEDS_MAX: u64 = 1; + + public(friend) fun err_EMESSAGE_SIZE_EXCEEDS_MAX(): u64 { + EMESSAGE_SIZE_EXCEEDS_MAX + } +} \ No newline at end of file diff --git a/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/sources/internal/uln_302_store.move b/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/sources/internal/uln_302_store.move new file mode 100644 index 00000000..0a634dff --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/sources/internal/uln_302_store.move @@ -0,0 +1,270 @@ +module uln_302::uln_302_store { + use std::option::{Self, Option}; + use std::table::{Self, Table}; + + use endpoint_v2_common::bytes32::Bytes32; + use msglib_types::configs_executor::ExecutorConfig; + use msglib_types::configs_uln::UlnConfig; + + friend uln_302::configuration; + friend uln_302::verification; + friend uln_302::msglib; + + #[test_only] + friend uln_302::configuration_tests; + + #[test_only] + friend uln_302::verification_tests; + + #[test_only] + friend uln_302::sending_tests; + + struct Store has key { + default_configs: Table, + oapp_configs: Table, + confirmations: Table, + worker_config_opt_in_out: Table, + } + + struct OAppEid has store, drop, copy { + oapp: address, + eid: u32, + } + + struct DefaultConfig has store, drop, copy { + executor_config: Option, + receive_uln_config: Option, + send_uln_config: Option, + } + + struct OAppConfig has store, drop, copy { + executor_config: Option, + receive_uln_config: Option, + send_uln_config: Option, + } + + struct ConfirmationsKey has store, drop, copy { + header_hash: Bytes32, + payload_hash: Bytes32, + dvn_address: address, + } + + fun init_module(account: &signer) { + move_to(account, Store { + default_configs: table::new(), + oapp_configs: table::new(), + confirmations: table::new(), + worker_config_opt_in_out: table::new(), + }); + } + + // ============================================= General Configuration ============================================ + + // the following functions are used to facilitate working with a nested data structure in the store. The + // initialization of certain table fields does not have a mapping to an exposed initialization process and are + // strictly implementation details. + + #[test_only] + // Initializes the module for testing purposes + public(friend) fun init_module_for_test() { + let admin = &std::account::create_signer_for_test(@uln_302); + init_module(admin); + } + + /// Internal function that checks whether the default config table is initialized for a given EID + fun is_eid_default_config_table_initialized(eid: u32): bool acquires Store { + table::contains(&store().default_configs, eid) + } + + /// Internal function that checks whether the oapp config table is initialized for a given EID and OApp + fun is_eid_oapp_config_table_initialized(eid: u32, oapp: address): bool acquires Store { + table::contains(&store().oapp_configs, OAppEid { oapp, eid }) + } + + /// Internal function that initializes the default config table for a given EID if it is not already + fun ensure_eid_default_config_table_initialized(eid: u32) acquires Store { + if (!is_eid_default_config_table_initialized(eid)) { + table::add(&mut store_mut().default_configs, eid, DefaultConfig { + executor_config: option::none(), + receive_uln_config: option::none(), + send_uln_config: option::none(), + }); + } + } + + /// Internal function that initializes the oapp config table for a given EID and OApp if it is not already + fun ensure_oapp_config_table_initialized(eid: u32, oapp: address) acquires Store { + if (!is_eid_oapp_config_table_initialized(eid, oapp)) { + table::add(&mut store_mut().oapp_configs, OAppEid { eid, oapp }, OAppConfig { + executor_config: option::none(), + receive_uln_config: option::none(), + send_uln_config: option::none(), + }); + } + } + + // ================================================= Worker Config ================================================ + + + public(friend) fun set_worker_config_for_fee_lib_routing_opt_in(worker: address, opt_in_out: bool) acquires Store { + table::upsert(&mut store_mut().worker_config_opt_in_out, worker, opt_in_out) + } + + public(friend) fun get_worker_config_for_fee_lib_routing_opt_in(worker: address): bool acquires Store { + *table::borrow_with_default(&store().worker_config_opt_in_out, worker, &false) + } + + // ================================================ Send ULN Config =============================================== + + public(friend) fun has_default_send_uln_config(dst_eid: u32): bool acquires Store { + is_eid_default_config_table_initialized(dst_eid) && + option::is_some(&default_configs(dst_eid).send_uln_config) + } + + /// Gets the default send configuration for a given EID + public(friend) fun get_default_send_uln_config(dst_eid: u32): Option acquires Store { + if (!is_eid_default_config_table_initialized(dst_eid)) { return option::none() }; + default_configs(dst_eid).send_uln_config + } + + /// Sets the default send configuration for a given EID. This is a raw setter and should not be used directly + public(friend) fun set_default_send_uln_config(eid: u32, config: UlnConfig) acquires Store { + ensure_eid_default_config_table_initialized(eid); + default_configs_mut(eid).send_uln_config = option::some(config); + } + + /// Sets the send configuration for a given EID. This is a raw setter and should not be used directly + public(friend) fun set_send_uln_config(oapp_address: address, eid: u32, config: UlnConfig) acquires Store { + ensure_oapp_config_table_initialized(eid, oapp_address); + oapp_configs_mut(eid, oapp_address).send_uln_config = option::some(config); + } + + /// Gets the oapp send configuration for an EID if it is set. This is a raw getter and should not be used directly + public(friend) fun get_send_uln_config(sender: address, dst_eid: u32): Option acquires Store { + if (!is_eid_oapp_config_table_initialized(dst_eid, sender)) { return option::none() }; + oapp_configs(dst_eid, sender).send_uln_config + } + + // ============================================== Receive ULN Config ============================================== + + public(friend) fun has_default_receive_uln_config(eid: u32): bool acquires Store { + is_eid_default_config_table_initialized(eid) && + option::is_some(&default_configs(eid).receive_uln_config) + } + + /// Gets the default receive configuration for a given EID + public(friend) fun get_default_receive_uln_config(eid: u32): Option acquires Store { + if (!is_eid_default_config_table_initialized(eid)) { return option::none() }; + default_configs(eid).receive_uln_config + } + + /// Sets the default receive configuration for a given EID. This is a raw setter and should not be used directly + public(friend) fun set_default_receive_uln_config(eid: u32, config: UlnConfig) acquires Store { + ensure_eid_default_config_table_initialized(eid); + default_configs_mut(eid).receive_uln_config = option::some(config); + } + + /// Gets the oapp receive configuration for an EID if it is set. This is a raw getter and should not be used + /// directly + public(friend) fun get_receive_uln_config(oapp_address: address, eid: u32): Option acquires Store { + if (!is_eid_oapp_config_table_initialized(eid, oapp_address)) { return option::none() }; + oapp_configs(eid, oapp_address).receive_uln_config + } + + /// Sets the oapp receive configuration for an EID. This is a raw setter and should not be used directly + public(friend) fun set_receive_uln_config(oapp: address, eid: u32, config: UlnConfig) acquires Store { + ensure_oapp_config_table_initialized(eid, oapp); + oapp_configs_mut(eid, oapp).receive_uln_config = option::some(config); + } + + // ================================================ Executor Config =============================================== + + /// Gets the default executor configuration for a given EID + public(friend) fun get_default_executor_config(eid: u32): Option acquires Store { + if (!is_eid_default_config_table_initialized(eid)) { return option::none() }; + default_configs(eid).executor_config + } + + /// Sets the default executor configuration for a given EID. This is a raw setter and should not be used directly + public(friend) fun set_default_executor_config(eid: u32, config: ExecutorConfig) acquires Store { + ensure_eid_default_config_table_initialized(eid); + default_configs_mut(eid).executor_config = option::some(config); + } + + /// Gets the oapp executor configuration for an EID if it is set. This is a raw getter and should not be used + public(friend) fun get_executor_config(sender: address, eid: u32): Option acquires Store { + if (!is_eid_oapp_config_table_initialized(eid, sender)) { return option::none() }; + oapp_configs(eid, sender).executor_config + } + + /// Sets the oapp executor configuration for an EID. This is a raw setter and should not be used directly + public(friend) fun set_executor_config(sender: address, eid: u32, config: ExecutorConfig) acquires Store { + ensure_oapp_config_table_initialized(eid, sender); + oapp_configs_mut(eid, sender).executor_config = option::some(config); + } + + // ============================================== Receive Side State ============================================== + + /// Checks if a message has received confirmations from a given DVN + public(friend) fun has_verification_confirmations( + header_hash: Bytes32, payload_hash: Bytes32, dvn_address: address, + ): bool acquires Store { + table::contains(confirmations(), ConfirmationsKey { header_hash, payload_hash, dvn_address }) + } + + /// Gets the number of verification confirmations received for a message from a given DVN + public(friend) fun get_verification_confirmations( + header_hash: Bytes32, payload_hash: Bytes32, dvn_address: address, + ): u64 acquires Store { + *table::borrow(confirmations(), ConfirmationsKey { header_hash, payload_hash, dvn_address }) + } + + /// Sets the number of verification confirmations received for a message from a given DVN. This is a raw setter and + /// should not be used directly + public(friend) fun set_verification_confirmations( + header_hash: Bytes32, payload_hash: Bytes32, dvn_address: address, confirmations: u64, + ) acquires Store { + table::upsert( + confirmations_mut(), + ConfirmationsKey { header_hash, payload_hash, dvn_address }, + confirmations, + ) + } + + /// Removes the verification confirmations for a message from a given DVN. This is a raw setter and should not be + /// used directly + public(friend) fun remove_verification_confirmations( + header_hash: Bytes32, payload_hash: Bytes32, dvn_address: address, + ): u64 acquires Store { + table::remove(confirmations_mut(), ConfirmationsKey { header_hash, payload_hash, dvn_address }) + } + + // ==================================================== Helpers =================================================== + + inline fun store(): &Store { borrow_global(@uln_302) } + + inline fun store_mut(): &mut Store { borrow_global_mut(@uln_302) } + + inline fun default_configs(eid: u32): &DefaultConfig { table::borrow(&store().default_configs, eid) } + + inline fun default_configs_mut(eid: u32): &mut DefaultConfig { + table::borrow_mut(&mut store_mut().default_configs, eid) + } + + inline fun oapp_configs(eid: u32, oapp: address): &OAppConfig { + table::borrow(&store().oapp_configs, OAppEid { oapp, eid }) + } + + inline fun oapp_configs_mut(eid: u32, oapp: address): &mut OAppConfig { + table::borrow_mut(&mut store_mut().oapp_configs, OAppEid { oapp, eid }) + } + + inline fun confirmations(): &Table { &store().confirmations } + + inline fun confirmations_mut(): &mut Table { &mut store_mut().confirmations } + + // ================================================== Error Codes ================================================= + + const EALREADY_SET: u64 = 1; + const EINVALID_INPUT: u64 = 2; +} diff --git a/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/sources/internal/verification.move b/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/sources/internal/verification.move new file mode 100644 index 00000000..b815c94e --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/sources/internal/verification.move @@ -0,0 +1,168 @@ +module uln_302::verification { + use std::event::emit; + use std::vector; + + use endpoint_v2_common::bytes32::{Self, Bytes32, from_bytes32}; + use endpoint_v2_common::packet_raw::{get_packet_bytes, RawPacket}; + use endpoint_v2_common::packet_v1_codec::{Self, assert_receive_header}; + use endpoint_v2_common::universal_config; + use msglib_types::configs_uln::{Self, UlnConfig}; + use uln_302::configuration; + use uln_302::for_each_dvn::for_each_dvn; + use uln_302::uln_302_store; + + friend uln_302::msglib; + friend uln_302::router_calls; + + #[test_only] + friend uln_302::verification_tests; + + // ================================================= Verification ================================================= + + /// Initiated from the DVN to give their verification of a payload as well as the number of confirmations + public(friend) fun verify( + dvn_address: address, + packet_header: RawPacket, + payload_hash: Bytes32, + confirmations: u64, + ) { + let packet_header_bytes = get_packet_bytes(packet_header); + let header_hash = bytes32::keccak256(packet_header_bytes); + uln_302_store::set_verification_confirmations(header_hash, payload_hash, dvn_address, confirmations); + emit(PayloadVerified { + dvn: dvn_address, + header: packet_header_bytes, + confirmations, + proof_hash: from_bytes32(payload_hash), + }); + } + + /// This checks if a message is verifiable by a sufficient group of DVNs to meet the requirements + public(friend) fun check_verifiable(config: &UlnConfig, header_hash: Bytes32, payload_hash: Bytes32): bool { + let required_dvn_count = configs_uln::get_required_dvn_count(config); + let optional_dvn_count = configs_uln::get_optional_dvn_count(config); + let optional_dvn_threshold = configs_uln::get_optional_dvn_threshold(config); + let required_confirmations = configs_uln::get_confirmations(config); + + if (required_dvn_count > 0) { + let required_dvns = configs_uln::get_required_dvns(config); + for (i in 0..required_dvn_count) { + let dvn = vector::borrow(&required_dvns, i); + if (!is_verified(*dvn, header_hash, payload_hash, required_confirmations)) { + return false + }; + }; + if (optional_dvn_threshold == 0) { + return true + }; + }; + + let count_optional_dvns_verified = 0; + let optional_dvns = configs_uln::get_optional_dvns(config); + for (i in 0..optional_dvn_count) { + let dvn = vector::borrow(&optional_dvns, i); + if (is_verified(*dvn, header_hash, payload_hash, required_confirmations)) { + count_optional_dvns_verified = count_optional_dvns_verified + 1; + if (count_optional_dvns_verified >= optional_dvn_threshold) { + return true + }; + }; + }; + + return false + } + + /// This checks if a DVN has verified a message with at least the required number of confirmations + public(friend) fun is_verified( + dvn_address: address, + header_hash: Bytes32, + payload_hash: Bytes32, + required_confirmations: u64, + ): bool { + if (!uln_302_store::has_verification_confirmations(header_hash, payload_hash, dvn_address)) { return false }; + let confirmations = uln_302_store::get_verification_confirmations(header_hash, payload_hash, dvn_address); + confirmations >= required_confirmations + } + + + /// Commits the verification of a payload + /// This uses the contents of a serialized packet header to then assert the complete verification of a payload and + /// then clear the storage related to it + public(friend) fun commit_verification( + packet_header: RawPacket, + payload_hash: Bytes32, + ): (address, u32, Bytes32, u64) { + assert_receive_header(&packet_header, universal_config::eid()); + verify_and_reclaim_storage( + &get_receive_uln_config_from_packet_header(&packet_header), + bytes32::keccak256(get_packet_bytes(packet_header)), + payload_hash, + ); + + // decode the header + let receiver = bytes32::to_address(packet_v1_codec::get_receiver(&packet_header)); + let src_eid = packet_v1_codec::get_src_eid(&packet_header); + let sender = packet_v1_codec::get_sender(&packet_header); + let nonce = packet_v1_codec::get_nonce(&packet_header); + (receiver, src_eid, sender, nonce) + } + + public(friend) fun get_receive_uln_config_from_packet_header(packet_header: &RawPacket): UlnConfig { + let src_eid = packet_v1_codec::get_src_eid(packet_header); + let oapp_address = bytes32::to_address(packet_v1_codec::get_receiver(packet_header)); + configuration::get_receive_uln_config(oapp_address, src_eid) + } + + /// If the message is verifiable, this function will remove the confirmations from the store + /// It will abort if the message is not verifiable + public(friend) fun verify_and_reclaim_storage(config: &UlnConfig, header_hash: Bytes32, payload_hash: Bytes32) { + assert!(check_verifiable(config, header_hash, payload_hash), ENOT_VERIFIABLE); + + reclaim_storage( + &configs_uln::get_required_dvns(config), + &configs_uln::get_optional_dvns(config), + header_hash, + payload_hash, + ); + } + + + /// Loop through the DVN addresses and clear confirmations for the given header and payload hash + public(friend) fun reclaim_storage( + required_dvns: &vector
, + optional_dvns: &vector
, + header_hash: Bytes32, + payload_hash: Bytes32, + ) { + for_each_dvn(required_dvns, optional_dvns, |dvn, _idx| { + if (uln_302_store::has_verification_confirmations(header_hash, payload_hash, dvn)) { + uln_302_store::remove_verification_confirmations(header_hash, payload_hash, dvn); + }; + }); + } + + // ==================================================== Events ==================================================== + + #[event] + struct PayloadVerified has drop, copy, store { + dvn: address, + header: vector, + confirmations: u64, + proof_hash: vector, + } + + #[test_only] + public fun payload_verified_event( + dvn: address, + header: vector, + confirmations: u64, + proof_hash: vector, + ): PayloadVerified { + PayloadVerified { dvn, header, confirmations, proof_hash } + } + + // ================================================== Error Codes ================================================= + + const ENO_DVNS: u64 = 1; + const ENOT_VERIFIABLE: u64 = 2; +} diff --git a/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/sources/msglib.move b/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/sources/msglib.move new file mode 100644 index 00000000..98d48d05 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/sources/msglib.move @@ -0,0 +1,145 @@ +/// View functions for the ULN-302 module +module uln_302::msglib { + use std::event::emit; + use std::option::Option; + use std::signer::address_of; + + use endpoint_v2_common::bytes32; + use endpoint_v2_common::packet_raw::bytes_to_raw_packet; + use endpoint_v2_common::packet_v1_codec::{Self, is_receive_header_valid}; + use endpoint_v2_common::universal_config; + use msglib_types::configs_executor::ExecutorConfig; + use msglib_types::configs_uln::UlnConfig; + use uln_302::configuration; + use uln_302::uln_302_store; + use uln_302::verification; + + #[test_only] + /// Initializes the uln for testing + public fun initialize_for_test() { + uln_302_store::init_module_for_test(); + } + + #[view] + /// Checks that a packet is verifiable and has received verification of the required confirmations from the needed + /// set of DVNs + public fun verifiable( + packet_header_bytes: vector, + payload_hash: vector, + ): bool { + let packet_header = bytes_to_raw_packet(packet_header_bytes); + let src_eid = packet_v1_codec::get_src_eid(&packet_header); + let receiver = bytes32::to_address( + packet_v1_codec::get_receiver(&packet_header) + ); + + is_receive_header_valid(&packet_header, universal_config::eid()) && + verification::check_verifiable( + &configuration::get_receive_uln_config(receiver, src_eid), + bytes32::keccak256(packet_header_bytes), + bytes32::to_bytes32(payload_hash) + ) + } + + #[view] + /// Gets the treasury address + public fun get_treasury(): address { + @treasury + } + + #[view] + /// Gets the default send ULN config for an EID + /// + /// This will return option::none() if it is not set. + public fun get_default_uln_send_config(eid: u32): Option { + uln_302_store::get_default_send_uln_config(eid) + } + + #[view] + /// Gets the default receive ULN config for an EID + /// + /// This will return option::none() if it is not set. + public fun get_default_uln_receive_config(eid: u32): Option { + uln_302_store::get_default_receive_uln_config(eid) + } + + #[view] + /// Gets the default executor config for an EID + /// + /// This will return option::none() if it is not set. + public fun get_default_executor_config(eid: u32): Option { + uln_302_store::get_default_executor_config(eid) + } + + #[view] + /// Gets the send ULN config for an OApp and EID + /// + /// This will return option::none() if it is not set. + public fun get_app_send_config(oapp: address, eid: u32): Option { + uln_302_store::get_send_uln_config(oapp, eid) + } + + #[view] + /// Gets the receive ULN config for an OApp and EID + /// + /// This will return option::none() if it is not set. + public fun get_app_receive_config(oapp: address, eid: u32): Option { + uln_302_store::get_receive_uln_config(oapp, eid) + } + + #[view] + /// Gets the executor config for an OApp and EID + /// + /// This will return option::none() if it is not set. + public fun get_app_executor_config(oapp: address, eid: u32): Option { + uln_302_store::get_executor_config(oapp, eid) + } + + #[view] + public fun get_verification_confirmations( + header_hash: vector, + payload_hash: vector, + dvn_address: address, + ): u64 { + let header_hash_bytes = bytes32::to_bytes32(header_hash); + let payload_hash_bytes = bytes32::to_bytes32(payload_hash); + + if (uln_302_store::has_verification_confirmations(header_hash_bytes, payload_hash_bytes, dvn_address)) { + uln_302_store::get_verification_confirmations(header_hash_bytes, payload_hash_bytes, dvn_address) + } else 0 + } + + /// Setter for a worker to opt in (true) or out (false) of using the worker_common::worker_config module to store + /// their fee library configuration + public entry fun set_worker_config_for_fee_lib_routing_opt_in(worker: &signer, opt_in: bool) { + let worker_address = address_of(move worker); + // Emit an event only if there is a change + if (uln_302_store::get_worker_config_for_fee_lib_routing_opt_in(worker_address) != opt_in) { + emit(WorkerConfigForFeeLibRoutingOptIn { worker: worker_address, opt_in }) + }; + + uln_302_store::set_worker_config_for_fee_lib_routing_opt_in(worker_address, opt_in); + } + + #[view] + /// Gets the opt in/out status of a worker + public fun worker_config_for_fee_lib_routing_opt_in(worker: address): bool { + uln_302_store::get_worker_config_for_fee_lib_routing_opt_in(worker) + } + + // ==================================================== Events ==================================================== + + #[event] + struct WorkerConfigForFeeLibRoutingOptIn has drop, store { + worker: address, + opt_in: bool, + } + + #[test_only] + public fun worker_config_for_fee_lib_routing_opt_in_event( + worker: address, + opt_in: bool, + ): WorkerConfigForFeeLibRoutingOptIn { + WorkerConfigForFeeLibRoutingOptIn { worker, opt_in } + } +} diff --git a/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/sources/router_calls.move b/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/sources/router_calls.move new file mode 100644 index 00000000..a87c2b4a --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/sources/router_calls.move @@ -0,0 +1,117 @@ +/// Entrypoint for all calls that come from the Message Library Router +module uln_302::router_calls { + use std::any::Any; + use std::fungible_asset::FungibleAsset; + use std::option::Option; + + use endpoint_v2_common::bytes32::Bytes32; + use endpoint_v2_common::contract_identity::{ + DynamicCallRef, + get_dynamic_call_ref_caller, + }; + use endpoint_v2_common::packet_raw::RawPacket; + use endpoint_v2_common::send_packet::SendPacket; + use msglib_types::dvn_verify_params; + use uln_302::configuration; + use uln_302::sending; + use uln_302::verification; + + // ==================================================== Sending =================================================== + + /// Provides a quote for sending a packet + public fun quote( + packet: SendPacket, + options: vector, + pay_in_zro: bool, + ): (u64, u64) { + sending::quote(packet, options, pay_in_zro) + } + + /// Takes payment for sending a packet and triggers offchain entities to verify and deliver the packet + public fun send( + call_ref: &DynamicCallRef, + packet: SendPacket, + options: vector, + native_token: &mut FungibleAsset, + zro_token: &mut Option, + ): (u64, u64, RawPacket) { + assert_caller_is_endpoint_v2(call_ref, b"send"); + sending::send( + packet, + options, + native_token, + zro_token, + ) + } + + // =================================================== Receiving ================================================== + + /// Commits a verification for a packet and clears the memory of that packet + /// + /// Once a packet is committed, it cannot be recommitted without receiving all verifications again. This will abort + /// if the packet has not been verified by all required parties. This is to be called in conjunction with the + /// endpoint's `verify()`, which identifies the message as ready to be delivered by storing the message hash. + public fun commit_verification( + call_ref: &DynamicCallRef, + packet_header: RawPacket, + payload_hash: Bytes32, + _extra_data: vector, + ): (address, u32, Bytes32, u64) { + assert_caller_is_endpoint_v2(call_ref, b"commit_verification"); + verification::commit_verification(packet_header, payload_hash) + } + + // ===================================================== DVNs ===================================================== + + /// This is called by the DVN to verify a packet + /// + /// This expects an Any of type DvnVerifyParams, which contains the packet header, payload hash, and the number of + /// confirmations. This is stored and the verifications are checked as a group when `commit_verification` is called. + public fun dvn_verify(contract_id: &DynamicCallRef, params: Any) { + let worker = get_dynamic_call_ref_caller(contract_id, @uln_302, b"dvn_verify"); + let (packet_header, payload_hash, confirmations) = dvn_verify_params::unpack_dvn_verify_params(params); + verification::verify(worker, packet_header, payload_hash, confirmations) + } + + // ================================================= Configuration ================================================ + + /// Sets the ULN and Executor configurations for an OApp + public fun set_config(call_ref: &DynamicCallRef, oapp: address, eid: u32, config_type: u32, config: vector) { + assert_caller_is_endpoint_v2(call_ref, b"set_config"); + configuration::set_config(oapp, eid, config_type, config) + } + + #[view] + /// Gets the ULN or Executor configuration for an eid on an OApp + public fun get_config(oapp: address, eid: u32, config_type: u32): vector { + configuration::get_config(oapp, eid, config_type) + } + + // ==================================================== Helper ==================================================== + + fun assert_caller_is_endpoint_v2(call_ref: &DynamicCallRef, authorization: vector) { + let caller = get_dynamic_call_ref_caller(call_ref, @uln_302, authorization); + assert!(caller == @endpoint_v2, EINVALID_CALLER); + } + + // ================================================ View Functions ================================================ + + #[view] + public fun version(): (u64 /*major*/, u8 /*minor*/, u8 /*endpoint_version*/) { + (3, 0, 2) + } + + #[view] + public fun is_supported_send_eid(eid: u32): bool { + configuration::supports_send_eid(eid) + } + + #[view] + public fun is_supported_receive_eid(eid: u32): bool { + configuration::supports_receive_eid(eid) + } + + // ================================================== Error Codes ================================================= + + const EINVALID_CALLER: u64 = 1; +} diff --git a/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/tests/internal/assert_valid_default_uln_config_tests.move b/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/tests/internal/assert_valid_default_uln_config_tests.move new file mode 100644 index 00000000..73a42631 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/tests/internal/assert_valid_default_uln_config_tests.move @@ -0,0 +1,110 @@ +#[test_only] +module uln_302::assert_valid_default_uln_config_tests { + use std::bcs::to_bytes; + use std::from_bcs::to_address; + use std::vector; + + use msglib_types::configs_uln::new_uln_config; + use uln_302::assert_valid_default_uln_config::assert_valid_default_uln_config; + + const MAX_DVNS: u64 = 127; + + #[test] + fun test_assert_valid_default_uln_config_valid_if_zero_required_dvs_if_optional_threshold_exists() { + let config = new_uln_config(1, 1, vector[], vector[@0x20], false, false, false); + assert_valid_default_uln_config(&config); + } + + #[test] + fun test_assert_valid_default_uln_config_allows_zero_confirmations() { + let config = new_uln_config(0, 1, vector[@0x20], vector[@0x20], false, false, false); + assert_valid_default_uln_config(&config); + } + + #[test] + #[expected_failure(abort_code = uln_302::assert_valid_default_uln_config::ENO_EFFECTIVE_DVN_THRESHOLD)] + fun test_assert_valid_default_uln_config_fails_in_no_required_dvns_and_no_effective_dvn_threshold() { + let config = new_uln_config(1, 0, vector[], vector[@0x10, @0x20], false, false, false); + assert_valid_default_uln_config(&config); + } + + #[test] + #[expected_failure(abort_code = uln_302::assert_valid_default_uln_config::EOPTIONAL_DVN_THRESHOLD_EXCEEDS_COUNT)] + fun test_assert_valid_default_uln_config_fails_if_optional_dvn_threshold_exceeds_optional_dvn_count() { + let config = new_uln_config(1, 3, vector[@0x20], vector[@0x10, @0x20], false, false, false); + assert_valid_default_uln_config(&config); + } + + + #[test] + #[expected_failure( + abort_code = uln_302::assert_valid_default_uln_config::EREQUESTING_USE_DEFAULT_REQUIRED_DVNS_FOR_DEFAULT_CONFIG + )] + fun test_assert_valid_default_uln_config_fails_if_enabled_default_for_required_dvns() { + let config = new_uln_config(1, 1, vector[@0x20], vector[@0x20], false, true, false); + assert_valid_default_uln_config(&config); + } + + #[test] + #[expected_failure( + abort_code = uln_302::assert_valid_default_uln_config::EREQUESTING_USE_DEFAULT_OPTIONAL_DVNS_FOR_DEFAULT_CONFIG + )] + fun test_assert_valid_default_uln_config_fails_if_enabled_use_default_for_optional_dvns() { + let config = new_uln_config(1, 1, vector[@0x20], vector[@0x20], false, false, true); + assert_valid_default_uln_config(&config); + } + + #[test] + #[expected_failure(abort_code = uln_302::assert_valid_default_uln_config::ETOO_MANY_REQUIRED_DVNS)] + fun test_assert_valid_default_uln_config_fails_if_required_dvns_exceeds_max() { + let required_dvns = vector
[]; + for (i in 0..(MAX_DVNS + 1)) { + vector::push_back(&mut required_dvns, to_address(to_bytes(&(i as u256)))); + }; + let config = new_uln_config(1, 1, required_dvns, vector[@0x20], false, false, false); + assert_valid_default_uln_config(&config); + } + + #[test] + #[expected_failure(abort_code = uln_302::assert_valid_default_uln_config::ETOO_MANY_OPTIONAL_DVNS)] + fun test_assert_valid_default_uln_config_fails_if_optional_dvns_exceeds_max() { + let optional_dvns = vector
[]; + for (i in 0..(MAX_DVNS + 1)) { + vector::push_back(&mut optional_dvns, to_address(to_bytes(&(i as u256)))); + }; + let config = new_uln_config(1, 1, vector[@0x10], optional_dvns, false, false, false); + assert_valid_default_uln_config(&config); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2_common::assert_no_duplicates::EDUPLICATE_ITEM)] + fun test_assert_valid_default_uln_config_fails_if_duplicate_required_dvn() { + let required_dvns = vector
[@1, @2, @3, @1, @5]; + let config = new_uln_config(2, 1, required_dvns, vector[@0x30], false, false, false); + assert_valid_default_uln_config(&config); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2_common::assert_no_duplicates::EDUPLICATE_ITEM)] + fun test_assert_valid_default_uln_config_fails_if_duplicate_optional_dvn() { + let optional_dvns = vector
[@1, @2, @3, @1, @5]; + let config = new_uln_config(2, 1, vector[@0x20], optional_dvns, false, false, false); + assert_valid_default_uln_config(&config); + } + + #[test] + #[expected_failure( + abort_code = uln_302::assert_valid_default_uln_config::EREQUESTING_USE_DEFAULT_CONFIRMATIONS_FOR_DEFAULT_CONFIG + )] + fun test_assert_valid_default_uln_config_fails_if_enabled_use_default_for_confirmations() { + let config = new_uln_config(1, 1, vector[@0x20], vector[@0x20], true, false, false); + assert_valid_default_uln_config(&config); + } + + #[test] + #[expected_failure(abort_code = uln_302::assert_valid_default_uln_config::EINVALID_DVN_THRESHOLD)] + fun test_assert_valid_default_uln_config_fails_if_no_threshold_with_optional_dvns_defined() { + let config = new_uln_config(1, 0, vector[@0x20], vector[@0x20], false, false, false); + assert_valid_default_uln_config(&config); + } +} diff --git a/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/tests/internal/assert_valid_oapp_uln_config_tests.move b/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/tests/internal/assert_valid_oapp_uln_config_tests.move new file mode 100644 index 00000000..2f73b9f6 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/tests/internal/assert_valid_oapp_uln_config_tests.move @@ -0,0 +1,125 @@ +#[test_only] +module uln_302::assert_valid_oapp_uln_config_tests { + use std::bcs::to_bytes; + use std::from_bcs::to_address; + use std::vector; + + use msglib_types::configs_uln::new_uln_config; + use uln_302::assert_valid_uln_config::assert_valid_uln_config; + + const MAX_DVNS: u64 = 127; + + #[test] + #[expected_failure(abort_code = uln_302::assert_valid_uln_config::ENO_EFFECTIVE_DVN_THRESHOLD)] + fun invalid_if_less_than_one_effective_dvn_theshold() { + let default_config = new_uln_config(2, 1, vector[@0x10], vector[@0x20], false, false, false); + let oapp_config = new_uln_config(2, 0, vector[], vector[], false, false, false); + assert_valid_uln_config(&oapp_config, &default_config); + } + + #[test] + #[expected_failure(abort_code = uln_302::assert_valid_uln_config::ENO_EFFECTIVE_DVN_THRESHOLD)] + fun invalid_if_no_effective_dvn_threshold_because_of_use_default_optional_dvns() { + let default_config = new_uln_config(2, 0, vector[@0x10], vector[@0x20], false, false, false); + let oapp_config = new_uln_config(2, 0, vector[], vector[], false, false, true); + assert_valid_uln_config(&oapp_config, &default_config); + } + + #[test] + #[expected_failure(abort_code = uln_302::assert_valid_uln_config::ENO_EFFECTIVE_DVN_THRESHOLD)] + fun invalid_if_no_effective_dvn_threshold_because_of_use_default_required_dvns() { + let default_config = new_uln_config(2, 1, vector[], vector[@0x20], false, false, false); + let oapp_config = new_uln_config(2, 0, vector[], vector[], false, true, false); + assert_valid_uln_config(&oapp_config, &default_config); + } + + #[test] + fun valid_if_one_effective_dvn_threshold_because_use_default_optional_dvns() { + let default_config = new_uln_config(2, 1, vector[@0x10], vector[@0x20], false, false, false); + let oapp_config = new_uln_config(2, 0, vector[], vector[], false, false, true); + assert_valid_uln_config(&oapp_config, &default_config); + } + + #[test] + fun valid_if_one_effective_dvn_threshold_because_use_default_required_dvns() { + let default_config = new_uln_config(2, 0, vector[@0x10], vector[@0x20], false, false, false); + let oapp_config = new_uln_config(2, 0, vector[], vector[], false, true, false); + assert_valid_uln_config(&oapp_config, &default_config); + } + + #[test] + #[expected_failure(abort_code = uln_302::assert_valid_uln_config::ENONEMPTY_REQUIRED_DVNS_WITH_USE_DEFAULT)] + fun invalid_if_required_dvns_specified_when_using_default() { + let default_config = new_uln_config(1, 1, vector[@0x20], vector[@0x30], false, false, false); + let oapp_config = new_uln_config(2, 3, vector[@0x10], vector[@0x20], false, true, false); + assert_valid_uln_config(&oapp_config, &default_config); + } + + #[test] + #[expected_failure(abort_code = uln_302::assert_valid_uln_config::ENONEMPTY_OPTIONAL_DVNS_WITH_USE_DEFAULT)] + fun invalid_if_optional_dvns_specified_when_using_default() { + let default_config = new_uln_config(1, 1, vector[@0x20], vector[@0x30], false, false, false); + let oapp_config = new_uln_config(2, 3, vector[@0x10], vector[@0x20], false, false, true); + assert_valid_uln_config(&oapp_config, &default_config); + } + + #[test] + #[expected_failure(abort_code = uln_302::assert_valid_uln_config::ETOO_MANY_REQUIRED_DVNS)] + fun invalid_if_more_than_max_required_dvns() { + let default_config = new_uln_config(1, 1, vector[@0x20], vector[@0x30], false, false, false); + let required_dvns = vector
[]; + for (i in 0..(MAX_DVNS + 1)) { + vector::push_back(&mut required_dvns, to_address(to_bytes(&(i as u256)))); + }; + let oapp_config = new_uln_config(2, 3, required_dvns, vector[@0x20], false, false, false); + assert_valid_uln_config(&oapp_config, &default_config); + } + + #[test] + #[expected_failure(abort_code = uln_302::assert_valid_uln_config::ETOO_MANY_OPTIONAL_DVNS)] + fun invalid_if_more_than_max_optional_dvns() { + let default_config = new_uln_config(1, 1, vector[@0x20], vector[@0x30], false, false, false); + let optional_dvns = vector
[]; + for (i in 0..(MAX_DVNS + 1)) { + vector::push_back(&mut optional_dvns, to_address(to_bytes(&(i as u256)))); + }; + let oapp_config = new_uln_config(2, 3, vector[@0x10], optional_dvns, false, false, false); + assert_valid_uln_config(&oapp_config, &default_config); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2_common::assert_no_duplicates::EDUPLICATE_ITEM)] + fun invalid_if_duplicate_addresses_in_required_dvns() { + let default_config = new_uln_config(1, 1, vector[@0x20], vector[@0x30], false, false, false); + let required_dvns = vector
[@1, @2, @3, @1, @5]; + let oapp_config = new_uln_config(2, 1, required_dvns, vector[@0x30], false, false, false); + assert_valid_uln_config(&oapp_config, &default_config); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2_common::assert_no_duplicates::EDUPLICATE_ITEM)] + fun invalid_if_duplicate_addresses_in_optional_dvns() { + let default_config = new_uln_config(1, 1, vector[@0x20], vector[@0x30], false, false, false); + let optional_dvns = vector
[@1, @2, @3, @1, @5]; + let oapp_config = new_uln_config(2, 1, vector[@0x20], optional_dvns, false, false, false); + assert_valid_uln_config(&oapp_config, &default_config); + } + + #[test] + #[expected_failure( + abort_code = uln_302::assert_valid_uln_config::ENONZERO_CONFIRMATIONS_PROVIDED_FOR_DEFAULT_CONFIG + )] + fun invalid_if_use_default_for_confirmations_but_provide_confirmations() { + let default_config = new_uln_config(1, 1, vector[@0x20], vector[@0x30], false, false, false); + let oapp_config = new_uln_config(2, 1, vector[@0x10], vector[@0x20], true, false, false); + assert_valid_uln_config(&oapp_config, &default_config); + } + + #[test] + #[expected_failure(abort_code = uln_302::assert_valid_uln_config::EINVALID_DVN_THRESHOLD)] + fun test_assert_valid_default_uln_config_fails_if_no_threshold_with_optional_dvns_defined() { + let default_config = new_uln_config(1, 1, vector[@0x20], vector[@0x30], false, false, false); + let config = new_uln_config(1, 0, vector[@0x20], vector[@0x20], false, false, false); + assert_valid_uln_config(&config, &default_config); + } +} diff --git a/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/tests/internal/configuration_tests.move b/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/tests/internal/configuration_tests.move new file mode 100644 index 00000000..5c57cbd3 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/tests/internal/configuration_tests.move @@ -0,0 +1,478 @@ +#[test_only] +module uln_302::configuration_tests { + use std::event::was_event_emitted; + use std::option; + + use endpoint_v2_common::serde::bytes_of; + use msglib_types::configs_executor; + use msglib_types::configs_executor::new_executor_config; + use msglib_types::configs_uln; + use msglib_types::configs_uln::new_uln_config; + use uln_302::configuration; + use uln_302::configuration::{ + assert_valid_default_executor_config, default_executor_config_set_event, default_uln_config_set_event, + executor_config_set_event, get_executor_config, get_receive_uln_config, get_send_uln_config, + merge_executor_configs, merge_uln_configs, set_default_executor_config, set_default_receive_uln_config, + set_default_send_uln_config, set_executor_config, set_receive_uln_config, set_send_uln_config, + supports_receive_eid, supports_send_eid, + }; + use uln_302::uln_302_store; + + const ULN_SEND_SIDE: u8 = 0; + const ULN_RECEIVE_SIDE: u8 = 1; + + fun setup() { + uln_302_store::init_module_for_test(); + } + + public fun enable_receive_eid_for_test(eid: u32) { + let config = configs_uln::new_uln_config(1, 0, vector[@dvn], vector[], false, false, false); + set_default_receive_uln_config(eid, config); + } + + public fun enable_send_eid_for_test(eid: u32) { + let config = configs_uln::new_uln_config(1, 0, vector[@dvn], vector[], false, false, false); + set_default_send_uln_config(eid, config); + } + + #[test] + fun test_set_default_receive_uln_config() { + setup(); + + // default config not set + assert!(!supports_receive_eid(1), 0); + let retrieved_config = uln_302_store::get_default_receive_uln_config(1); + assert!(option::is_none(&retrieved_config), 0); + + // set config + let config = new_uln_config(1, 1, vector[@0x20], vector[@0x30], false, false, false); + set_default_receive_uln_config(1, config); + assert!(was_event_emitted(&default_uln_config_set_event(1, config, ULN_RECEIVE_SIDE)), 0); + assert!(supports_receive_eid(1), 1); + + let retrieved_config = uln_302_store::get_default_receive_uln_config(1); + assert!(option::borrow(&retrieved_config) == &config, 2); + } + + #[test] + fun test_set_default_send_uln_config() { + setup(); + + // default config not set + let retrieved_config = uln_302_store::get_default_send_uln_config(1); + assert!(option::is_none(&retrieved_config), 0); + assert!(!supports_send_eid(1), 0); + + // set config + let config = new_uln_config(1, 1, vector[@0x20], vector[@0x30], false, false, false); + set_default_send_uln_config(1, config); + assert!(was_event_emitted(&default_uln_config_set_event(1, config, ULN_SEND_SIDE)), 0); + assert!(supports_send_eid(1), 1); + + let retrieved_config = uln_302_store::get_default_send_uln_config(1); + assert!(option::borrow(&retrieved_config) == &config, 2); + } + + + #[test] + #[expected_failure( + abort_code = uln_302::assert_valid_default_uln_config::EREQUESTING_USE_DEFAULT_REQUIRED_DVNS_FOR_DEFAULT_CONFIG + )] + fun test_set_default_receive_uln_config_fails_if_invalid() { + setup(); + // cannot "use default" for default config + let config = new_uln_config(1, 1, vector[@0x20], vector[@0x30], false, true, false); + set_default_receive_uln_config(1, config); + } + + #[test] + #[expected_failure( + abort_code = uln_302::assert_valid_default_uln_config::EREQUESTING_USE_DEFAULT_REQUIRED_DVNS_FOR_DEFAULT_CONFIG + )] + fun test_set_default_send_uln_config_fails_if_invalid() { + setup(); + // cannot "use default" for default config + let config = new_uln_config(1, 1, vector[@0x20], vector[@0x30], false, true, false); + set_default_send_uln_config(1, config); + } + + #[test] + fun test_set_send_uln_config() { + setup(); + let default_config = new_uln_config(1, 1, vector[@0x10], vector[@0x30, @0x50], false, false, false); + set_default_send_uln_config(1, default_config); + + // before setting oapp config, it should pull default + let retreived_config = get_send_uln_config(@9999, 1); + assert!(retreived_config == default_config, 0); + + // setting a different sender has no effect + let config = new_uln_config(2, 0, vector[@0x20], vector[], false, false, true); + set_send_uln_config(@1111, 1, config); + assert!(was_event_emitted(&uln_302::configuration::uln_config_set_event(@1111, 1, config, ULN_SEND_SIDE)), 0); + let retreived_config = get_send_uln_config(@9999, 1); + assert!(retreived_config == default_config, 0); + + // set same sender + set_send_uln_config(@9999, 1, config); + + // should return merged config - using configured required and default optional + let retrieved_config = get_send_uln_config(@9999, 1); + let expected_merged_config = new_uln_config(2, 1, vector[@0x20], vector[@0x30, @0x50], false, false, true); + assert!(retrieved_config == expected_merged_config, 1); + } + + #[test] + #[expected_failure(abort_code = uln_302::configuration::EEID_NOT_CONFIGURED)] + fun test_set_send_uln_config_fails_if_default_not_set() { + setup(); + let config = new_uln_config(2, 0, vector[@0x20], vector[], false, false, true); + set_send_uln_config(@9, 1, config); + } + + #[test] + #[expected_failure(abort_code = uln_302::assert_valid_uln_config::ENO_EFFECTIVE_DVN_THRESHOLD)] + fun test_set_send_uln_config_fails_if_default_and_config_merge_to_invalid_config() { + setup(); + let default_config = new_uln_config(1, 1, vector[], vector[@0x30], false, false, false); + set_default_send_uln_config(1, default_config); + + // combined, this has no dvns + let config = new_uln_config(10, 0, vector[], vector[], false, true, false); + set_send_uln_config(@9, 1, config); + } + + #[test] + fun test_set_receive_uln_config() { + setup(); + let default_config = new_uln_config(1, 1, vector[@0x10], vector[@0x30, @0x50], false, false, false); + set_default_receive_uln_config(1, default_config); + + // before setting oapp config, it should pull default + let retreived_config = get_receive_uln_config(@9999, 1); + assert!(retreived_config == default_config, 0); + + let config = new_uln_config(2, 0, vector[@0x20], vector[], false, false, true); + + // setting a different receiver has no effect + set_receive_uln_config(@1234, 1, config); + let retreived_config = get_receive_uln_config(@9999, 1); + assert!( + was_event_emitted(&uln_302::configuration::uln_config_set_event(@1234, 1, config, ULN_RECEIVE_SIDE)), + 0, + ); + assert!(retreived_config == default_config, 0); + + // set the intended receiver + set_receive_uln_config(@9999, 1, config); + assert!( + was_event_emitted(&uln_302::configuration::uln_config_set_event(@9999, 1, config, ULN_RECEIVE_SIDE)), + 0, + ); + + // should return merged config - using configured required and default optional + let retrieved_config = get_receive_uln_config(@9999, 1); + let expected_merged_config = new_uln_config(2, 1, vector[@0x20], vector[@0x30, @0x50], false, false, true); + assert!(retrieved_config == expected_merged_config, 1); + } + + #[test] + #[expected_failure(abort_code = uln_302::configuration::EEID_NOT_CONFIGURED)] + fun test_set_receive_uln_config_fails_if_default_not_set() { + setup(); + let config = new_uln_config(2, 0, vector[@0x20], vector[], false, false, true); + set_receive_uln_config(@9, 1, config); + } + + #[test] + #[expected_failure(abort_code = uln_302::assert_valid_uln_config::ENO_EFFECTIVE_DVN_THRESHOLD)] + fun test_set_receive_uln_config_fails_if_default_and_config_merge_to_invalid_config() { + setup(); + let default_config = new_uln_config(1, 1, vector[], vector[@0x30], false, false, false); + set_default_receive_uln_config(1, default_config); + + // combined, this has no dvns + let config = new_uln_config(10, 0, vector[], vector[], false, true, false); + set_receive_uln_config(@9, 1, config); + } + + #[test] + fun test_get_send_uln_config_gets_default_if_oapp_not_set() { + setup(); + let default_config = new_uln_config(1, 1, vector[@0x10], vector[@0x30, @0x50], false, false, false); + set_default_send_uln_config(1, default_config); + + let retrieved_config = get_send_uln_config(@9999, 1); + assert!(retrieved_config == default_config, 0); + } + + #[test] + fun test_get_receive_uln_config_gets_default_if_oapp_not_set() { + setup(); + let default_config = new_uln_config(1, 1, vector[@0x10], vector[@0x30, @0x50], false, false, false); + set_default_receive_uln_config(1, default_config); + + let retrieved_config = get_receive_uln_config(@9999, 1); + assert!(retrieved_config == default_config, 0); + } + + #[test] + fun test_get_send_uln_config_gets_merged_if_oapp_is_set() { + setup(); + let default_config = new_uln_config(1, 1, vector[@0x10], vector[@0x30, @0x50], false, false, false); + set_default_send_uln_config(1, default_config); + + let oapp_config = new_uln_config(2, 0, vector[@0x20], vector[], false, false, true); + set_send_uln_config(@9999, 1, oapp_config); + + let retrieved_config = get_send_uln_config(@9999, 1); + let expected_config = new_uln_config(2, 1, vector[@0x20], vector[@0x30, @0x50], false, false, true); + assert!(retrieved_config == expected_config, 0); + } + + #[test] + fun test_get_receive_uln_config_gets_merged_if_oapp_is_set() { + setup(); + let default_config = new_uln_config(1, 1, vector[@0x10], vector[@0x30, @0x50], false, false, false); + set_default_receive_uln_config(1, default_config); + + let oapp_config = new_uln_config(2, 0, vector[@0x20], vector[], false, false, true); + set_receive_uln_config(@9999, 1, oapp_config); + + let retrieved_config = get_receive_uln_config(@9999, 1); + let expected_config = new_uln_config(2, 1, vector[@0x20], vector[@0x30, @0x50], false, false, true); + assert!(retrieved_config == expected_config, 0); + } + + #[test] + #[expected_failure(abort_code = uln_302::configuration::EEID_NOT_CONFIGURED)] + fun test_get_send_uln_config_fails_if_eid_not_configured() { + setup(); + get_send_uln_config(@9999, 1); + } + + #[test] + #[expected_failure(abort_code = uln_302::configuration::EEID_NOT_CONFIGURED)] + fun test_get_receive_uln_config_fails_if_eid_not_configured() { + setup(); + get_receive_uln_config(@9999, 1); + } + + #[test] + fun test_set_default_executor_config() { + setup(); + + // default config not set + let retrieved_config = uln_302_store::get_default_executor_config(1); + assert!(option::is_none(&retrieved_config), 0); + + // set config + let config = new_executor_config(1000, @9001); + set_default_executor_config(1, config); + assert!(was_event_emitted(&default_executor_config_set_event(1, config)), 0); + + let retrieved_config = uln_302_store::get_default_executor_config(1); + assert!(option::borrow(&retrieved_config) == &config, 0); + } + + #[test] + #[expected_failure(abort_code = uln_302::configuration::EEXECUTOR_ADDRESS_IS_ZERO)] + fun test_set_default_executor_config_fails_if_invalid_executor_address() { + setup(); + let config = new_executor_config(1000, @0x0); + set_default_executor_config(1, config); + } + + #[test] + fun test_set_executor_config() { + setup(); + let default_config = new_executor_config(1000, @9001); + set_default_executor_config(1, default_config); + assert!(was_event_emitted(&default_executor_config_set_event(1, default_config)), 0); + + // before setting oapp config, it should pull default + let retreived_config = get_executor_config(@9999, 1); + assert!(retreived_config == default_config, 0); + + let config = new_executor_config(2000, @9002); + + // setting another receiver has no effect + set_executor_config(@123, 1, config); + let retreived_config = get_executor_config(@9999, 1); + assert!(retreived_config == default_config, 0); + + // setting the correct receiver + set_executor_config(@9999, 1, config); + assert!(was_event_emitted(&executor_config_set_event(@9999, 1, config)), 0); + + // should return merged config - using configured required and default optional + let retrieved_config = get_executor_config(@9999, 1); + assert!(retrieved_config == config, 1); + } + + #[test] + fun test_get_executor_config_gets_default_if_oapp_not_set() { + setup(); + let default_config = new_executor_config(1000, @9001); + set_default_executor_config(1, default_config); + + let retrieved_config = get_executor_config(@9999, 1); + assert!(retrieved_config == default_config, 0); + } + + #[test] + fun test_get_executor_config_gets_oapp_if_set() { + setup(); + let default_config = new_executor_config(1000, @9001); + set_default_executor_config(1, default_config); + + let oapp_config = new_executor_config(2000, @9002); + set_executor_config(@9999, 1, oapp_config); + + let retrieved_config = get_executor_config(@9999, 1); + assert!(retrieved_config == oapp_config, 0); + } + + #[test] + fun test_get_executor_config_gets_merged_if_oapp_max_message_size_set_to_zero() { + setup(); + let default_config = new_executor_config(1000, @9001); + set_default_executor_config(1, default_config); + + let oapp_config = new_executor_config(0, @9002); + set_executor_config(@9999, 1, oapp_config); + + let retrieved_config = get_executor_config(@9999, 1); + let expected_config = new_executor_config(1000, @9002); + assert!(retrieved_config == expected_config, 0); + } + + #[test] + fun test_assert_valid_default_executor_config() { + let config = new_executor_config(1000, @9001); + assert_valid_default_executor_config(&config); + } + + #[test] + #[expected_failure(abort_code = uln_302::configuration::EMAX_MESSAGE_SIZE_ZERO)] + fun test_assert_valid_default_executor_config_fails_if_invalid_message_size() { + let config = new_executor_config(0, @9001); + assert_valid_default_executor_config(&config); + } + + #[test] + #[expected_failure(abort_code = uln_302::configuration::EEXECUTOR_ADDRESS_IS_ZERO)] + fun test_assert_valid_default_executor_config_fails_if_invalid_executor_address() { + let config = new_executor_config(1000, @0x0); + assert_valid_default_executor_config(&config); + } + + #[test] + fun test_merge_executor_configs_uses_oapp_is_complete() { + let default_config = new_executor_config(1000, @9001); + let oapp_config = new_executor_config(2000, @9002); + let merged_config = merge_executor_configs(&default_config, &oapp_config); + assert!(merged_config == oapp_config, 0); + } + + #[test] + fun test_merge_executor_configs_uses_default_if_oapp_max_message_size_is_zero() { + let default_config = new_executor_config(1000, @9001); + let oapp_config = new_executor_config(0, @9002); + let merged_config = merge_executor_configs(&default_config, &oapp_config); + let expected_config = new_executor_config(1000, @9002); + assert!(merged_config == expected_config, 0); + } + + #[test] + fun test_merge_executor_configs_uses_default_if_oapp_executor_address_is_zero() { + let default_config = new_executor_config(1000, @9001); + let oapp_config = new_executor_config(2000, @0x0); + let merged_config = merge_executor_configs(&default_config, &oapp_config); + let expected_config = new_executor_config(2000, @9001); + assert!(merged_config == expected_config, 0); + } + + #[test] + fun test_merge_configs_should_squash_the_default_required_dvns_when_use_default_for_required_dvns_is_set_to_true() { + let default_config = new_uln_config(2, 1, vector[@0x10], vector[@0x20], false, false, false); + let oapp_config = new_uln_config(0, 1, vector[], vector[@0x40], false, true, false); + let merged_config = merge_uln_configs(&default_config, &oapp_config); + let expected_config = new_uln_config(0, 1, vector[@0x10], vector[@0x40], false, true, false); + assert!(merged_config == expected_config, 0); + } + + #[test] + fun test_merge_configs_should_use_the_default_optional_dvns_and_threshold_when_use_default_for_optional_dvns_is_set_to_true( + ) { + let default_config = new_uln_config(2, 1, vector[@0x10], vector[@0x20], false, false, false); + let oapp_config = new_uln_config(3, 0, vector[@0x40], vector[], false, false, true); + let merged_config = merge_uln_configs(&default_config, &oapp_config); + let expected_config = new_uln_config(3, 1, vector[@0x40], vector[@0x20], false, false, true); + + assert!(merged_config == expected_config, 0); + } + + #[test] + fun test_merge_configs_should_use_the_default_confirmations_when_use_default_for_confirmations_is_set_to_true() { + let default_config = new_uln_config(2, 1, vector[@0x10], vector[@0x20], false, false, false); + let oapp_config = new_uln_config(0, 1, vector[@0x40], vector[@0x50], true, false, false); + let merged_config = merge_uln_configs(&default_config, &oapp_config); + let expected_config = new_uln_config(2, 1, vector[@0x40], vector[@0x50], true, false, false); + assert!(merged_config == expected_config, 0); + } + + #[test] + fun test_merge_configs_should_merge_multiple_fields() { + let default_config = new_uln_config(2, 1, vector[@0x10], vector[@0x20], false, false, false); + let oapp_config = new_uln_config(3, 0, vector[], vector[], true, true, true); + let merged_config = merge_uln_configs(&default_config, &oapp_config); + let expected_config = new_uln_config(2, 1, vector[@0x10], vector[@0x20], true, true, true); + assert!(merged_config == expected_config, 0); + } + + #[test] + fun get_config_should_get_the_send_uln_config_if_that_is_requested() { + setup(); + let send_config = new_uln_config(1, 1, vector[@0x10], vector[@0x30, @0x50], false, false, false); + set_default_send_uln_config(1, send_config); + let receive_config = new_uln_config(2, 1, vector[@0x20], vector[@0x40, @0x60], false, false, false); + set_default_receive_uln_config(1, receive_config); + let executor_config = new_executor_config(1000, @9001); + set_default_executor_config(1, executor_config); + + let retrieved_config = configuration::get_config(@9999, 1, 2); + let expected_config = bytes_of(|buf| configs_uln::append_uln_config(buf, send_config)); + assert!(expected_config == retrieved_config, 0); + } + + #[test] + fun get_config_should_get_the_receive_uln_config_if_that_is_requested() { + setup(); + let send_config = new_uln_config(1, 1, vector[@0x10], vector[@0x30, @0x50], false, false, false); + set_default_send_uln_config(1, send_config); + let receive_config = new_uln_config(2, 1, vector[@0x20], vector[@0x40, @0x60], false, false, false); + set_default_receive_uln_config(1, receive_config); + let executor_config = new_executor_config(1000, @9001); + set_default_executor_config(1, executor_config); + + let retrieved_config = configuration::get_config(@9999, 1, 3); + let expected_config = bytes_of(|buf| configs_uln::append_uln_config(buf, receive_config)); + assert!(expected_config == retrieved_config, 0); + } + + #[test] + fun get_config_should_get_the_executor_config_if_that_is_requested() { + setup(); + let send_config = new_uln_config(1, 1, vector[@0x10], vector[@0x30, @0x50], false, false, false); + set_default_send_uln_config(1, send_config); + let receive_config = new_uln_config(2, 1, vector[@0x20], vector[@0x40, @0x60], false, false, false); + set_default_receive_uln_config(1, receive_config); + let executor_config = new_executor_config(1000, @9001); + set_default_executor_config(1, executor_config); + + let retrieved_config = configuration::get_config(@9999, 1, 1); + let expected_config = bytes_of(|buf| configs_executor::append_executor_config(buf, executor_config)); + assert!(expected_config == retrieved_config, 0); + } +} diff --git a/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/tests/internal/for_each_dvn_tests.move b/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/tests/internal/for_each_dvn_tests.move new file mode 100644 index 00000000..e0c299e9 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/tests/internal/for_each_dvn_tests.move @@ -0,0 +1,18 @@ +#[test_only] +module uln_302::for_each_dvn_tests { + use std::vector; + + use uln_302::for_each_dvn::for_each_dvn; + + #[test] + fun test_for_each_dvn() { + let required_dvns = vector[@1, @2, @3]; + let optional_dvns = vector[@4, @5, @6]; + let expected_dvns = vector[@1, @2, @3, @4, @5, @6]; + + for_each_dvn(&required_dvns, &optional_dvns, |dvn, idx| { + let expected_dvn = *vector::borrow(&expected_dvns, idx); + assert!(dvn == expected_dvn, 1); + }); + } +} diff --git a/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/tests/internal/msglib_tests.move b/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/tests/internal/msglib_tests.move new file mode 100644 index 00000000..411d25d0 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/tests/internal/msglib_tests.move @@ -0,0 +1,102 @@ +#[test_only] +module uln_302::msglib_tests { + use std::option; + + use msglib_types::configs_executor::new_executor_config; + use msglib_types::configs_uln::new_uln_config; + use uln_302::configuration::{ + set_default_executor_config, set_default_receive_uln_config, set_default_send_uln_config, set_executor_config, + set_receive_uln_config, set_send_uln_config, + }; + use uln_302::msglib; + + fun setup() { + msglib::initialize_for_test(); + } + + #[test] + fun test_get_app_send_config() { + setup(); + let send_config = new_uln_config(1, 1, vector[@0x10], vector[@0x30, @0x50], false, false, false); + set_default_send_uln_config(1, send_config); + + set_send_uln_config(@9999, 1, send_config); + + let retrieved_config = msglib::get_app_send_config(@9999, 1); + assert!(retrieved_config == option::some(send_config), 0); + } + + #[test] + fun test_get_app_send_config_should_return_option_none_if_only_default_is_set() { + setup(); + let send_config = new_uln_config(1, 1, vector[@0x10], vector[@0x30, @0x50], false, false, false); + set_default_send_uln_config(1, send_config); + + let retrieved_config = msglib::get_app_send_config(@9999, 1); + assert!(retrieved_config == option::none(), 0); + } + + #[test] + fun get_app_send_config_should_return_option_none_if_default_not_set() { + setup(); + let retrieved_config = msglib::get_app_send_config(@9999, 1); + assert!(retrieved_config == option::none(), 0); + } + + #[test] + fun test_get_app_receive_config() { + setup(); + let receive_config = new_uln_config(2, 1, vector[@0x20], vector[@0x40, @0x60], false, false, false); + set_default_receive_uln_config(1, receive_config); + + set_receive_uln_config(@9999, 1, receive_config); + + let retrieved_config = msglib::get_app_receive_config(@9999, 1); + assert!(retrieved_config == option::some(receive_config), 0); + } + + #[test] + fun test_get_app_receive_config_should_return_option_none_if_only_default_is_set() { + setup(); + let receive_config = new_uln_config(2, 1, vector[@0x20], vector[@0x40, @0x60], false, false, false); + set_default_receive_uln_config(1, receive_config); + + let retrieved_config = msglib::get_app_receive_config(@9999, 1); + assert!(retrieved_config == option::none(), 0); + } + + #[test] + fun get_app_receive_config_should_return_option_none_if_default_not_set() { + setup(); + let retrieved_config = msglib::get_app_receive_config(@9999, 1); + assert!(retrieved_config == option::none(), 0); + } + + #[test] + fun test_get_app_executor_config() { + setup(); + let executor_config = new_executor_config(1000, @9001); + set_default_executor_config(1, executor_config); + set_executor_config(@9999, 1, executor_config); + + let retrieved_config = msglib::get_app_executor_config(@9999, 1); + assert!(retrieved_config == option::some(executor_config), 0); + } + + #[test] + fun test_get_app_executor_config_should_return_option_none_if_only_default_is_set() { + setup(); + let executor_config = new_executor_config(1000, @9001); + set_default_executor_config(1, executor_config); + + let retrieved_config = msglib::get_app_executor_config(@9999, 1); + assert!(retrieved_config == option::none(), 0); + } + + #[test] + fun get_app_executor_config_should_return_option_none_if_default_not_set() { + setup(); + let retrieved_config = msglib::get_app_executor_config(@9999, 1); + assert!(retrieved_config == option::none(), 0); + } +} \ No newline at end of file diff --git a/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/tests/internal/sending_tests.move b/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/tests/internal/sending_tests.move new file mode 100644 index 00000000..9c1e723b --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/tests/internal/sending_tests.move @@ -0,0 +1,331 @@ +#[test_only] +module uln_302::sending_tests { + use std::event::was_event_emitted; + use std::fungible_asset::FungibleAsset; + use std::option::{Self, destroy_none}; + use std::vector; + + use endpoint_v2_common::bytes32; + use endpoint_v2_common::guid; + use endpoint_v2_common::native_token_test_helpers::{burn_token_for_test, mint_native_token_for_test}; + use endpoint_v2_common::packet_raw; + use endpoint_v2_common::packet_v1_codec; + use endpoint_v2_common::send_packet; + use endpoint_v2_common::serde::flatten; + use msglib_types::configs_executor::new_executor_config; + use msglib_types::configs_uln::new_uln_config; + use msglib_types::worker_options::{ + append_dvn_option, + append_generic_type_3_executor_option, + new_empty_type_3_options, + }; + use treasury::treasury; + use uln_302::configuration; + use uln_302::sending::{dvn_fee_paid_event, executor_fee_paid_event, quote_internal, send_internal}; + use uln_302::uln_302_store; + + #[test] + fun test_send_internal() { + uln_302_store::init_module_for_test(); + treasury::init_module_for_test(); + configuration::set_default_send_uln_config(103, new_uln_config( + 5, + 1, + vector[@10001], + vector[@10002, @10003], + false, + false, + false, + )); + configuration::set_default_executor_config(103, new_executor_config( + 10000, + @10100, + )); + + let native_token = mint_native_token_for_test(10000); + let zro_token = option::none(); + + let send_packet = send_packet::new_send_packet( + 1, + 102, + bytes32::from_address(@1234), + 103, + bytes32::from_address(@5678), + b"payload", + ); + + let worker_options = new_empty_type_3_options(); + append_dvn_option(&mut worker_options, + 0, + 9, + x"aa00", + ); + append_dvn_option(&mut worker_options, + 2, + 9, + x"aa20", + ); + append_dvn_option(&mut worker_options, + 2, + 5, + x"aa21", + ); + append_generic_type_3_executor_option(&mut worker_options, b"777"); + + let get_executor_fee_call_count = 0; + let get_dvn_fee_call_count = 0; + let called_dvns = vector
[]; + + // needed to prevent the compiler warning + assert!(get_executor_fee_call_count == 0, 0); + assert!(get_dvn_fee_call_count == 0, 0); + assert!(vector::length(&called_dvns) == 0, 0); + + let expected_packet_header = packet_v1_codec::new_packet_v1_header_only( + 102, + bytes32::from_address(@1234), + 103, + bytes32::from_address(@5678), + 1, + ); + let guid = bytes32::from_bytes32(guid::compute_guid( + 1, + 102, + bytes32::from_address(@1234), + 103, + bytes32::from_address(@5678), + )); + + let expected_payload_hash = bytes32::keccak256(flatten(vector[guid, b"payload"])); + + let (native_fee, zro_fee, encoded_packet) = send_internal( + send_packet, + worker_options, + &mut native_token, + &mut zro_token, + |executor_address, executor_options| { + get_executor_fee_call_count = get_executor_fee_call_count + 1; + assert!(executor_address == @10100, 101); + let expected_option = flatten(vector[ + x"01", // executor option + x"0003", // length = 3 + b"777" // option + ]); + assert!(executor_options == expected_option, 102); + (101, @555) + }, + |dvn_address, confirmations, dvn_options, packet_header, payload_hash| { + assert!(packet_header == expected_packet_header, 0); + assert!(payload_hash == expected_payload_hash, 0); + get_dvn_fee_call_count = get_dvn_fee_call_count + 1; + vector::push_back(&mut called_dvns, dvn_address); + assert!(confirmations == 5, 0); + if (dvn_address == @10001) { + let expected_option = flatten(vector[ + x"02", // dvn option + x"0004", // length = 4 + x"00", // dvn index + x"09", // option type + x"aa00", // option + ]); + assert!(dvn_options == expected_option, 0); + (203, @777) + } else if (dvn_address == @10002) { + let expected_option = x""; // empty + assert!(dvn_options == expected_option, 0); + + (204, @778) + } else if (dvn_address == @10003) { + let expected_option = flatten(vector[ + // first option + x"02", // dvn option + x"0004", // length = 4 + x"02", // dvn index + x"09", // option type + x"aa20", // option + // second option + x"02", // dvn option + x"0004", // length = 4 + x"02", // dvn index + x"05", // option type + x"aa21", // option + ]); + assert!(dvn_options == expected_option, 0); + (205, @779) + } else { + (1, @111) + } + }, + ); + + // (203 + 204 + 205) + 101 = 713 + assert!(native_fee == 713, 0); + assert!(zro_fee == 0, 0); + + let expected_encoded_packet = flatten(vector[ + packet_raw::get_packet_bytes(expected_packet_header), + guid, + b"payload", + ]); + assert!(packet_raw::get_packet_bytes(encoded_packet) == expected_encoded_packet, 0); + + assert!(get_executor_fee_call_count == 1, 1); + assert!(get_dvn_fee_call_count == 3, 1); + assert!(vector::contains(&called_dvns, &@10001), 0); + assert!(vector::contains(&called_dvns, &@10002), 0); + assert!(vector::contains(&called_dvns, &@10003), 0); + + assert!(was_event_emitted(&executor_fee_paid_event( + @10100, + @555, + 101, + )), 0); + + assert!(was_event_emitted(&dvn_fee_paid_event( + vector[@10001], + vector[@10002, @10003], + vector[203, 204, 205], + vector[@777, @778, @779], + )), 0); + burn_token_for_test(native_token); + destroy_none(zro_token); + } + + #[test] + fun test_quote_internal() { + uln_302_store::init_module_for_test(); + treasury::init_module_for_test(); + configuration::set_default_send_uln_config(103, new_uln_config( + 5, + 1, + vector[@10001], + vector[@10002, @10003], + false, + false, + false, + )); + configuration::set_default_executor_config(103, new_executor_config( + 10000, + @10100, + )); + + let send_packet = send_packet::new_send_packet( + 1, + 102, + bytes32::from_address(@1234), + 103, + bytes32::from_address(@5678), + b"payload", + ); + + let worker_options = new_empty_type_3_options(); + append_dvn_option(&mut worker_options, + 0, + 9, + x"aa00", + ); + append_dvn_option(&mut worker_options, + 2, + 9, + x"aa20", + ); + append_dvn_option(&mut worker_options, + 2, + 5, + x"aa21", + ); + append_generic_type_3_executor_option(&mut worker_options, b"777"); + + let get_executor_fee_call_count = 0; + let get_dvn_fee_call_count = 0; + + // needed to prevent the compiler warning + assert!(get_executor_fee_call_count == 0, 0); + assert!(get_dvn_fee_call_count == 0, 0); + + let expected_packet_header = packet_v1_codec::new_packet_v1_header_only( + 102, + bytes32::from_address(@1234), + 103, + bytes32::from_address(@5678), + 1, + ); + let guid = bytes32::from_bytes32(guid::compute_guid( + 1, + 102, + bytes32::from_address(@1234), + 103, + bytes32::from_address(@5678), + )); + + let expected_payload_hash = bytes32::keccak256(flatten(vector[guid, b"payload"])); + let called_dvns = vector
[]; + assert!(vector::length(&called_dvns) == 0, 0); + + let (native_fee, zro_fee) = quote_internal( + send_packet, + worker_options, + false, + |executor_address, executor_options| { + get_executor_fee_call_count = get_executor_fee_call_count + 1; + assert!(executor_address == @10100, 101); + let expected_option = flatten(vector[ + x"01", // executor option + x"0003", // length = 3 + b"777" // option + ]); + assert!(executor_options == expected_option, 102); + (101, @555) + }, + |dvn_address, confirmations, dvn_options, packet_header, payload_hash| { + assert!(packet_header == expected_packet_header, 0); + assert!(payload_hash == expected_payload_hash, 0); + get_dvn_fee_call_count = get_dvn_fee_call_count + 1; + vector::push_back(&mut called_dvns, dvn_address); + assert!(confirmations == 5, 0); + if (dvn_address == @10001) { + let expected_option = flatten(vector[ + x"02", // dvn option + x"0004", // length = 4 + x"00", // dvn index + x"09", // option type + x"aa00", // option + ]); + assert!(dvn_options == expected_option, 0); + }; + if (dvn_address == @10002) { + let expected_option = x""; // empty + assert!(dvn_options == expected_option, 0); + }; + if (dvn_address == @10003) { + let expected_option = flatten(vector[ + // first option + x"02", // dvn option + x"0004", // length = 4 + x"02", // dvn index + x"09", // option type + x"aa20", // option + // second option + x"02", // dvn option + x"0004", // length = 4 + x"02", // dvn index + x"05", // option type + x"aa21", // option + ]); + assert!(dvn_options == expected_option, 0); + }; + (203, @777) + }, + ); + + // 203 * 3 + 101 = 710 + assert!(native_fee == 710, 0); + assert!(zro_fee == 0, 0); + + assert!(get_executor_fee_call_count == 1, 1); + assert!(get_dvn_fee_call_count == 3, 1); + assert!(vector::contains(&called_dvns, &@10001), 0); + assert!(vector::contains(&called_dvns, &@10002), 0); + assert!(vector::contains(&called_dvns, &@10003), 0); + } +} diff --git a/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/tests/internal/verification_tests.move b/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/tests/internal/verification_tests.move new file mode 100644 index 00000000..bfa52e26 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/msglib/libs/uln_302/tests/internal/verification_tests.move @@ -0,0 +1,378 @@ +#[test_only] +module uln_302::verification_tests { + use endpoint_v2_common::bytes32; + use endpoint_v2_common::guid::compute_guid; + use endpoint_v2_common::packet_raw::{bytes_to_raw_packet, get_packet_bytes}; + use endpoint_v2_common::packet_v1_codec; + use endpoint_v2_common::universal_config; + use msglib_types::configs_uln; + use uln_302::configuration; + use uln_302::uln_302_store; + use uln_302::verification::{ + check_verifiable, + commit_verification, + get_receive_uln_config_from_packet_header, + is_verified, + reclaim_storage, + verify, + verify_and_reclaim_storage, + }; + + fun setup() { + universal_config::init_module_for_test(33333); + uln_302_store::init_module_for_test(); + } + + #[test] + fun test_verify_saves_entry_and_emits_event() { + setup(); + + verify( + @1111, + bytes_to_raw_packet(b"header"), + bytes32::keccak256(b"payload_hash"), + 5, + ); + + let expected_header_hash = bytes32::to_bytes32( + x"1fe1673da51f096dc3720c34d3002519bd6c4e0d13dc62302f0d04c06d30786e" + ); + let confirmations = uln_302_store::get_verification_confirmations( + expected_header_hash, + bytes32::keccak256(b"payload_hash"), + @1111, + ); + + assert!(confirmations == 5, 0); + } + + #[test] + fun test_check_verifiable_returns_true_when_verified() { + setup(); + + let config = configs_uln::new_uln_config( + 2, + 0, + vector[@123], + vector[], + false, + false, + false, + ); + configuration::set_default_receive_uln_config(1, config); + + verify(@123, bytes_to_raw_packet(b"header"), bytes32::keccak256(b"payload_hash"), 2); + + let verifiable = check_verifiable( + &config, + bytes32::keccak256(b"header"), + bytes32::keccak256(b"payload_hash"), + ); + + assert!(verifiable, 0); + } + + #[test] + fun test_check_verifiable_returns_false_when_not_verified() { + setup(); + + let config = configs_uln::new_uln_config( + 2, + 2, + vector[@123], + vector[@456, @567, @678], + false, + false, + false, + ); + configuration::set_default_receive_uln_config(1, config); + + let packet_header = bytes_to_raw_packet(b"header"); + verify(@123, packet_header, bytes32::keccak256(b"payload_hash"), 2); + verify(@456, packet_header, bytes32::keccak256(b"payload_hash"), 3); + verify(@567, packet_header, bytes32::keccak256(b"payload_hash"), 1 /* insufficient confirmations */); + + let verifiable = check_verifiable( + &config, + bytes32::keccak256(b"header"), + bytes32::keccak256(b"payload_hash"), + ); + + assert!(!verifiable, 0); + } + + #[test] + fun test_is_verified_returns_false_when_not_verified() { + setup(); + + let verified = is_verified( + @1111, + bytes32::keccak256(b"header"), + bytes32::keccak256(b"payload_hash"), + 2, + ); + + assert!(!verified, 0); + } + + #[test] + fun test_commit_verification_reclaims_storage() { + setup(); + + let default_config = configs_uln::new_uln_config( + 2, + 2, + vector[@123], + vector[@456, @567, @678], + false, + false, + false, + ); + configuration::set_default_receive_uln_config(22222, default_config); + + // Store an oapp specific config + let oapp_config = configs_uln::new_uln_config( + 2, + 0, + vector[@712], // Intentionally different from default, to make sure Oapp config is used + vector[], + false, + false, + false, + ); + configuration::set_receive_uln_config(@0x999999, 22222, oapp_config); + let header = packet_v1_codec::new_packet_v1_header_only( + 22222, // matches eid in config + bytes32::to_bytes32(x"0000000000000000000000000000000000000000000000000000000001111111"), + 33333, + bytes32::to_bytes32( + x"0000000000000000000000000000000000000000000000000000000000999999" + ), // matches the oapp address + 123456, + ); + let header_hash = bytes32::keccak256(get_packet_bytes(header)); + let payload_hash = bytes32::keccak256(b"payload_hash"); + let dvn_address = @712; + + // Receive a verification from the oapp selected required DVN + verify( + dvn_address, + header, + payload_hash, + 2, + ); + + // Confirmations should be present before reclaiming + assert!(uln_302_store::has_verification_confirmations(header_hash, payload_hash, dvn_address), 0); + + commit_verification( + header, + bytes32::keccak256(b"payload_hash"), + ); + + // Confirmations should be removed after reclaiming + assert!(!uln_302_store::has_verification_confirmations(header_hash, payload_hash, dvn_address), 0); + + let verifiable = check_verifiable( + &oapp_config, + header_hash, + bytes32::keccak256(b"payload_hash"), + ); + + assert!(!verifiable, 0); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2_common::packet_v1_codec::EINVALID_PACKET_HEADER)] + fun test_commit_verification_asserts_packet_header() { + setup(); + + commit_verification( + bytes_to_raw_packet(b"header"), + bytes32::keccak256(b"payload_hash"), + ); + } + + #[test] + fun test_is_verified_returns_false_when_verified_but_insufficient_confirmations() { + setup(); + + verify( + @1111, + bytes_to_raw_packet(b"header"), + bytes32::keccak256(b"payload_hash"), + 1, + ); + + let verified = is_verified( + @1111, + bytes32::keccak256(b"header"), + bytes32::keccak256(b"payload_hash"), + 2, + ); + + assert!(!verified, 0); + } + + + #[test] + fun test_is_verified_returns_true_when_verified() { + setup(); + + verify( + @1111, + bytes_to_raw_packet(b"header"), + bytes32::keccak256(b"payload_hash"), + 5, + ); + + let verified = is_verified( + @1111, + bytes32::keccak256(b"header"), + bytes32::keccak256(b"payload_hash"), + 2, + ); + + assert!(verified, 0); + } + + #[test] + fun test_get_receive_uln_config_from_packet_header_returns_default_config() { + setup(); + + let default_config = configs_uln::new_uln_config( + 2, + 2, + vector[@123], + vector[@456, @567, @678], + false, + false, + false, + ); + configuration::set_default_receive_uln_config(22222, default_config); + + let guid = compute_guid( + 123456, + 22222, + bytes32::from_address(@0x654321), + 33333, + bytes32::from_address(@0x123456) + ); + let header = packet_v1_codec::new_packet_v1( + 22222, + bytes32::from_address(@0x654321), + 33333, + bytes32::from_address(@0x123456), + 123456, + guid, + b"", + ); + let config = get_receive_uln_config_from_packet_header(&header); + assert!(config == default_config, 0); + } + + + #[test] + fun test_verify_and_reclaim_storage_reclaims_when_verified() { + setup(); + + let config = configs_uln::new_uln_config( + 2, + 0, + vector[@123], + vector[], + false, + false, + false, + ); + configuration::set_default_receive_uln_config(1, config); + + let dvn_address = @123; + let payload_hash = bytes32::keccak256(b"payload"); + verify( + dvn_address, + bytes_to_raw_packet(b"header"), + payload_hash, + 2, + ); + + let header_hash = bytes32::keccak256(b"header"); + + // Confirmations should be present before reclaiming + assert!(uln_302_store::has_verification_confirmations(header_hash, payload_hash, dvn_address), 0); + assert!(uln_302_store::get_verification_confirmations(header_hash, payload_hash, dvn_address) == 2, 0); + + verify_and_reclaim_storage( + &config, + header_hash, + payload_hash, + ); + + // Confirmations should be removed after reclaiming + assert!(!uln_302_store::has_verification_confirmations(header_hash, payload_hash, dvn_address), 0); + + let verifiable = check_verifiable( + &config, + bytes32::keccak256(b"header"), + bytes32::keccak256(b"payload_hash"), + ); + + assert!(!verifiable, 0); + } + + #[test] + fun test_reclaim_storage_removes_required_and_optional_confirmations() { + setup(); + + let required_dvns = vector[@123, @456]; + let optional_dvns = vector[@789]; + let header = bytes_to_raw_packet(b"header"); + let header_hash = bytes32::keccak256(get_packet_bytes(header)); + let payload_hash = bytes32::keccak256(b"payload_hash"); + + verify(@123, header, payload_hash, 2); + verify(@456, header, payload_hash, 2); + verify(@789, header, payload_hash, 2); + + // Confirmations should be present before reclaiming + assert!(uln_302_store::has_verification_confirmations(header_hash, payload_hash, @123), 0); + assert!(uln_302_store::has_verification_confirmations(header_hash, payload_hash, @456), 1); + assert!(uln_302_store::has_verification_confirmations(header_hash, payload_hash, @789), 2); + + reclaim_storage(&required_dvns, &optional_dvns, header_hash, payload_hash); + + // Confirmations should be removed after reclaiming + assert!(!uln_302_store::has_verification_confirmations(header_hash, payload_hash, @123), 3); + assert!(!uln_302_store::has_verification_confirmations(header_hash, payload_hash, @456), 4); + assert!(!uln_302_store::has_verification_confirmations(header_hash, payload_hash, @789), 4); + } + + #[test] + fun test_check_verifiable_returns_true_verified_larger_set() { + setup(); + + let config = configs_uln::new_uln_config( + 2, + 2, + vector[@123], + vector[@456, @567, @678], + false, + false, + false, + ); + configuration::set_default_receive_uln_config(1, config); + + let header = bytes_to_raw_packet(b"header"); + verify(@123, header, bytes32::keccak256(b"payload_hash"), 2); + verify(@456, header, bytes32::keccak256(b"payload_hash"), 3); + verify(@567, header, bytes32::keccak256(b"payload_hash"), 2); + + let verifiable = check_verifiable( + &config, + bytes32::keccak256(b"header"), + bytes32::keccak256(b"payload_hash"), + ); + + assert!(verifiable, 0); + } +} \ No newline at end of file diff --git a/packages/layerzero-v2/initia/contracts/msglib/msglib_types/Move.toml b/packages/layerzero-v2/initia/contracts/msglib/msglib_types/Move.toml new file mode 100644 index 00000000..8989e32f --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/msglib/msglib_types/Move.toml @@ -0,0 +1,26 @@ +[package] +name = "msglib_types" +version = "1.0.0" +authors = [] + +[addresses] +msglib_types = "_" +endpoint_v2_common = "_" +endpoint_v2 = "_" +layerzero_admin = "_" +layerzero_treasury_admin = "_" + +[dev-addresses] +msglib_types = "0x97324123" +endpoint_v2_common = "0x9098" +endpoint_v2 = "0x12345678" +layerzero_admin = "0x18943124" +layerzero_treasury_admin = "0x1894312499" + +[dependencies.InitiaStdlib] +git = "https://github.com/initia-labs/move-natives.git" +rev = "77d5f3e140143bdaa41f850115b3035c134193e3" +subdir = "initia_stdlib" + +[dependencies] +endpoint_v2_common = { local = "../../endpoint_v2_common" } diff --git a/packages/layerzero-v2/initia/contracts/msglib/msglib_types/sources/configs_executor.move b/packages/layerzero-v2/initia/contracts/msglib/msglib_types/sources/configs_executor.move new file mode 100644 index 00000000..b1f2fc53 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/msglib/msglib_types/sources/configs_executor.move @@ -0,0 +1,36 @@ +/// This module contains the serialization and deserialization logic for handling Executor configurations +/// +/// The serialized format is as follows: +/// [max_message_size: u32] +/// [executor_address: bytes32] +module msglib_types::configs_executor { + use endpoint_v2_common::serde::{append_address, append_u32, extract_address, extract_u32}; + + struct ExecutorConfig has drop, copy, store { + max_message_size: u32, + executor_address: address, + } + + public fun new_executor_config(max_message_size: u32, executor_address: address): ExecutorConfig { + ExecutorConfig { max_message_size, executor_address } + } + + // =================================================== Accessors ================================================== + + public fun get_max_message_size(self: &ExecutorConfig): u32 { self.max_message_size } + + public fun get_executor_address(self: &ExecutorConfig): address { self.executor_address } + + // ======================================== Serialization / Deserialization ======================================= + + public fun append_executor_config(bytes: &mut vector, config: ExecutorConfig) { + append_u32(bytes, config.max_message_size); + append_address(bytes, config.executor_address); + } + + public fun extract_executor_config(bytes: &vector, position: &mut u64): ExecutorConfig { + let max_message_size = extract_u32(bytes, position); + let executor_address = extract_address(bytes, position); + ExecutorConfig { max_message_size, executor_address } + } +} \ No newline at end of file diff --git a/packages/layerzero-v2/initia/contracts/msglib/msglib_types/sources/configs_uln.move b/packages/layerzero-v2/initia/contracts/msglib/msglib_types/sources/configs_uln.move new file mode 100644 index 00000000..77c67294 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/msglib/msglib_types/sources/configs_uln.move @@ -0,0 +1,127 @@ +/// This module contains the serialization and deserialization logic for handling Send and Receive ULN configurations +module msglib_types::configs_uln { + use std::vector; + + use endpoint_v2_common::serde::{ + append_address, append_u64, append_u8, extract_address, extract_u64, extract_u8, map_count, + }; + + struct UlnConfig has drop, copy, store { + confirmations: u64, + optional_dvn_threshold: u8, + required_dvns: vector
, + optional_dvns: vector
, + use_default_for_confirmations: bool, + use_default_for_required_dvns: bool, + use_default_for_optional_dvns: bool, + } + + public fun new_uln_config( + confirmations: u64, + optional_dvn_threshold: u8, + required_dvns: vector
, + optional_dvns: vector
, + use_default_for_confirmations: bool, + use_default_for_required_dvns: bool, + use_default_for_optional_dvns: bool, + ): UlnConfig { + UlnConfig { + confirmations, + optional_dvn_threshold, + required_dvns, + optional_dvns, + use_default_for_confirmations, + use_default_for_required_dvns, + use_default_for_optional_dvns, + } + } + + // ================================================ Field Accessors =============================================== + + public fun unpack_uln_config(config: UlnConfig): (u64, u8, vector
, vector
, bool, bool, bool) { + let UlnConfig { + confirmations, + optional_dvn_threshold, + required_dvns, + optional_dvns, + use_default_for_confirmations, + use_default_for_required_dvns, + use_default_for_optional_dvns, + } = config; + ( + confirmations, optional_dvn_threshold, required_dvns, optional_dvns, use_default_for_confirmations, + use_default_for_required_dvns, use_default_for_optional_dvns, + ) + } + + public fun get_confirmations(self: &UlnConfig): u64 { self.confirmations } + + public fun get_required_dvn_count(self: &UlnConfig): u64 { vector::length(&self.required_dvns) } + + public fun get_optional_dvn_count(self: &UlnConfig): u64 { vector::length(&self.optional_dvns) } + + public fun get_optional_dvn_threshold(self: &UlnConfig): u8 { self.optional_dvn_threshold } + + public fun get_required_dvns(self: &UlnConfig): vector
{ self.required_dvns } + + public fun borrow_required_dvns(self: &UlnConfig): &vector
{ &self.required_dvns } + + public fun get_optional_dvns(self: &UlnConfig): vector
{ self.optional_dvns } + + public fun borrow_optional_dvns(self: &UlnConfig): &vector
{ &self.optional_dvns } + + public fun get_use_default_for_confirmations(self: &UlnConfig): bool { self.use_default_for_confirmations } + + public fun get_use_default_for_required_dvns(self: &UlnConfig): bool { self.use_default_for_required_dvns } + + public fun get_use_default_for_optional_dvns(self: &UlnConfig): bool { self.use_default_for_optional_dvns } + + + // ======================================== Serialization / Deserialization ======================================= + + public fun append_uln_config(target: &mut vector, config: UlnConfig) { + append_u64(target, config.confirmations); + append_u8(target, config.optional_dvn_threshold); + append_u8(target, (vector::length(&config.required_dvns) as u8)); + vector::for_each(config.required_dvns, |address| append_address(target, address)); + append_u8(target, (vector::length(&config.optional_dvns) as u8)); + vector::for_each(config.optional_dvns, |address| append_address(target, address)); + append_u8(target, from_bool(config.use_default_for_confirmations)); + append_u8(target, from_bool(config.use_default_for_required_dvns)); + append_u8(target, from_bool(config.use_default_for_optional_dvns)); + } + + public fun extract_uln_config(input: &vector, position: &mut u64): UlnConfig { + let confirmations = extract_u64(input, position); + let optional_dvn_threshold = extract_u8(input, position); + let required_dvns_count = extract_u8(input, position); + let required_dvns = map_count((required_dvns_count as u64), |_i| extract_address(input, position)); + let optional_dvns_count = extract_u8(input, position); + let optional_dvns = map_count((optional_dvns_count as u64), |_i| extract_address(input, position)); + let use_default_for_confirmations = to_bool(extract_u8(input, position)); + let use_default_for_required_dvns = to_bool(extract_u8(input, position)); + let use_default_for_optional_dvns = to_bool(extract_u8(input, position)); + + UlnConfig { + confirmations, + optional_dvn_threshold, + required_dvns, + optional_dvns, + use_default_for_confirmations, + use_default_for_required_dvns, + use_default_for_optional_dvns, + } + } + + fun to_bool(uint: u8): bool { + if (uint == 1) { true } else if (uint == 0) { false } else { abort EINVALID_BOOLEAN } + } + + fun from_bool(bool: bool): u8 { + if (bool) { 1 } else { 0 } + } + + // ================================================== Error Codes ================================================= + + const EINVALID_BOOLEAN: u64 = 1; +} \ No newline at end of file diff --git a/packages/layerzero-v2/initia/contracts/msglib/msglib_types/sources/dvn_verify_params.move b/packages/layerzero-v2/initia/contracts/msglib/msglib_types/sources/dvn_verify_params.move new file mode 100644 index 00000000..37e907c4 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/msglib/msglib_types/sources/dvn_verify_params.move @@ -0,0 +1,29 @@ +/// These DVN Verify Params are used to verify the DVN packet for ULN302. +/// The format of this may change in future Message Libraries, so is sent through the Message Library router as an +/// `Any` type to ensure the format can be upgraded for future Message Libraries. +module msglib_types::dvn_verify_params { + use std::any::{Self, Any}; + + use endpoint_v2_common::bytes32::Bytes32; + use endpoint_v2_common::packet_raw::RawPacket; + + struct DvnVerifyParams has drop, store { + packet_header: RawPacket, + payload_hash: Bytes32, + confirmations: u64, + } + + public fun pack_dvn_verify_params( + packet_header: RawPacket, + payload_hash: Bytes32, + confirmations: u64, + ): Any { + any::pack(DvnVerifyParams { packet_header, payload_hash, confirmations }) + } + + public fun unpack_dvn_verify_params(params: Any): (RawPacket, Bytes32, u64) { + let params = any::unpack(params); + let DvnVerifyParams { packet_header, payload_hash, confirmations } = params; + (packet_header, payload_hash, confirmations) + } +} diff --git a/packages/layerzero-v2/initia/contracts/msglib/msglib_types/sources/worker_options.move b/packages/layerzero-v2/initia/contracts/msglib/msglib_types/sources/worker_options.move new file mode 100644 index 00000000..4dfebb5b --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/msglib/msglib_types/sources/worker_options.move @@ -0,0 +1,226 @@ +module msglib_types::worker_options { + use std::vector; + + use endpoint_v2_common::serde; + + public inline fun EXECUTOR_WORKER_ID(): u8 { 1 } + + public inline fun DVN_WORKER_ID(): u8 { 2 } + + const EXECUTOR_OPTION_TYPE_LZ_RECEIVE: u8 = 1; + const EXECUTOR_OPTION_TYPE_NATIVE_DROP: u8 = 2; + + /// Convenience structure to bind the DVN index and the serialized (concatted) options for that DVN index + struct IndexOptionsPair has copy, drop, store { + options: vector, + dvn_idx: u8, + } + + /// Unpacks the DVN index and the serialized (concatted) options for that DVN index + public fun unpack_index_option_pair(pair: IndexOptionsPair): (u8, vector) { + let IndexOptionsPair { options, dvn_idx } = pair; + (dvn_idx, options) + } + + /// Searches a vector of IndexOptionsPair and returns the concatinated options that matches the given DVN index + /// This returns an empty vector if no match is found + public fun get_matching_options(index_option_pairs: &vector, dvn_index: u8): vector { + for (i in 0..vector::length(index_option_pairs)) { + let pair = vector::borrow(index_option_pairs, i); + if (pair.dvn_idx == dvn_index) { + return pair.options + } + }; + vector[] + } + + // ============================================== Process DNV Options ============================================= + + /// Split Options into Executor and DVN Options + /// @return (executor_options, dvn_options) + public fun extract_and_split_options( + options: &vector, + ): (vector, vector) { + // Options must contain at least 2 bytes (the u16 "option type") to be considered valid + assert!(vector::length(options) >= 2, EINVALID_OPTIONS); + + let uln_options_type = serde::extract_u16(options, &mut 0); + + if (uln_options_type == 3) { + extract_type_3_options(options) + } else { + extract_legacy_options(uln_options_type, options) + } + } + + /// Extracts the current type 3 option format + /// Format: [worker_option][worker_option][worker_option]... + /// Worker Option Format: [worker_id: u8][option_size: u16][option: bytes(option_size)] + /// @return (executor_options, dvn_options) + public fun extract_type_3_options( + options: &vector, + ): (vector, vector) { + // start after the u16 option type + let position: u64 = 2; + + let executor_options = vector[]; + let dvn_options = vector[]; + + // serde extract methods will move the position cursor according to the size of the extracted value + let len = vector::length(options); + while (position < len) { + let internal_cursor = position; + let worker_id = serde::extract_u8(options, &mut internal_cursor); + let option_size = serde::extract_u16(options, &mut internal_cursor); + let total_option_size = (option_size as u64) + 3; // 1 byte for worker_id, 2 bytes for option_size + let option_bytes = serde::extract_fixed_len_bytes(options, &mut position, total_option_size); + if (worker_id == EXECUTOR_WORKER_ID()) { + vector::append(&mut executor_options, option_bytes); + } else if (worker_id == DVN_WORKER_ID()) { + vector::append(&mut dvn_options, option_bytes); + } else { + abort EINVALID_WORKER_ID + }; + }; + + (executor_options, dvn_options) + } + + /// This creates a stem for type 3 options after which a series of executor and/or DVN options can be appended + public fun new_empty_type_3_options(): vector { + x"0003" // type 3 + } + + #[test_only] + /// Test only function to append an executor option to a buffer. This is only for testing the general behavior + /// when the options don't matter. Please use the method provided by the executor fee lib to append fee-lib-specific + /// executor options when not testing + public fun append_generic_type_3_executor_option( + buf: &mut vector, + option: vector, + ) { + serde::append_u8(buf, EXECUTOR_WORKER_ID()); + serde::append_u16(buf, (vector::length(&option) as u16)); + serde::append_bytes(buf, option); + } + + // ============================================ Process Legacy Options ============================================ + + /// Extracts options in legacy formats + /// @return (executor_options, dvn_options) + public fun extract_legacy_options(option_type: u16, options: &vector): (vector, vector) { + // start after the u16 option type + let position: u64 = 2; // skip the option type + let total_options_size = vector::length(options); + let executor_options = vector[]; + + // type 1 and 2 lzReceive options use u256 but type 3 uses u128 + // casting operation is safe: will abort if too large + if (option_type == 1) { + assert!(total_options_size == 34, EINVALID_LEGACY_OPTIONS_TYPE_1); + let execution_gas = (serde::extract_u256(options, &mut position) as u128); + append_legacy_option_lz_receive(&mut executor_options, execution_gas); + } else if (option_type == 2) { + assert!(total_options_size > 66 && total_options_size <= 98, EINVALID_LEGACY_OPTIONS_TYPE_2); + let execution_gas = (serde::extract_u256(options, &mut position) as u128); + + // native_drop (amount + receiver) + let amount = (serde::extract_u256(options, &mut position) as u128); + // receiver addresses are not necessarily bytes32 + let receiver = serde::extract_bytes_until_end(options, &mut position); + receiver = serde::pad_zero_left(receiver, 32); + + append_legacy_option_lz_receive(&mut executor_options, execution_gas); + append_legacy_option_native_drop(&mut executor_options, amount, receiver); + } else { + abort EINVALID_OPTION_TYPE + }; + (executor_options, vector[]) + } + + fun append_legacy_option_lz_receive(buf: &mut vector, execution_gas: u128) { + serde::append_u8(buf, EXECUTOR_WORKER_ID()); + serde::append_u16(buf, 17); // 16 + 1, 16 for option_length, + 1 for option_type + serde::append_u8(buf, EXECUTOR_OPTION_TYPE_LZ_RECEIVE); + serde::append_u128(buf, execution_gas); + } + + fun append_legacy_option_native_drop(buf: &mut vector, amount: u128, receiver: vector) { + serde::append_u8(buf, EXECUTOR_WORKER_ID()); + serde::append_u16(buf, 49); // 48 + 1, 32 + 16 for option_length, + 1 for option_type + serde::append_u8(buf, EXECUTOR_OPTION_TYPE_NATIVE_DROP); + serde::append_u128(buf, amount); + serde::append_bytes(buf, receiver); + } + + // ====================================== Prepare DVN Options for Fee Library ===================================== + + /// Group DVN Options into IndexOptionsPairs, such that each element has a DVN index and a concatted vector of + /// serialized options + /// serialized options + /// Format: { dvn_idx: u8, options: [dvn_option][dvn_option][dvn_option]... + /// DVN Option format: [worker_id][option_size][dvn_idx][option_type][option] + public fun group_dvn_options_by_index(dvn_options_bytes: &vector): vector { + let index_option_pairs = vector[]; + let position: u64 = 0; + let len = vector::length(dvn_options_bytes); + while (position < len) { + let internal_cursor = position; + internal_cursor = internal_cursor + 1; // skip worker_id + let option_size = serde::extract_u16(dvn_options_bytes, &mut internal_cursor); + let dvn_idx = serde::extract_u8(dvn_options_bytes, &mut internal_cursor); + let total_option_size = (option_size as u64) + 3; // 1 byte for worker_id, 2 bytes for option_size + + let option = serde::extract_fixed_len_bytes(dvn_options_bytes, &mut position, total_option_size); + + assert!(option_size >= 2, EINVALID_OPTION_LENGTH); + assert!(dvn_idx != 255, EINVALID_DVN_IDX); + insert_dvn_option(&mut index_option_pairs, dvn_idx, option); + }; + + index_option_pairs + } + + /// Inserts a new DVN option into the vector of IndexOptionsPair, appending to the existing options of the DVN index + /// or creating a new entry if the DVN index does not exist + fun insert_dvn_option( + index_option_pairs: &mut vector, + dvn_idx: u8, + new_options: vector, + ) { + // If the dvn_idx already exists, append the new options to the existing options + let count = vector::length(index_option_pairs); + for (ii in 0..count) { + // Reverse the scan, to save gas when options are appended in ordered groups + let i = count - ii - 1; + let pair = vector::borrow(index_option_pairs, i); + if (pair.dvn_idx == dvn_idx) { + let existing_option = vector::borrow_mut(index_option_pairs, i); + vector::append(&mut existing_option.options, new_options); + return + } + }; + // Otherwise, create a new entry + vector::push_back(index_option_pairs, IndexOptionsPair { options: new_options, dvn_idx }); + } + + // This appends a dvn_option to the buffer + public fun append_dvn_option(buf: &mut vector, dvn_idx: u8, option_type: u8, option: vector) { + serde::append_u8(buf, DVN_WORKER_ID()); + let length = vector::length(&option) + 2; // 2 for option_type and dvn_idx + serde::append_u16(buf, (length as u16)); + serde::append_u8(buf, dvn_idx); + serde::append_u8(buf, option_type); + serde::append_bytes(buf, option); + } + + // ================================================== Error Codes ================================================= + + const EINVALID_DVN_IDX: u64 = 1; + const EINVALID_LEGACY_OPTIONS_TYPE_1: u64 = 2; + const EINVALID_LEGACY_OPTIONS_TYPE_2: u64 = 3; + const EINVALID_OPTIONS: u64 = 4; + const EINVALID_OPTION_LENGTH: u64 = 5; + const EINVALID_OPTION_TYPE: u64 = 6; + const EINVALID_WORKER_ID: u64 = 7; +} \ No newline at end of file diff --git a/packages/layerzero-v2/initia/contracts/msglib/msglib_types/tests/test_dvn_verify_params.move b/packages/layerzero-v2/initia/contracts/msglib/msglib_types/tests/test_dvn_verify_params.move new file mode 100644 index 00000000..4c6ac174 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/msglib/msglib_types/tests/test_dvn_verify_params.move @@ -0,0 +1,21 @@ +#[test_only] +module msglib_types::test_dvn_verify_params { + use endpoint_v2_common::bytes32; + use endpoint_v2_common::packet_raw; + + #[test] + fun test_dvn_verify_params_pack_and_unpack() { + let original_header = packet_raw::bytes_to_raw_packet(b"1234"); + let packed = msglib_types::dvn_verify_params::pack_dvn_verify_params( + original_header, + bytes32::to_bytes32(b"12345678901234567890123456789012"), + 42, + ); + let (packet_header, payload_hash, confirmations) = msglib_types::dvn_verify_params::unpack_dvn_verify_params( + packed, + ); + assert!(packet_header == original_header, 0); + assert!(payload_hash == bytes32::to_bytes32(b"12345678901234567890123456789012"), 1); + assert!(confirmations == 42, 2); + } +} diff --git a/packages/layerzero-v2/initia/contracts/msglib/msglib_types/tests/worker_options_tests.move b/packages/layerzero-v2/initia/contracts/msglib/msglib_types/tests/worker_options_tests.move new file mode 100644 index 00000000..2e7795a3 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/msglib/msglib_types/tests/worker_options_tests.move @@ -0,0 +1,159 @@ +#[test_only] +module msglib_types::worker_options_tests { + use std::vector; + + use endpoint_v2_common::serde; + use msglib_types::worker_options; + use msglib_types::worker_options::unpack_index_option_pair; + + #[test] + fun test_extract_and_split_options_dvn_only() { + let option_type = x"0003"; + let dvn_options_raw = x"020002000102000302ff0102000200010200020101"; + let options = serde::flatten(vector[ + option_type, + dvn_options_raw, + ]); + let (executor_options, dvn_options) = worker_options::extract_and_split_options( + &options, + ); + + assert!(executor_options == x"", 0); + assert!(dvn_options == dvn_options_raw, 1); + } + + #[test] + fun test_extract_and_split_options_executor_only() { + let option_type = x"0003"; + let executor_options_raw = x"0100110100000000000000000000000000009470010011010000000000000000000000000000ea60"; + let options = serde::flatten(vector[ + option_type, + executor_options_raw, + ]); + let (executor_options, dvn_options) = worker_options::extract_and_split_options( + &options, + ); + + assert!(executor_options == executor_options_raw, 0); + assert!(dvn_options == x"", 1); + } + + #[test] + fun test_extract_and_split_options() { + let option_type = x"0003"; + let executor_options_raw = x"0100110100000000000000000000000000009470010011010000000000000000000000000000ea60"; + let dvn_options_raw = x"020002000102000302ff0102000200010200020101"; + let options = serde::flatten(vector[ + option_type, + executor_options_raw, + dvn_options_raw, + ]); + + let (executor_options, dvn_options) = worker_options::extract_and_split_options( + &options, + ); + + assert!(executor_options == executor_options_raw, 0); + assert!(dvn_options == dvn_options_raw, 1); + } + + #[test] + fun test_decode_legacy_options_type_1() { + let option_type = 1; + let legacy_options = x"00010000000000000000000000000000000000000000000000000000000000030d40"; + let expected_options = x"0100110100000000000000000000000000030d40"; + + let (executor_options, _) = worker_options::extract_legacy_options(option_type, &legacy_options); + // assert that the new executor option follows: [worker_id][option_size][option_type][option] + assert!(executor_options == expected_options, 0); + let pos = &mut 0; + assert!(serde::extract_u8(&executor_options, pos) == 1, 1); // worker_id + assert!(serde::extract_u16(&executor_options, pos) == 17, 2); // option_size + assert!(serde::extract_u8(&executor_options, pos) == 1, 3); // option_type + assert!(serde::extract_u128(&executor_options, pos) == 200000, 4); // option value (execution gas) + } + + #[test] + fun test_decode_legacy_options_type_2() { + let option_type = 2; + let legacy_options = x"00020000000000000000000000000000000000000000000000000000000000030d400000000000000000000000000000000000000000000000000000000000989680f39fd6e51aad88f6f4ce6ab8827279cfffb92266"; + let expected_options = x"0100110100000000000000000000000000030d400100310200000000000000000000000000989680000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266"; + let (executor_options, _) = worker_options::extract_legacy_options(option_type, &legacy_options); + + // adapter params type 2 includes both 1 and 2 + assert!(executor_options == expected_options, 0); + let pos = &mut 0; + // adapter params type 1 + assert!(serde::extract_u8(&executor_options, pos) == 1, 1); // worker_id + assert!(serde::extract_u16(&executor_options, pos) == 17, 2); // option_size + assert!(serde::extract_u8(&executor_options, pos) == 1, 3); // option_type + assert!(serde::extract_u128(&executor_options, pos) == 200000, 4); // option value (execution gas) + // adapter params type 2 + assert!(serde::extract_u8(&executor_options, pos) == 1, 5); // worker_id + assert!(serde::extract_u16(&executor_options, pos) == 49, 6); // option_size + assert!(serde::extract_u8(&executor_options, pos) == 2, 7); // option_type + assert!(serde::extract_u128(&executor_options, pos) == 10000000, 8); // option value (amount) + let expected_receiver = endpoint_v2_common::bytes32::to_bytes32( + x"000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266" + ); + assert!(serde::extract_bytes32(&executor_options, pos) == expected_receiver, 9); // option value (receiver) + } + + #[test] + fun test_extract_and_split_options_using_legacy_option() { + let legacy_options = x"00020000000000000000000000000000000000000000000000000000000000030d400000000000000000000000000000000000000000000000000000000000989680f39fd6e51aad88f6f4ce6ab8827279cfffb92266"; + let expected_options = x"0100110100000000000000000000000000030d400100310200000000000000000000000000989680000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266"; + let (executor_options, _) = worker_options::extract_and_split_options(&legacy_options); + + // adapter params type 2 includes both 1 and 2 + assert!(executor_options == expected_options, 0); + let pos = &mut 0; + // adapter params type 1 + assert!(serde::extract_u8(&executor_options, pos) == 1, 1); // worker_id + assert!(serde::extract_u16(&executor_options, pos) == 17, 2); // option_size + assert!(serde::extract_u8(&executor_options, pos) == 1, 3); // option_type + assert!(serde::extract_u128(&executor_options, pos) == 200000, 4); // option value (execution gas) + // adapter params type 2 + assert!(serde::extract_u8(&executor_options, pos) == 1, 5); // worker_id + assert!(serde::extract_u16(&executor_options, pos) == 49, 6); // option_size + assert!(serde::extract_u8(&executor_options, pos) == 2, 7); // option_type + assert!(serde::extract_u128(&executor_options, pos) == 10000000, 8); // option value (amount) + let expected_receiver = endpoint_v2_common::bytes32::to_bytes32( + x"000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266" + ); + assert!(serde::extract_bytes32(&executor_options, pos) == expected_receiver, 9); // option value (receiver) + } + + #[test] + fun test_group_dvn_options_by_index() { + let dvn_option_bytes = x"020002000102000302ff0102000200010200020101"; + let expected_dvn_0_options = x"02000200010200020001"; + let expected_dvn_1_options = x"0200020101"; + let expected_dvn_2_options = x"02000302ff01"; + + let pairs = worker_options::group_dvn_options_by_index(&dvn_option_bytes); + + + let found_0 = false; + let found_1 = false; + let found_2 = false; + + // get_all_dvn_fee logic check + for (i in 0..vector::length(&pairs)) { + let (index, option) = unpack_index_option_pair(*vector::borrow(&pairs, i)); + if (index == 0) { + found_0 = true; + assert!(option == expected_dvn_0_options, 0); + }; + if (index == 1) { + found_1 = true; + assert!(option == expected_dvn_1_options, 1); + }; + if (index == 2) { + found_2 = true; + assert!(option == expected_dvn_2_options, 2); + }; + }; + assert!(found_0 && found_1 && found_2, 3); + } +} diff --git a/packages/layerzero-v2/initia/contracts/msglib/routers/router_node_0/Move.toml b/packages/layerzero-v2/initia/contracts/msglib/routers/router_node_0/Move.toml new file mode 100644 index 00000000..cd2c2f5b --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/msglib/routers/router_node_0/Move.toml @@ -0,0 +1,65 @@ +[package] +name = "router_node_0" +version = "1.0.0" +authors = [] + +[addresses] +router_node_0 = "_" +endpoint_v2_common = "_" +endpoint_v2 = "_" +layerzero_admin = "_" +layerzero_treasury_admin = "_" +simple_msglib = "_" +blocked_msglib = "_" +uln_302 = "_" +router_node_1 = "_" +msglib_types = "_" +treasury = "_" +price_feed_router_0 = "_" +price_feed_router_1 = "_" +price_feed_module_0 = "_" +worker_common = "_" +executor_fee_lib_0 = "_" +dvn_fee_lib_0 = "_" +executor_fee_lib_router_0 = "_" +executor_fee_lib_router_1 = "_" +dvn_fee_lib_router_0 = "_" +dvn_fee_lib_router_1 = "_" +dvn = "_" +native_token_metadata_address = "0x8e4733bdabcf7d4afc3d14f0dd46c9bf52fb0fce9e4b996c939e195b8bc891d9" + +[dev-addresses] +router_node_0 = "0x9001" +endpoint_v2_common = "0x9098" +endpoint_v2 = "0x12345678" +layerzero_admin = "0x18943124" +layerzero_treasury_admin = "0x1894312499" +simple_msglib = "0x9002" +blocked_msglib = "0x9003" +uln_302 = "0x9005" +router_node_1 = "0x9007" +msglib_types = "0x52112234" +treasury = "0x123432432" +price_feed_router_0 = "0x65DD71A" +price_feed_router_1 = "0x65DD71AB" +price_feed_module_0 = "0x65DD71" +worker_common = "0x3999" +executor_fee_lib_0 = "0x3000" +dvn_fee_lib_0 = "0x3000a" +executor_fee_lib_router_0 = "0x30001" +executor_fee_lib_router_1 = "0x30002" +dvn_fee_lib_router_0 = "0x30001a" +dvn_fee_lib_router_1 = "0x30002a" +dvn = "0x234234" + +[dependencies] +simple_msglib = { local = "../../libs/simple_msglib" } +blocked_msglib = { local = "../../libs/blocked_msglib" } +uln_302 = { local = "../../libs/uln_302" } + +router_node_1 = { local = "../router_node_1_placeholder" } +msglib_types = { local = "../../msglib_types" } +endpoint_v2_common = { local = "../../../endpoint_v2_common" } +price_feed_module_0 = { local = "../../../worker_peripherals/price_feed_modules/price_feed_module_0" } + +[dev-dependencies] diff --git a/packages/layerzero-v2/initia/contracts/msglib/routers/router_node_0/sources/router_node.move b/packages/layerzero-v2/initia/contracts/msglib/routers/router_node_0/sources/router_node.move new file mode 100644 index 00000000..ab2098b2 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/msglib/routers/router_node_0/sources/router_node.move @@ -0,0 +1,158 @@ +/// This module provides the base branching mechanism for routing to the correct msglib implementation. +/// The design provides multiple msglib slots per router node. Each is checked against the msglib address until the +/// correct implementation is found. If no implementation is found, the request is forwarded to the next router node. +/// The final router node will always be a placeholder contract that will return an error stating that the desired +/// library was not found. +/// Any unused slot points to a upgradable placeholder contract, which makes appending new msglib implementations +/// possible while the router or any msglib contracts can remain permanently undisturbed. +module router_node_0::router_node { + use std::any::Any; + use std::fungible_asset::FungibleAsset; + use std::option::Option; + + use blocked_msglib::router_calls as blocked_msglib; + use endpoint_v2_common::bytes32::Bytes32; + use endpoint_v2_common::contract_identity::DynamicCallRef; + use endpoint_v2_common::packet_raw::RawPacket; + use endpoint_v2_common::send_packet::SendPacket; + use router_node_1::router_node as router_node_next; + use simple_msglib::router_calls as simple_msglib; + use uln_302::router_calls as uln_302; + + public fun quote( + msglib: address, + packet: SendPacket, + options: vector, + pay_in_zro: bool, + ): (u64, u64) { + if (msglib == @uln_302) { + uln_302::quote(packet, options, pay_in_zro) + } else if (msglib == @simple_msglib) { + simple_msglib::quote(packet, options, pay_in_zro) + } else if (msglib == @blocked_msglib) { + blocked_msglib::quote(packet, options, pay_in_zro) + } else { + router_node_next::quote(msglib, packet, options, pay_in_zro) + } + } + + public fun send( + msglib: address, + call_ref: &DynamicCallRef, + packet: SendPacket, + options: vector, + native_token: &mut FungibleAsset, + zro_token: &mut Option, + ): (u64, u64, RawPacket) { + if (msglib == @uln_302) { + uln_302::send(call_ref, packet, options, native_token, zro_token) + } else if (msglib == @simple_msglib) { + simple_msglib::send(call_ref, packet, options, native_token, zro_token) + } else if (msglib == @blocked_msglib) { + blocked_msglib::send(call_ref, packet, options, native_token, zro_token) + } else { + router_node_next::send(msglib, call_ref, packet, options, native_token, zro_token) + } + } + + public fun commit_verification( + msglib: address, + call_ref: &DynamicCallRef, + packet_header: RawPacket, + payload_hash: Bytes32, + extra_data: vector, + ): (address, u32, Bytes32, u64) { + if (msglib == @uln_302) { + uln_302::commit_verification(call_ref, packet_header, payload_hash, extra_data) + } else if (msglib == @simple_msglib) { + simple_msglib::commit_verification(call_ref, packet_header, payload_hash, extra_data) + } else if (msglib == @blocked_msglib) { + blocked_msglib::commit_verification(call_ref, packet_header, payload_hash, extra_data) + } else { + router_node_next::commit_verification(msglib, call_ref, packet_header, payload_hash, extra_data) + } + } + + public fun dvn_verify(msglib: address, call_ref: &DynamicCallRef, params: Any) { + if (msglib == @uln_302) { + uln_302::dvn_verify(call_ref, params) + } else if (msglib == @simple_msglib) { + simple_msglib::dvn_verify(call_ref, params) + } else if (msglib == @blocked_msglib) { + blocked_msglib::dvn_verify(call_ref, params) + } else { + router_node_next::dvn_verify(msglib, call_ref, params) + } + } + + public fun set_config( + msglib: address, + call_ref: &DynamicCallRef, + oapp: address, + eid: u32, + config_type: u32, + config: vector, + ) { + if (msglib == @uln_302) { + uln_302::set_config(call_ref, oapp, eid, config_type, config) + } else if (msglib == @simple_msglib) { + simple_msglib::set_config(call_ref, oapp, eid, config_type, config) + } else if (msglib == @blocked_msglib) { + blocked_msglib::set_config(call_ref, oapp, eid, config_type, config) + } else { + router_node_next::set_config(msglib, call_ref, oapp, eid, config_type, config) + } + } + + #[view] + public fun get_config(msglib: address, oapp: address, eid: u32, config_type: u32): vector { + if (msglib == @uln_302) { + uln_302::get_config(oapp, eid, config_type) + } else if (msglib == @simple_msglib) { + simple_msglib::get_config(oapp, eid, config_type) + } else if (msglib == @blocked_msglib) { + blocked_msglib::get_config(oapp, eid, config_type) + } else { + router_node_next::get_config(msglib, oapp, eid, config_type) + } + } + + #[view] + public fun version(msglib: address): (u64, u8, u8) { + if (msglib == @uln_302) { + uln_302::version() + } else if (msglib == @simple_msglib) { + simple_msglib::version() + } else if (msglib == @blocked_msglib) { + blocked_msglib::version() + } else { + router_node_next::version(msglib) + } + } + + #[view] + public fun is_supported_send_eid(msglib: address, eid: u32): bool { + if (msglib == @uln_302) { + uln_302::is_supported_send_eid(eid) + } else if (msglib == @simple_msglib) { + simple_msglib::is_supported_send_eid(eid) + } else if (msglib == @blocked_msglib) { + blocked_msglib::is_supported_send_eid(eid) + } else { + router_node_next::is_supported_send_eid(msglib, eid) + } + } + + #[view] + public fun is_supported_receive_eid(msglib: address, eid: u32): bool { + if (msglib == @uln_302) { + uln_302::is_supported_receive_eid(eid) + } else if (msglib == @simple_msglib) { + simple_msglib::is_supported_receive_eid(eid) + } else if (msglib == @blocked_msglib) { + blocked_msglib::is_supported_receive_eid(eid) + } else { + router_node_next::is_supported_receive_eid(msglib, eid) + } + } +} \ No newline at end of file diff --git a/packages/layerzero-v2/initia/contracts/msglib/routers/router_node_1_placeholder/Move.toml b/packages/layerzero-v2/initia/contracts/msglib/routers/router_node_1_placeholder/Move.toml new file mode 100644 index 00000000..f6186846 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/msglib/routers/router_node_1_placeholder/Move.toml @@ -0,0 +1,27 @@ +[package] +name = "router_node_1" +version = "1.0.0" +authors = [] + +[addresses] +router_node_1 = "_" +endpoint_v2_common = "_" +endpoint_v2 = "_" +layerzero_admin = "_" +layerzero_treasury_admin = "_" +msglib_types = "_" +worker_common = "_" + +[dev-addresses] +router_node_1 = "0x9007" +endpoint_v2_common = "0x9098" +endpoint_v2 = "0x12345678" +layerzero_admin = "0x18943124" +layerzero_treasury_admin = "0x1894312499" +msglib_types = "0x521234" +worker_common = "0x3204817234" + +[dependencies] +msglib_types = { local = "../../msglib_types" } +endpoint_v2_common = { local = "../../../endpoint_v2_common" } +worker_common = { local = "../../../worker_peripherals/worker_common" } \ No newline at end of file diff --git a/packages/layerzero-v2/initia/contracts/msglib/routers/router_node_1_placeholder/sources/router_node.move b/packages/layerzero-v2/initia/contracts/msglib/routers/router_node_1_placeholder/sources/router_node.move new file mode 100644 index 00000000..c588c120 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/msglib/routers/router_node_1_placeholder/sources/router_node.move @@ -0,0 +1,81 @@ +module router_node_1::router_node { + use std::any::Any; + use std::fungible_asset::FungibleAsset; + use std::option::Option; + + use endpoint_v2_common::bytes32::Bytes32; + use endpoint_v2_common::contract_identity::DynamicCallRef; + use endpoint_v2_common::packet_raw::RawPacket; + use endpoint_v2_common::send_packet::SendPacket; + + const ENOT_IMPLEMENTED: u64 = 1; + + public fun quote( + _msglib: address, + _packet: SendPacket, + _options: vector, + _pay_in_zro: bool, + ): (u64, u64) { + abort ENOT_IMPLEMENTED + } + + public fun send( + _msglib: address, + _call_ref: &DynamicCallRef, + _packet: SendPacket, + _options: vector, + _native_token: &mut FungibleAsset, + _zro_token: &mut Option, + ): (u64, u64, RawPacket) { + abort ENOT_IMPLEMENTED + } + + public fun commit_verification( + _msglib: address, + _call_ref: &DynamicCallRef, + _packet_header: RawPacket, + _payload_hash: Bytes32, + _extra_data: vector, + ): (address, u32, Bytes32, u64) { + abort ENOT_IMPLEMENTED + } + + public fun dvn_verify( + _msglib: address, + _call_ref: &DynamicCallRef, + _params: Any, + ) { + abort ENOT_IMPLEMENTED + } + + public fun set_config( + _msglib: address, + _call_ref: &DynamicCallRef, + _oapp: address, + _eid: u32, + _config_type: u32, + _config: vector, + ) { + abort ENOT_IMPLEMENTED + } + + #[view] + public fun get_config(_msglib: address, _oapp: address, _eid: u32, _config_type: u32): vector { + abort ENOT_IMPLEMENTED + } + + #[view] + public fun version(_msglib: address): (u64, u8, u8) { + abort ENOT_IMPLEMENTED + } + + #[view] + public fun is_supported_send_eid(_msglib: address, _eid: u32): bool { + abort ENOT_IMPLEMENTED + } + + #[view] + public fun is_supported_receive_eid(_msglib: address, _eid: u32): bool { + abort ENOT_IMPLEMENTED + } +} diff --git a/packages/layerzero-v2/initia/contracts/oapps/oft_common/Move.toml b/packages/layerzero-v2/initia/contracts/oapps/oft_common/Move.toml new file mode 100644 index 00000000..c140a089 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/oapps/oft_common/Move.toml @@ -0,0 +1,26 @@ +[package] +name = "oft_common" +version = "1.0.0" +authors = [] + +[addresses] +oft_common = "_" +endpoint_v2_common = "_" +endpoint_v2 = "_" +layerzero_admin = "_" +layerzero_treasury_admin = "_" + +[dev-addresses] +oft_common = "0x302814823" +endpoint_v2_common = "0x9098" +endpoint_v2 = "0x12345678" +layerzero_admin = "0x18943124" +layerzero_treasury_admin = "0x123456231423" + +[dependencies.InitiaStdlib] +git = "https://github.com/initia-labs/move-natives.git" +rev = "77d5f3e140143bdaa41f850115b3035c134193e3" +subdir = "initia_stdlib" + +[dependencies] +endpoint_v2_common = { local = "../../endpoint_v2_common" } diff --git a/packages/layerzero-v2/initia/contracts/oapps/oft_common/sources/oft_compose_msg_codec.move b/packages/layerzero-v2/initia/contracts/oapps/oft_common/sources/oft_compose_msg_codec.move new file mode 100644 index 00000000..ea4df400 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/oapps/oft_common/sources/oft_compose_msg_codec.move @@ -0,0 +1,63 @@ +/// This module provides functions to encode and decode OFT compose messages +module oft_common::oft_compose_msg_codec { + use std::vector; + + use endpoint_v2_common::bytes32::Bytes32; + use endpoint_v2_common::serde; + + const NONCE_OFFSET: u64 = 0; + const SRC_EID_OFFSET: u64 = 8; + const AMOUNT_LD_OFFSET: u64 = 12; + const COMPOSE_FROM_OFFSET: u64 = 44; + const COMPOSE_MSG_OFFSET: u64 = 76; + + /// Encode a compose message into a byte vector + /// @param nonce: The nonce of the LayerZero message that contains the compose message + /// @param src_eid: The source endpoint ID of the compose message + /// @param amount_ld: The amount in local decimals of the compose message + /// @param compose_payload: The compose message to encode [compose_payload_from][compose_payload_message] + public fun encode( + nonce: u64, + src_eid: u32, + amount_ld: u64, + compose_payload: vector, + ): vector { + let encoded = vector[]; + serde::append_u64(&mut encoded, nonce); + serde::append_u32(&mut encoded, src_eid); + serde::append_u256(&mut encoded, (amount_ld as u256)); + serde::append_bytes(&mut encoded, compose_payload); + encoded + } + + /// Get the nonce from an encoded compose message + public fun nonce(encoded: &vector): u64 { + serde::extract_u64(encoded, &mut NONCE_OFFSET) + } + + /// Get the source endpoint ID from an encoded compose message + public fun src_eid(encoded: &vector): u32 { + serde::extract_u32(encoded, &mut SRC_EID_OFFSET) + } + + /// Get the amount in local decimals from an encoded compose message + public fun amount_ld(encoded: &vector): u64 { + (serde::extract_u256(encoded, &mut AMOUNT_LD_OFFSET) as u64) + } + + /// Get the compose from address from an encoded compose message + public fun compose_payload_from(encoded: &vector): Bytes32 { + assert!(vector::length(encoded) >= COMPOSE_MSG_OFFSET, ENO_COMPOSE_MSG); + serde::extract_bytes32(encoded, &mut COMPOSE_FROM_OFFSET) + } + + /// Get the compose payload from an encoded compose message + public fun compose_payload_message(encoded: &vector): vector { + assert!(vector::length(encoded) >= COMPOSE_MSG_OFFSET, ENO_COMPOSE_MSG); + serde::extract_bytes_until_end(encoded, &mut COMPOSE_MSG_OFFSET) + } + + // ================================================== Error Codes ================================================= + + const ENO_COMPOSE_MSG: u64 = 1; +} diff --git a/packages/layerzero-v2/initia/contracts/oapps/oft_common/sources/oft_fee_detail.move b/packages/layerzero-v2/initia/contracts/oapps/oft_common/sources/oft_fee_detail.move new file mode 100644 index 00000000..98cfddd8 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/oapps/oft_common/sources/oft_fee_detail.move @@ -0,0 +1,31 @@ +// This module provides functions to encode and decode OFT fee detail struct +module oft_common::oft_fee_detail { + use std::string::String; + + struct OftFeeDetail has store, copy, drop { + // Amount of the fee in local decimals + fee_amount_ld: u64, + // If true, the fee is a reward; this means the fee should be taken as negative + is_reward: bool, + // Description of the fee + description: String, + } + + /// Create a new OftFeeDetail + public fun new_oft_fee_detail(fee_amount_ld: u64, is_reward: bool, description: String): OftFeeDetail { + OftFeeDetail { fee_amount_ld, is_reward, description } + } + + /// Get the amount of the fee in local decimals (if is_reward is true, the fee should be taken as negative) + public fun fee_amount_ld(fd: &OftFeeDetail): (u64, bool) { (fd.fee_amount_ld, fd.is_reward) } + + /// Get the description of the fee + public fun description(fd: &OftFeeDetail): String { fd.description } + + /// Get all the fields of the OftFeeDetail + /// @return (fee_amount_ld, is_reward, description) + public fun unpack_oft_fee_detail(fd: OftFeeDetail): (u64, bool, String) { + let OftFeeDetail { fee_amount_ld, is_reward, description } = fd; + (fee_amount_ld, is_reward, description) + } +} diff --git a/packages/layerzero-v2/initia/contracts/oapps/oft_common/sources/oft_limit.move b/packages/layerzero-v2/initia/contracts/oapps/oft_common/sources/oft_limit.move new file mode 100644 index 00000000..5455d070 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/oapps/oft_common/sources/oft_limit.move @@ -0,0 +1,33 @@ +/// This provides a struct that represents an OFT limit (min and max amount transferrable in local decimals) +module oft_common::oft_limit { + + const MAX_U64: u64 = 0xffffffffffffffff; + + struct OftLimit has store, copy, drop { + min_amount_ld: u64, + max_amount_ld: u64, + } + + /// Create a new OftLimit + public fun new_oft_limit(min_amount_ld: u64, max_amount_ld: u64): OftLimit { + OftLimit { min_amount_ld, max_amount_ld } + } + + /// Create a new unbounded OFT Limit + public fun new_unbounded_oft_limit(): OftLimit { + OftLimit { min_amount_ld: 0, max_amount_ld: MAX_U64 } + } + + /// Get the minimum amount in local decimals + public fun min_amount_ld(oft_limit: &OftLimit): u64 { oft_limit.min_amount_ld } + + /// Get the maximum amount in local decimals + public fun max_amount_ld(oft_limit: &OftLimit): u64 { oft_limit.max_amount_ld } + + /// Get all the fields of the OftLimit + /// @return (min_amount_ld, max_amount_ld) + public fun unpack_oft_limit(oft_limit: OftLimit): (u64, u64) { + let OftLimit { min_amount_ld, max_amount_ld } = oft_limit; + (min_amount_ld, max_amount_ld) + } +} diff --git a/packages/layerzero-v2/initia/contracts/oapps/oft_common/sources/oft_msg_codec.move b/packages/layerzero-v2/initia/contracts/oapps/oft_common/sources/oft_msg_codec.move new file mode 100644 index 00000000..a8f39055 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/oapps/oft_common/sources/oft_msg_codec.move @@ -0,0 +1,55 @@ +/// This module provides the encoding and decoding of OFT messages +module oft_common::oft_msg_codec { + use std::vector; + + use endpoint_v2_common::bytes32::Bytes32; + use endpoint_v2_common::serde; + + const SEND_TO_OFFSET: u64 = 0; + const SEND_AMOUNT_OFFSET: u64 = 32; + const COMPOSE_MESSAGE_OFFSET: u64 = 40; + + /// Create a new OFT Message with this codec + /// @param send_to: The address to send the message to + /// @param amount_shared: The amount in shared decimals to send + /// @param sender: The address of the sender (used as the compose_from in the compose message if present) + /// @param compose_msg: The compose message to send + /// @return The encoded OFT Message + public fun encode(send_to: Bytes32, amount_shared: u64, sender: Bytes32, compose_payload: vector): vector { + let encoded = vector[]; + serde::append_bytes32(&mut encoded, send_to); + serde::append_u64(&mut encoded, amount_shared); + if (!vector::is_empty(&compose_payload)) { + serde::append_bytes32(&mut encoded, sender); + vector::append(&mut encoded, compose_payload); + }; + encoded + } + + /// Check whether an encoded OFT Message includes a compose + public fun has_compose(message: &vector): bool { + vector::length(message) > COMPOSE_MESSAGE_OFFSET + } + + /// Check the send to address in an encoded OFT Message + public fun send_to(message: &vector): Bytes32 { + serde::extract_bytes32(message, &mut SEND_TO_OFFSET) + } + + /// Check the amount in shared decimals in an encoded OFT Message + public fun amount_sd(message: &vector): u64 { + serde::extract_u64(message, &mut SEND_AMOUNT_OFFSET) + } + + /// Check the sender in an encoded OFT Message + /// Make sure to check if the message `has_compose()` before calling this function, which will fail without a clear + /// error message if it is not present + public fun sender(message: &vector): Bytes32 { + serde::extract_bytes32(message, &mut COMPOSE_MESSAGE_OFFSET) + } + + /// Read the compose payload, including the sender, from an encoded OFT Message + public fun compose_payload(message: &vector): vector { + vector::slice(message, COMPOSE_MESSAGE_OFFSET, vector::length(message)) + } +} \ No newline at end of file diff --git a/packages/layerzero-v2/initia/contracts/oapps/oft_common/sources/oft_v1_msg_codec.move b/packages/layerzero-v2/initia/contracts/oapps/oft_common/sources/oft_v1_msg_codec.move new file mode 100644 index 00000000..080fed0b --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/oapps/oft_common/sources/oft_v1_msg_codec.move @@ -0,0 +1,97 @@ +/// This module provides the encoding and decoding of legacy OFT v1 (OFT on LayerZero V1 Endpoint) messages +module oft_common::oft_v1_msg_codec { + use std::vector; + + use endpoint_v2_common::bytes32::{Bytes32, from_bytes32}; + use endpoint_v2_common::serde; + use endpoint_v2_common::serde::flatten; + + const TYPE_OFFSET: u64 = 0; + const SEND_TO_OFFSET: u64 = 1; + const SEND_AMOUNT_OFFSET: u64 = 33; + const COMPOSE_MESSAGE_OFFSET_SENDER: u64 = 41; + const COMPOSE_MESSAGE_OFFSET_COMPOSE_GAS: u64 = 73; + const COMPOSE_MESSAGE_CONTENT_OFFSET: u64 = 81; + + public inline fun PT_SEND(): u8 { 0 } + + public inline fun PT_SEND_AND_CALL(): u8 { 1 } + + /// Create a new OFT (Endpoint V1) Message with this codec + /// @param message_type: The type of message to send (0 = PT_SEND, 1 = PT_SEND_AND_CALL). Please note that these + /// enums do not align with the SEND / SEND_AND_CALL consts used in the OFT + /// @param send_to: The address to send the message to + /// @param amount_shared: The amount in shared decimals to send + /// @param sender: The address of the sender (used as the compose_from in the compose message if present) + /// @param compose_msg: The compose message to send + /// @return The encoded OFT Message + public fun encode( + message_type: u8, + send_to: Bytes32, + amount_shared: u64, + sender: Bytes32, + compose_gas: u64, + compose_payload: vector, + ): vector { + assert!(message_type == PT_SEND() || message_type == PT_SEND_AND_CALL(), EUNKNOWN_MESSAGE_TYPE); + let encoded = vector[]; + serde::append_u8(&mut encoded, message_type); + serde::append_bytes32(&mut encoded, send_to); + serde::append_u64(&mut encoded, amount_shared); + if (message_type == PT_SEND_AND_CALL()) { + serde::append_bytes32(&mut encoded, sender); + serde::append_u64(&mut encoded, compose_gas); + vector::append(&mut encoded, compose_payload); + }; + encoded + } + + /// Check the message type in an encoded OFT Message + public fun message_type(message: &vector): u8 { + serde::extract_u8(message, &mut TYPE_OFFSET) + } + + /// Check whether an encoded OFT Message includes a compose + public fun has_compose(message: &vector): bool { + vector::length(message) > COMPOSE_MESSAGE_OFFSET_SENDER + } + + /// Check the send to address in an encoded OFT Message + public fun send_to(message: &vector): Bytes32 { + serde::extract_bytes32(message, &mut SEND_TO_OFFSET) + } + + /// Check the amount in shared decimals in an encoded OFT Message + public fun amount_sd(message: &vector): u64 { + serde::extract_u64(message, &mut SEND_AMOUNT_OFFSET) + } + + /// Check the sender in an encoded OFT Message + /// This function should only be called after verifying that the message has a compose message + public fun sender(message: &vector): Bytes32 { + serde::extract_bytes32(message, &mut COMPOSE_MESSAGE_OFFSET_SENDER) + } + + /// Check the compose gas in an encoded OFT Message + /// This function should only be called after verifying that the message has a compose message + public fun compose_gas(message: &vector): u64 { + serde::extract_u64(message, &mut COMPOSE_MESSAGE_OFFSET_COMPOSE_GAS) + } + + public fun compose_message_content(message: &vector): vector { + serde::extract_bytes_until_end(message, &mut COMPOSE_MESSAGE_CONTENT_OFFSET) + } + + /// Return the compose "payload", including the sender, from an encoded OFT Message + /// This will return an empty string if the message does not have a compose message + public fun v2_compatible_compose_payload(message: &vector): vector { + flatten(vector[ + from_bytes32(sender(message)), + compose_message_content(message), + ]) + } + + // ================================================== Error Codes ================================================= + + const EUNKNOWN_MESSAGE_TYPE: u64 = 1; +} \ No newline at end of file diff --git a/packages/layerzero-v2/initia/contracts/oapps/oft_common/tests/oft_compose_codec_tests.move b/packages/layerzero-v2/initia/contracts/oapps/oft_common/tests/oft_compose_codec_tests.move new file mode 100644 index 00000000..58aa3007 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/oapps/oft_common/tests/oft_compose_codec_tests.move @@ -0,0 +1,61 @@ +#[test_only] +module oft_common::oft_compose_codec_tests { + use endpoint_v2_common::bytes32; + use endpoint_v2_common::serde::flatten; + use oft_common::oft_compose_msg_codec; + + #[test] + fun test_encode_should_encode_and_decode_with_compose() { + let nonce = 123; + let src_eid = 456; + let amount_ld = 1000; + let compose_msg = flatten(vector[ + x"1234567890123456789012345678901234567890123456789012345678901234", + x"9999888855", + ]); + + let encoded = oft_compose_msg_codec::encode( + nonce, + src_eid, + amount_ld, + compose_msg, + ); + + assert!(oft_compose_msg_codec::nonce(&encoded) == nonce, 1); + assert!(oft_compose_msg_codec::src_eid(&encoded) == src_eid, 2); + assert!(oft_compose_msg_codec::amount_ld(&encoded) == amount_ld, 3); + assert!( + oft_compose_msg_codec::compose_payload_from(&encoded) == bytes32::to_bytes32( + x"1234567890123456789012345678901234567890123456789012345678901234" + ), + 5, + ); + assert!(oft_compose_msg_codec::compose_payload_message(&encoded) == x"9999888855", 6); + } + + #[test] + #[expected_failure(abort_code = oft_common::oft_compose_msg_codec::ENO_COMPOSE_MSG)] + fun test_compose_from_should_fail_when_no_compose() { + let encoded = oft_compose_msg_codec::encode( + 123, + 456, + 1000, + vector[], + ); + + oft_compose_msg_codec::compose_payload_from(&encoded); + } + + #[test] + #[expected_failure(abort_code = oft_common::oft_compose_msg_codec::ENO_COMPOSE_MSG)] + fun test_compose_msg_from_oft_compose_msg_should_fail_when_no_compose() { + let encoded = oft_compose_msg_codec::encode( + 123, + 456, + 1000, + vector[], + ); + + oft_compose_msg_codec::compose_payload_message(&encoded); + } +} diff --git a/packages/layerzero-v2/initia/contracts/oapps/oft_common/tests/oft_msg_codec_tests.move b/packages/layerzero-v2/initia/contracts/oapps/oft_common/tests/oft_msg_codec_tests.move new file mode 100644 index 00000000..5b9fabf0 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/oapps/oft_common/tests/oft_msg_codec_tests.move @@ -0,0 +1,40 @@ +#[test_only] +module oft_common::oft_msg_codec_tests { + use endpoint_v2_common::bytes32; + use oft_common::oft_msg_codec; + + #[test] + fun test_encode_should_encode_and_decode_without_compose() { + let send_to = bytes32::to_bytes32(b"12345678901234567890123456789012"); + let amount = 1000; + + let encoded = oft_msg_codec::encode( + send_to, + amount, + bytes32::from_address(@0x1234567890123456789012345678901234567890123456789012345678901234), + // empty compose message signifies no compose message + vector[], + ); + + assert!(!oft_msg_codec::has_compose(&encoded), 0); + assert!(oft_msg_codec::send_to(&encoded) == send_to, 1); + assert!(oft_msg_codec::amount_sd(&encoded) == amount, 2); + } + + #[test] + fun test_encode_should_encode_and_decode_with_compose() { + let sender = @0x1234567890123456789012345678901234567890123456789012345678901234; + let send_to = bytes32::to_bytes32(b"12345678901234567890123456789012"); + let amount = 1000; + let compose_msg = x"9999888855"; + + let encoded = oft_msg_codec::encode(send_to, amount, bytes32::from_address(sender), compose_msg); + + assert!(oft_msg_codec::has_compose(&encoded), 0); + assert!(oft_msg_codec::send_to(&encoded) == send_to, 1); + assert!(oft_msg_codec::amount_sd(&encoded) == amount, 2); + + let compose_packet = x"12345678901234567890123456789012345678901234567890123456789012349999888855"; + assert!(oft_msg_codec::compose_payload(&encoded) == compose_packet, 3); + } +} diff --git a/packages/layerzero-v2/initia/contracts/oapps/oft_common/tests/oft_v1_msg_codec_tests.move b/packages/layerzero-v2/initia/contracts/oapps/oft_common/tests/oft_v1_msg_codec_tests.move new file mode 100644 index 00000000..ad2e440f --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/oapps/oft_common/tests/oft_v1_msg_codec_tests.move @@ -0,0 +1,52 @@ +#[test_only] +module oft_common::oft_1_msg_codec_tests { + use endpoint_v2_common::bytes32; + use oft_common::oft_v1_msg_codec; + use oft_common::oft_v1_msg_codec::{PT_SEND, PT_SEND_AND_CALL}; + + #[test] + fun test_encode_should_encode_and_decode_without_compose() { + let send_to = bytes32::to_bytes32(b"12345678901234567890123456789012"); + let amount = 1000; + + let encoded = oft_v1_msg_codec::encode( + PT_SEND(), + send_to, + amount, + bytes32::from_address(@0x1234567890123456789012345678901234567890123456789012345678901234), + 0, + vector[], + ); + + assert!(oft_v1_msg_codec::message_type(&encoded) == PT_SEND(), 1); + assert!(!oft_v1_msg_codec::has_compose(&encoded), 0); + assert!(oft_v1_msg_codec::send_to(&encoded) == send_to, 1); + assert!(oft_v1_msg_codec::amount_sd(&encoded) == amount, 2); + } + + #[test] + fun test_encode_should_encode_and_decode_with_compose() { + let sender = @0x1234567890123456789012345678901234567890123456789012345678901234; + let send_to = bytes32::to_bytes32(b"12345678901234567890123456789012"); + let amount = 1000; + let compose_msg = x"9999888855"; + + let encoded = oft_v1_msg_codec::encode( + PT_SEND_AND_CALL(), + send_to, + amount, + bytes32::from_address(sender), + 0x101, + compose_msg, + ); + + assert!(oft_v1_msg_codec::message_type(&encoded) == PT_SEND_AND_CALL(), 1); + assert!(oft_v1_msg_codec::has_compose(&encoded), 0); + assert!(oft_v1_msg_codec::send_to(&encoded) == send_to, 1); + assert!(oft_v1_msg_codec::amount_sd(&encoded) == amount, 2); + assert!(oft_v1_msg_codec::compose_gas(&encoded) == 0x101, 3); + + let v2_style_compose_packet = x"12345678901234567890123456789012345678901234567890123456789012349999888855"; + assert!(oft_v1_msg_codec::v2_compatible_compose_payload(&encoded) == v2_style_compose_packet, 3); + } +} diff --git a/packages/layerzero-v2/initia/contracts/run_tests.sh b/packages/layerzero-v2/initia/contracts/run_tests.sh new file mode 100755 index 00000000..5af6d456 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/run_tests.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +# Find all directories containing Move.toml and run the command +find . -name 'Move.toml' -execdir sh -c 'echo "Running tests in $(pwd)" && initiad move test --dev' \; diff --git a/packages/layerzero-v2/initia/contracts/treasury/Move.toml b/packages/layerzero-v2/initia/contracts/treasury/Move.toml new file mode 100644 index 00000000..aa6c630b --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/treasury/Move.toml @@ -0,0 +1,27 @@ +[package] +name = "treasury" +version = "1.0.0" +authors = [] + +[addresses] +treasury = "_" +endpoint_v2_common = "_" +endpoint_v2 = "_" +layerzero_admin = "_" +layerzero_treasury_admin = "_" +native_token_metadata_address = "0x8e4733bdabcf7d4afc3d14f0dd46c9bf52fb0fce9e4b996c939e195b8bc891d9" + +[dev-addresses] +treasury = "0x123432432" +endpoint_v2_common = "0x9098" +endpoint_v2 = "0x12345678" +layerzero_admin = "0x18943124" +layerzero_treasury_admin = "0x1894312499" + +[dependencies.InitiaStdlib] +git = "https://github.com/initia-labs/move-natives.git" +rev = "77d5f3e140143bdaa41f850115b3035c134193e3" +subdir = "initia_stdlib" + +[dependencies] +endpoint_v2_common = { local = "../endpoint_v2_common" } diff --git a/packages/layerzero-v2/initia/contracts/treasury/sources/treasury.move b/packages/layerzero-v2/initia/contracts/treasury/sources/treasury.move new file mode 100644 index 00000000..10ddc749 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/treasury/sources/treasury.move @@ -0,0 +1,192 @@ +module treasury::treasury { + use std::account; + use std::event::emit; + use std::fungible_asset::{Self, FungibleAsset}; + use std::object::object_address; + use std::primary_fungible_store; + use std::signer::address_of; + + use endpoint_v2_common::universal_config; + + #[test_only] + friend treasury::treasury_tests; + + const TREASURY_ADMIN: address = @layerzero_treasury_admin; + + // Treasury Fee cannot be set above 100% + const MAX_BPS: u64 = 10000; + + struct TreasuryConfig has key { + // The native treasury fee (in basis points of worker fee subtotal) + native_fee_bps: u64, + // The ZRO treasury fee (a fixed amount per message) + zro_fee: u64, + // Whether the treasury fee can be paid in ZRO + zro_enabled: bool, + // The address to which the treasury fee should be deposited + deposit_address: address, + } + + fun init_module(account: &signer) { + move_to(account, TreasuryConfig { + native_fee_bps: 0, + zro_fee: 0, + zro_enabled: false, + deposit_address: TREASURY_ADMIN, + }); + } + + #[test_only] + public fun init_module_for_test() { + let account = &std::account::create_signer_for_test(@treasury); + init_module(account); + } + + // ================================================== Admin Only ================================================== + + inline fun assert_admin(admin: address) { + assert!(admin == TREASURY_ADMIN, EUNAUTHORIZED); + } + + /// Updates the address to which the treasury fee is sent (must be a valid account) + public entry fun update_deposit_address(account: &signer, deposit_address: address) acquires TreasuryConfig { + assert!(account::exists_at(deposit_address), EINVALID_ACCOUNT_ADDRESS); + assert_admin(address_of(move account)); + config_mut().deposit_address = deposit_address; + emit(DepositAddressUpdated { new_deposit_address: deposit_address }); + } + + /// Enables receipt of ZRO + public entry fun set_zro_enabled(account: &signer, enabled: bool) acquires TreasuryConfig { + assert_admin(address_of(move account)); + config_mut().zro_enabled = enabled; + emit(ZroEnabledSet { enabled }); + } + + /// Sets the treasury fee in basis points of the worker fee subtotal + public entry fun set_native_bp(account: &signer, native_bps: u64) acquires TreasuryConfig { + assert_admin(address_of(move account)); + assert!(native_bps <= MAX_BPS, EINVALID_FEE); + config_mut().native_fee_bps = native_bps; + emit(NativeBpSet { native_bps }); + } + + /// Sets the treasury fee in ZRO (as a fixed amount) + public entry fun set_zro_fee(account: &signer, zro_fixed_fee: u64) acquires TreasuryConfig { + assert_admin(address_of(move account)); + config_mut().zro_fee = zro_fixed_fee; + emit(ZroFeeSet { zro_fee: zro_fixed_fee }); + } + + // =============================================== Public Functions =============================================== + + #[view] + /// Calculates the treasury fee based on the worker fee (excluding treasury) and whether the fee should be paid in + /// ZRO. If the fee should be paid in ZRO, the fee is returned in ZRO, otherwise the fee is returned is Native token + public fun get_fee(total_worker_fee: u64, pay_in_zro: bool): u64 acquires TreasuryConfig { + if (pay_in_zro) { + assert!(get_zro_enabled(), EPAY_IN_ZRO_NOT_ENABLED); + config().zro_fee + } else { + total_worker_fee * config().native_fee_bps / 10000 + } + } + + #[view] + public fun get_native_bp(): u64 acquires TreasuryConfig { config().native_fee_bps } + + #[view] + public fun get_zro_fee(): u64 acquires TreasuryConfig { config().zro_fee } + + #[view] + public fun get_zro_enabled(): bool acquires TreasuryConfig { config().zro_enabled } + + #[view] + public fun get_deposit_address(): address acquires TreasuryConfig { config().deposit_address } + + /// Pay the fee to the treasury. The fee is calculated based on the worker fee (excluding treasury), and whether + /// the FungibleAsset payment is in ZRO or Native token. The fee is extracted from the provided &mut FungibleAsset + public fun pay_fee( + total_worker_fee: u64, + payment: &mut FungibleAsset, + ): (u64) acquires TreasuryConfig { + let metadata = fungible_asset::asset_metadata(payment); + + if (object_address(&metadata) == @native_token_metadata_address) { + let fee = get_fee(total_worker_fee, false); + deposit_fungible_asset(fee, payment); + fee + } else if (config().zro_enabled && universal_config::is_zro_metadata(metadata)) { + let fee = get_fee(total_worker_fee, true); + deposit_fungible_asset(fee, payment); + fee + } else if (!config().zro_enabled) { + abort EPAY_IN_ZRO_NOT_ENABLED + } else { + abort EUNEXPECTED_TOKEN_TYPE + } + } + + /// Deposits the payment into the treasury + fun deposit_fungible_asset(charge: u64, payment: &mut FungibleAsset) acquires TreasuryConfig { + let deposit_address = config().deposit_address; + let deposit = fungible_asset::extract(payment, charge); + primary_fungible_store::deposit(deposit_address, deposit); + } + + // =============================================== Helper Functions =============================================== + + inline fun config(): &TreasuryConfig { borrow_global(@treasury) } + + inline fun config_mut(): &mut TreasuryConfig { borrow_global_mut(@treasury) } + + // ==================================================== Events ==================================================== + + #[event] + struct ZroEnabledSet has drop, store { + enabled: bool, + } + + #[event] + struct NativeBpSet has drop, store { + native_bps: u64, + } + + #[event] + struct ZroFeeSet has drop, store { + zro_fee: u64, + } + + #[event] + struct DepositAddressUpdated has drop, store { + new_deposit_address: address, + } + + #[test_only] + public fun zro_enabled_set_event(enabled: bool): ZroEnabledSet { + ZroEnabledSet { enabled } + } + + #[test_only] + public fun native_bp_set_event(native_bp: u64): NativeBpSet { + NativeBpSet { native_bps: native_bp } + } + + #[test_only] + public fun zro_fee_set_event(zro_fee: u64): ZroFeeSet { + ZroFeeSet { zro_fee } + } + + #[test_only] + public fun deposit_address_updated_event(new_deposit_address: address): DepositAddressUpdated { + DepositAddressUpdated { new_deposit_address } + } + + // ================================================== Error Codes ================================================= + + const EUNEXPECTED_TOKEN_TYPE: u64 = 1; + const EINVALID_ACCOUNT_ADDRESS: u64 = 2; + const EINVALID_FEE: u64 = 3; + const EPAY_IN_ZRO_NOT_ENABLED: u64 = 4; + const EUNAUTHORIZED: u64 = 5; +} diff --git a/packages/layerzero-v2/initia/contracts/treasury/tests/treasury_tests.move b/packages/layerzero-v2/initia/contracts/treasury/tests/treasury_tests.move new file mode 100644 index 00000000..c41ba84b --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/treasury/tests/treasury_tests.move @@ -0,0 +1,152 @@ +#[test_only] +module treasury::treasury_tests { + use std::account::{Self, create_signer_for_test}; + use std::event::was_event_emitted; + use std::fungible_asset; + use std::primary_fungible_store; + + use endpoint_v2_common::native_token_test_helpers::{burn_token_for_test, mint_native_token_for_test}; + use endpoint_v2_common::universal_config; + use endpoint_v2_common::zro_test_helpers::create_fa; + use treasury::treasury::{ + deposit_address_updated_event, get_native_bp, get_zro_fee, init_module_for_test, native_bp_set_event, pay_fee, + set_native_bp, set_zro_enabled, set_zro_fee, update_deposit_address, zro_enabled_set_event, zro_fee_set_event, + }; + + #[test] + fun test_fee_payment_using_native() { + let lz = &create_signer_for_test(@layerzero_treasury_admin); + init_module_for_test(); + let (zro_addr, _, _) = create_fa(b"ZRO"); + set_native_bp(lz, 100); + + universal_config::init_module_for_test(100); + let lz_admin = &create_signer_for_test(@layerzero_admin); + universal_config::set_zro_address(lz_admin, zro_addr); + set_zro_enabled(lz, true); + + let new_deposit_address = @0x1234; + account::create_account_for_test(new_deposit_address); + update_deposit_address(lz, new_deposit_address); + assert!(was_event_emitted(&deposit_address_updated_event(new_deposit_address)), 0); + + let payment_native = mint_native_token_for_test(2222); + pay_fee(2000, &mut payment_native); + // a 2000 worker fee * 100 BP = 20 native treasury fee + // remaining amount = 2222 - 20 = 2202 + assert!(fungible_asset::amount(&payment_native) == 2202, 0); + + let metadata = fungible_asset::metadata_from_asset(&payment_native); + let treasury_balance = primary_fungible_store::balance(new_deposit_address, metadata); + assert!(treasury_balance == 20, 1); + + // test cleanup + burn_token_for_test(payment_native); + } + + #[test] + #[expected_failure(abort_code = treasury::treasury::EPAY_IN_ZRO_NOT_ENABLED)] + fun test_fee_payment_using_zro_fails_when_zro_disabled() { + init_module_for_test(); + let (_, metadata, _) = create_fa(b"ZRO"); + let payment_zro = fungible_asset::zero(metadata); + pay_fee(1000, &mut payment_zro); + fungible_asset::destroy_zero(payment_zro); + } + + #[test] + fun test_pay_fee_works_in_zro() { + let lz = &create_signer_for_test(@layerzero_treasury_admin); + init_module_for_test(); + set_zro_fee(lz, 300); + let (zro_addr, metadata, mint_ref) = create_fa(b"ZRO"); + + universal_config::init_module_for_test(100); + let lz_admin = &create_signer_for_test(@layerzero_admin); + universal_config::set_zro_address(lz_admin, zro_addr); + set_zro_enabled(lz, true); + + let payment_zro = fungible_asset::mint(&mint_ref, 3000); + + pay_fee(3000, &mut payment_zro); + // 20 spent + assert!(fungible_asset::amount(&payment_zro) == 2700, 0); + let treasury_balance = primary_fungible_store::balance(@layerzero_treasury_admin, metadata); + assert!(treasury_balance == 300, 1); + + // cleanup + burn_token_for_test(payment_zro) + } + + #[test] + #[expected_failure(abort_code = treasury::treasury::EUNAUTHORIZED)] + fun test_update_deposit_address_fails_for_non_admin() { + init_module_for_test(); + let new_deposit_address = @0x1234; + account::create_account_for_test(new_deposit_address); + let non_admin = &create_signer_for_test(@0x1234); + update_deposit_address(non_admin, new_deposit_address); + } + + #[test] + #[expected_failure(abort_code = treasury::treasury::EINVALID_ACCOUNT_ADDRESS)] + fun test_update_deposit_address_fails_for_invalid_address() { + let lz = &create_signer_for_test(@layerzero_treasury_admin); + init_module_for_test(); + let new_deposit_address = @0x1234; + account::create_account_for_test(new_deposit_address); + update_deposit_address(lz, @0x0); + } + + #[test] + fun test_set_zro_enabled() { + let lz = &create_signer_for_test(@layerzero_treasury_admin); + init_module_for_test(); + let (zro_addr, _, _) = create_fa(b"ZRO"); + + universal_config::init_module_for_test(100); + let lz_admin = &create_signer_for_test(@layerzero_admin); + universal_config::set_zro_address(lz_admin, zro_addr); + + set_zro_enabled(lz, true); + assert!(was_event_emitted(&zro_enabled_set_event(true)), 0); + + set_zro_enabled(lz, false); + assert!(was_event_emitted(&zro_enabled_set_event(true)), 0); + } + + #[test] + fun test_set_zro_fee() { + let lz = &create_signer_for_test(@layerzero_treasury_admin); + init_module_for_test(); + let (zro_addr, _, _) = create_fa(b"ZRO"); + + universal_config::init_module_for_test(100); + let lz_admin = &create_signer_for_test(@layerzero_admin); + universal_config::set_zro_address(lz_admin, zro_addr); + set_zro_enabled(lz, true); + + set_zro_fee(lz, 120); + assert!(was_event_emitted(&zro_fee_set_event(120)), 0); + assert!(get_zro_fee() == 120, 1); + } + + #[test] + fun test_set_native_bp() { + let lz = &create_signer_for_test(@layerzero_treasury_admin); + init_module_for_test(); + set_native_bp(lz, 124); + + let bp = get_native_bp(); + assert!(bp == 124, 0); + assert!(was_event_emitted(&native_bp_set_event(124)), 1) + } + + #[test] + #[expected_failure(abort_code = treasury::treasury::EINVALID_FEE)] + fun test_set_native_bp_above_10000() { + let lz = &create_signer_for_test(@layerzero_treasury_admin); + init_module_for_test(); + set_native_bp(lz, 10001); + } +} \ No newline at end of file diff --git a/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_lib_routers/dvn_fee_lib_router_0/Move.toml b/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_lib_routers/dvn_fee_lib_router_0/Move.toml new file mode 100644 index 00000000..d4545010 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_lib_routers/dvn_fee_lib_router_0/Move.toml @@ -0,0 +1,38 @@ +[package] +name = "dvn_fee_lib_router_0" +version = "1.0.0" + +[addresses] +dvn_fee_lib_router_0 = "_" +dvn_fee_lib_router_1 = "_" +price_feed_router_0 = "_" +price_feed_router_1 = "_" +price_feed_module_0 = "_" +endpoint_v2_common = "_" +endpoint_v2 = "_" +layerzero_admin = "_" +layerzero_treasury_admin = "_" +worker_common = "_" +dvn_fee_lib_0 = "_" +msglib_types = "_" + +[dev-addresses] +dvn_fee_lib_router_0 = "0x30001" +dvn_fee_lib_router_1 = "0x30002" +price_feed_router_0 = "0x65DD71A" +price_feed_router_1 = "0x65DD71AB" +price_feed_module_0 = "0x65DD71" +endpoint_v2_common = "0x9098" +endpoint_v2 = "0x12345678" +layerzero_admin = "0x18943124" +layerzero_treasury_admin = "0x1894312499" +worker_common = "0x3999" +dvn_fee_lib_0 = "0x32123523a" +msglib_types = "0x13242342" + +[dependencies] +endpoint_v2_common = { local = "../../../endpoint_v2_common" } +worker_common = { local = "../../worker_common" } +dvn_fee_lib_0 = { local = "../../fee_libs/dvn_fee_lib_0" } +price_feed_module_0 = { local = "../../price_feed_modules/price_feed_module_0" } +dvn_fee_lib_router_1 = { local = "../dvn_fee_lib_router_1_placeholder" } \ No newline at end of file diff --git a/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_lib_routers/dvn_fee_lib_router_0/sources/dvn_fee_lib_router.move b/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_lib_routers/dvn_fee_lib_router_0/sources/dvn_fee_lib_router.move new file mode 100644 index 00000000..d244cd23 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_lib_routers/dvn_fee_lib_router_0/sources/dvn_fee_lib_router.move @@ -0,0 +1,40 @@ +module dvn_fee_lib_router_0::dvn_fee_lib_router { + use dvn_fee_lib_router_1::dvn_fee_lib_router as dvn_fee_lib_router_next; + + public fun get_dvn_fee( + msglib: address, + dvn_fee_lib: address, + worker: address, + dst_eid: u32, + sender: address, + packet_header: vector, + payload_hash: vector, + confirmations: u64, + options: vector, + ): (u64, address) { + if (dvn_fee_lib == @dvn_fee_lib_0) { + dvn_fee_lib_0::dvn_fee_lib::get_dvn_fee( + msglib, + worker, + dst_eid, + sender, + packet_header, + payload_hash, + confirmations, + options, + ) + } else { + dvn_fee_lib_router_next::get_dvn_fee( + msglib, + dvn_fee_lib, + worker, + dst_eid, + sender, + packet_header, + payload_hash, + confirmations, + options, + ) + } + } +} diff --git a/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_lib_routers/dvn_fee_lib_router_1_placeholder/Move.toml b/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_lib_routers/dvn_fee_lib_router_1_placeholder/Move.toml new file mode 100644 index 00000000..5bc9ebc5 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_lib_routers/dvn_fee_lib_router_1_placeholder/Move.toml @@ -0,0 +1,16 @@ +[package] +name = "dvn_fee_lib_router_1" +version = "1.0.0" + +[addresses] +dvn_fee_lib_router_1 = "_" + +[dev-addresses] +dvn_fee_lib_router_1 = "0x3294873" + +[dependencies.InitiaStdlib] +git = "https://github.com/initia-labs/move-natives.git" +rev = "77d5f3e140143bdaa41f850115b3035c134193e3" +subdir = "initia_stdlib" + +[dev-dependencies] diff --git a/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_lib_routers/dvn_fee_lib_router_1_placeholder/sources/dvn_fee_lib_router.move b/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_lib_routers/dvn_fee_lib_router_1_placeholder/sources/dvn_fee_lib_router.move new file mode 100644 index 00000000..ee94c6da --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_lib_routers/dvn_fee_lib_router_1_placeholder/sources/dvn_fee_lib_router.move @@ -0,0 +1,17 @@ +module dvn_fee_lib_router_1::dvn_fee_lib_router { + const ENOT_IMPLEMENTED: u64 = 1; + + public fun get_dvn_fee( + _msglib: address, + _fee_lib: address, + _worker: address, + _dst_eid: u32, + _sender: address, + _packet_header: vector, + _payload_hash: vector, + _confirmations: u64, + _options: vector, + ): (u64, address) { + abort ENOT_IMPLEMENTED + } +} \ No newline at end of file diff --git a/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_lib_routers/executor_fee_lib_router_0/Move.toml b/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_lib_routers/executor_fee_lib_router_0/Move.toml new file mode 100644 index 00000000..dacb5664 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_lib_routers/executor_fee_lib_router_0/Move.toml @@ -0,0 +1,38 @@ +[package] +name = "executor_fee_lib_router_0" +version = "1.0.0" + +[addresses] +executor_fee_lib_router_0 = "_" +executor_fee_lib_router_1 = "_" +price_feed_router_0 = "_" +price_feed_router_1 = "_" +price_feed_module_0 = "_" +endpoint_v2_common = "_" +endpoint_v2 = "_" +layerzero_admin = "_" +layerzero_treasury_admin = "_" +worker_common = "_" +executor_fee_lib_0 = "_" +msglib_types = "_" + +[dev-addresses] +executor_fee_lib_router_0 = "0x30001" +executor_fee_lib_router_1 = "0x30001a" +price_feed_router_0 = "0x65DD71A" +price_feed_router_1 = "0x65DD71AB" +price_feed_module_0 = "0x65DD71" +endpoint_v2_common = "0x9098" +endpoint_v2 = "0x12345678" +layerzero_admin = "0x18943124" +layerzero_treasury_admin = "0x1894312499" +worker_common = "0x3999" +executor_fee_lib_0 = "0x32123523" +msglib_types = "0x13242342" + +[dependencies] +endpoint_v2_common = { local = "../../../endpoint_v2_common" } +worker_common = { local = "../../worker_common" } +executor_fee_lib_router_1 = { local = "../../fee_lib_routers/executor_fee_lib_router_1_placeholder" } +executor_fee_lib_0 = { local = "../../fee_libs/executor_fee_lib_0" } +price_feed_module_0 = { local = "../../price_feed_modules/price_feed_module_0" } \ No newline at end of file diff --git a/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_lib_routers/executor_fee_lib_router_0/sources/executor_fee_lib_router.move b/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_lib_routers/executor_fee_lib_router_0/sources/executor_fee_lib_router.move new file mode 100644 index 00000000..d65d6d28 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_lib_routers/executor_fee_lib_router_0/sources/executor_fee_lib_router.move @@ -0,0 +1,34 @@ +module executor_fee_lib_router_0::executor_fee_lib_router { + use executor_fee_lib_router_1::executor_fee_lib_router as executor_fee_lib_router_next; + + public fun get_executor_fee( + msglib: address, + executor_fee_lib: address, + worker: address, + dst_eid: u32, + sender: address, + message_size: u64, + options: vector, + ): (u64, address) { + if (executor_fee_lib == @executor_fee_lib_0) { + executor_fee_lib_0::executor_fee_lib::get_executor_fee( + msglib, + worker, + dst_eid, + sender, + message_size, + options, + ) + } else { + executor_fee_lib_router_next::get_executor_fee( + msglib, + executor_fee_lib, + worker, + dst_eid, + sender, + message_size, + options, + ) + } + } +} diff --git a/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_lib_routers/executor_fee_lib_router_1_placeholder/Move.toml b/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_lib_routers/executor_fee_lib_router_1_placeholder/Move.toml new file mode 100644 index 00000000..70702dc0 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_lib_routers/executor_fee_lib_router_1_placeholder/Move.toml @@ -0,0 +1,16 @@ +[package] +name = "executor_fee_lib_router_1" +version = "1.0.0" + +[addresses] +executor_fee_lib_router_1 = "_" + +[dev-addresses] +executor_fee_lib_router_1 = "0x93270987" + +[dependencies.InitiaStdlib] +git = "https://github.com/initia-labs/move-natives.git" +rev = "77d5f3e140143bdaa41f850115b3035c134193e3" +subdir = "initia_stdlib" + +[dev-dependencies] diff --git a/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_lib_routers/executor_fee_lib_router_1_placeholder/sources/executor_fee_lib_router.move b/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_lib_routers/executor_fee_lib_router_1_placeholder/sources/executor_fee_lib_router.move new file mode 100644 index 00000000..f072dc4f --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_lib_routers/executor_fee_lib_router_1_placeholder/sources/executor_fee_lib_router.move @@ -0,0 +1,15 @@ +module executor_fee_lib_router_1::executor_fee_lib_router { + const ENOT_IMPLEMENTED: u64 = 1; + + public fun get_executor_fee( + _msglib: address, + _fee_lib: address, + _worker: address, + _dst_eid: u32, + _sender: address, + _message_size: u64, + _options: vector, + ): (u64, address) { + abort ENOT_IMPLEMENTED + } +} diff --git a/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_libs/dvn_fee_lib_0/Move.toml b/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_libs/dvn_fee_lib_0/Move.toml new file mode 100644 index 00000000..d0bc3bf9 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_libs/dvn_fee_lib_0/Move.toml @@ -0,0 +1,33 @@ +[package] +name = "dvn_fee_lib_0" +version = "1.0.0" + +[addresses] +dvn_fee_lib_0 = "_" +endpoint_v2_common = "_" +endpoint_v2 = "_" +layerzero_admin = "_" +layerzero_treasury_admin = "_" +price_feed_router_0 = "_" +price_feed_router_1 = "_" +price_feed_module_0 = "_" +worker_common = "_" +msglib_types = "_" + +[dev-addresses] +dvn_fee_lib_0 = "0x3000a" +endpoint_v2_common = "0x9098" +endpoint_v2 = "0x12345678" +layerzero_admin = "0x18943124" +layerzero_treasury_admin = "0x1894312499" +worker_common = "0x3999" +msglib_types = "0x13242342" +price_feed_router_0 = "0x65DD71A" +price_feed_router_1 = "0x65DD71AB" +price_feed_module_0 = "0x65DD71" + +[dependencies] +endpoint_v2_common = { local = "../../../endpoint_v2_common" } +worker_common = { local = "../../worker_common" } +price_feed_router_0 = { local = "../../price_feed_routers/price_feed_router_0" } +msglib_types = { local = "../../../msglib/msglib_types" } \ No newline at end of file diff --git a/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_libs/dvn_fee_lib_0/sources/dvn_fee_lib.move b/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_libs/dvn_fee_lib_0/sources/dvn_fee_lib.move new file mode 100644 index 00000000..f581488a --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_libs/dvn_fee_lib_0/sources/dvn_fee_lib.move @@ -0,0 +1,122 @@ +module dvn_fee_lib_0::dvn_fee_lib { + use msglib_types::worker_options::DVN_WORKER_ID; + use price_feed_router_0::router as price_feed_router; + use worker_common::multisig; + use worker_common::worker_config; + + #[test_only] + friend dvn_fee_lib_0::dvn_fee_lib_tests; + + const EXECUTE_FIXED_BYTES: u64 = 68; + const SIGNATURE_RAW_BYTES: u64 = 65; + const VERIFY_BYTES: u64 = 320; + + #[view] + // Get the total fee, including premiums for a DVN worker to verify a message + public fun get_dvn_fee( + msglib: address, + worker: address, + dst_eid: u32, + sender: address, + _packet_header: vector, + _payload_hash: vector, + _confirmations: u64, + _options: vector, + ): (u64, address) { + worker_config::assert_fee_lib_supports_transaction(worker, DVN_WORKER_ID(), sender, msglib); + let calldata_size = get_calldata_size_for_fee(worker); + + let fee = get_dvn_fee_internal( + worker, + dst_eid, + // Price Feed Estimate Fee on Send - partially applying parameters available in scope + |price_feed, feed_address, total_gas| price_feed_router::estimate_fee_on_send( + price_feed, + feed_address, + dst_eid, + calldata_size, + total_gas, + ) + ); + + let deposit_address = worker_config::get_deposit_address(worker); + (fee, deposit_address) + } + + /// Get the total fee, including premiums for a DVN packet, while ensuring that this feelib is supported by the + /// worker, the sender is allowed, and the worker is unpaused + /// + /// @param worker_address: The address of the worker + /// @param dst_eid: The destination EID + /// @param estimate_fee_on_send: fee estimator (via price feed, with partially applied parameters) + /// |price_feed, feed_address, total_gas| (fee, price ratio, denominator, native token price in USD) + public(friend) inline fun get_dvn_fee_internal( + worker_address: address, + dst_eid: u32, + estimate_fee_on_send: |address, address, u128| (u128, u128, u128, u128), + ): u64 { + let (gas, multiplier_bps, floor_margin_usd) = worker_config::get_dvn_dst_config_values(worker_address, dst_eid); + assert!(gas != 0, err_EDVN_EID_NOT_SUPPORTED()); + + let (price_feed_module, feed_address) = worker_config::get_effective_price_feed(worker_address); + let (chain_fee, _, _, native_price_usd) = estimate_fee_on_send( + price_feed_module, + feed_address, + (gas as u128) + ); + + let default_multiplier_bps = worker_config::get_default_multiplier_bps(worker_address); + let native_decimals_rate = worker_config::get_native_decimals_rate(); + + (apply_premium( + chain_fee, + native_price_usd, + multiplier_bps, + floor_margin_usd, + default_multiplier_bps, + native_decimals_rate, + ) as u64) + } + + /// Apply the premium to the fee. It takes the higher of using the multiplier or the floor margin + public(friend) fun apply_premium( + chain_fee: u128, + native_price_usd: u128, // in native_decimals_rate + multiplier_bps: u16, + floor_margin_usd: u128, + default_multiplier_bps: u16, + native_decimals_rate: u128, + ): u128 { + let multiplier_bps = if (multiplier_bps == 0) default_multiplier_bps else multiplier_bps; + // multiplier bps is 1e5 e.g. 12000 is 120% + let fee_with_multiplier = chain_fee * (multiplier_bps as u128) / 10000; + + if (native_price_usd == 0 || floor_margin_usd == 0) { + return fee_with_multiplier + }; + + let fee_with_floor_margin = chain_fee + (floor_margin_usd * native_decimals_rate) / native_price_usd; + + if (fee_with_floor_margin > fee_with_multiplier) { fee_with_floor_margin } else { fee_with_multiplier } + } + + // =================================================== Internal =================================================== + + /// Get the calldata size for a fee; this scales with the number of quorum signatures required + public(friend) fun get_calldata_size_for_fee(worker_address: address): u64 { + let quorum = multisig::get_quorum(worker_address); + + let total_signature_bytes: u64 = quorum * SIGNATURE_RAW_BYTES; + if (total_signature_bytes % 32 != 0) { + total_signature_bytes = total_signature_bytes - (total_signature_bytes % 32) + 32; + }; + // Total includes 64 byte overhead + EXECUTE_FIXED_BYTES + VERIFY_BYTES + total_signature_bytes + 64 + } + + // ================================================== Error Codes ================================================= + + const EDVN_EID_NOT_SUPPORTED: u64 = 1; + + public(friend) fun err_EDVN_EID_NOT_SUPPORTED(): u64 { EDVN_EID_NOT_SUPPORTED } +} diff --git a/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_libs/dvn_fee_lib_0/tests/dvn_fee_lib_tests.move b/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_libs/dvn_fee_lib_0/tests/dvn_fee_lib_tests.move new file mode 100644 index 00000000..f0402e87 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_libs/dvn_fee_lib_0/tests/dvn_fee_lib_tests.move @@ -0,0 +1,452 @@ +#[test_only] +module dvn_fee_lib_0::dvn_fee_lib_tests { + use std::account::{Self, create_signer_for_test}; + + use dvn_fee_lib_0::dvn_fee_lib::{apply_premium, get_calldata_size_for_fee, get_dvn_fee, get_dvn_fee_internal}; + use endpoint_v2_common::contract_identity::make_call_ref_for_test; + use endpoint_v2_common::native_token_test_helpers::initialize_native_token_for_test; + use price_feed_module_0::eid_model_pair::{ + Self, + ARBITRUM_MODEL_TYPE, + DEFAULT_MODEL_TYPE, + EidModelPair, + new_eid_model_pair, + OPTIMISM_MODEL_TYPE + }; + use price_feed_module_0::price::{Self, EidTaggedPrice, tag_price_with_eid}; + use worker_common::worker_config::{Self, WORKER_ID_DVN}; + + const APTOS_NATIVE_DECIMAL_RATE: u128 = 100_000_000; + + // Test params + const CHAIN_FEE: u128 = 1 * 100_000_000; // 1 native * 1e18 e.g. on ethereum + + #[test] + fun test_get_fee() { + // 1. Set up the price feed (@price_feed_module_0, @1111) + use price_feed_module_0::feeds; + + initialize_native_token_for_test(); + let feed = &create_signer_for_test(@1111); + let updater = &create_signer_for_test(@9999); + feeds::initialize(feed); + feeds::enable_feed_updater(feed, @9999); + + // These prices are the same as used in the individual model tests + // We are testing whether we get the expected model response + // Using different price ratios for goerli, sepolia to see that arbitrum calcs are using the correct L2 price + let eth_price = price::new_price(4000, 51, 33); + let eth_goerli_price = price::new_price(40000, 51, 33); + let eth_sepolia_price = price::new_price(400000, 51, 33); + let arb_price = price::new_price(1222, 12, 3); + let opt_price = price::new_price(200, 43, 5); + + feeds::set_denominator(feed, 100); + feeds::set_arbitrum_compression_percent(feed, 47); + feeds::set_arbitrum_traits(updater, @1111, 5432, 11); + feeds::set_native_token_price_usd(updater, @1111, 6); + + // Test some non-hardcoded model types + let eid_model_pairs = vector[ + new_eid_model_pair( + 110, + DEFAULT_MODEL_TYPE() + ), // cannot override hardcoded type - this will still be "ARBITRUM" + new_eid_model_pair(11000, OPTIMISM_MODEL_TYPE()), // optimism using L1 sepolia + new_eid_model_pair(25555, ARBITRUM_MODEL_TYPE()), + new_eid_model_pair(26666, OPTIMISM_MODEL_TYPE()), + ]; + + let pairs_serialized = eid_model_pair::serialize_eid_model_pair_list(&eid_model_pairs); + feeds::set_eid_models(feed, pairs_serialized); + + let list = vector[ + tag_price_with_eid(101, eth_price), // First 6 EIDs are all of hardcoded types + tag_price_with_eid(110, arb_price), + tag_price_with_eid(111, opt_price), + tag_price_with_eid(10101, eth_price), + tag_price_with_eid(10143, arb_price), + tag_price_with_eid(10132, opt_price), + tag_price_with_eid(11000, opt_price), // optimism using L1 sepolia + tag_price_with_eid(10121, eth_goerli_price), // eth-goerli - used for arbitrum estimate + tag_price_with_eid(10161, eth_sepolia_price), // eth-sepolia - used for arbitrum estimate + + tag_price_with_eid(24444, eth_price), // not hardcoded and not set - should default to "DEFAULT" + tag_price_with_eid(25555, arb_price), // configured to "ARBITRUM" + tag_price_with_eid(26666, opt_price), // configured to "OPTIMISM" + tag_price_with_eid(20121, eth_goerli_price), // eth-goerli - used for arbitrum estimate + ]; + let prices_serialized = price::serialize_eid_tagged_price_list(&list); + feeds::set_price(updater, @1111, prices_serialized); + + let (fee, price_ratio, denominator, native_token_price) = feeds::estimate_fee_on_send( + @1111, + 10101, + 50, + 100, + ); + assert!(fee == 3570000, 0); + assert!(price_ratio == 4000, 1); + assert!(denominator == 100, 2); + assert!(native_token_price == 6, 3); + + // 2. Set up the worker (@1234) + let worker = @1234; + initialize_native_token_for_test(); + worker_config::initialize_for_worker_test_only( + worker, + WORKER_ID_DVN(), + worker, + @0x501ead, + vector[@111], + vector[@222], + @0xfee11b, + ); + worker_config::set_dvn_dst_config( + &make_call_ref_for_test(worker), + 10101, + 1000, + 50, + 100, + ); + worker_common::multisig::initialize_for_worker_test_only(worker, 1, vector[ + x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a" + ]); + worker_config::set_price_feed(&make_call_ref_for_test(worker), @price_feed_module_0, @1111); + + let (fee, deposit) = get_dvn_fee( + @222, + worker, + 10101, + @1234, + b"unused header", + b"unused hash", + 2, + b"1123", + ); + + assert!(fee != 0, 0); + assert!(deposit == @1234, 0); + + // test with a different deposit address + account::create_account_for_test(@4321); + worker_config::set_deposit_address(&make_call_ref_for_test(worker), @4321); + + let (_fee, deposit) = get_dvn_fee( + @222, + worker, + 10101, + @1234, + b"unused header", + b"unused hash", + 2, + b"1123", + ); + assert!(deposit == @4321, 0); + } + + #[test] + fun test_get_fee_internal() { + initialize_native_token_for_test(); + // Set up the worker (@1234) + let worker = @1234; + initialize_native_token_for_test(); + worker_config::initialize_for_worker_test_only( + worker, + WORKER_ID_DVN(), + worker, + @0x501ead, + vector[@111], + vector[@222], + @0xfee11b, + ); + worker_config::set_dvn_dst_config( + &make_call_ref_for_test(worker), + 10101, + 900, + 10050, + 1, + ); + worker_common::multisig::initialize_for_worker_test_only(worker, 1, vector[ + x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a" + ]); + worker_config::set_price_feed(&make_call_ref_for_test(worker), @1111, @2222); + + let called = false; + assert!(!called, 0); + + let fee = get_dvn_fee_internal( + worker, + 10101, + |price_feed, feed_address, total_gas| { + called = true; + assert!(price_feed == @1111, 0); + assert!(feed_address == @2222, 1); + // from the dvn_dst_config + assert!(total_gas == 900, 2); + + // 20_000_000 for APTOS 8-decimals - adjust for other native tokens + let native_price_usd = 20_000_000 * worker_config::get_native_decimals_rate() / 100_000_000; + (40000, 200, 1_000_000, native_price_usd) + }, + ); + + assert!(called, 1); + + // (10050 multiplier_bps) * (40000 chain_fee) / 10000 = 40200 + // vs. + // 40000 chain_fee + (1 floor margin) * (100_000_000 native_decimals_rate) / 20_000_000 native_price_usd = 40005 + // 40200 > 40005 + assert!(fee == 40200, 0); + } + + #[test] + fun test_get_fee_with_delegate() { + // other worker + initialize_native_token_for_test(); + worker_config::initialize_for_worker_test_only( + @5555, + 1, + @5555, + @0x501ead, + vector[@111], + vector[@222], + @0xfee11b, + ); + let other_worker_call_ref = &make_call_ref_for_test(@5555); + worker_config::set_price_feed( + other_worker_call_ref, + @0xabcd, + @1234, + ); + + // Set up the worker (@1234) + let worker = @1234; + initialize_native_token_for_test(); + worker_config::initialize_for_worker_test_only( + worker, + WORKER_ID_DVN(), + worker, + @0x501ead, + vector[@111], + vector[@222], + @0xfee11b, + ); + worker_config::set_dvn_dst_config( + &make_call_ref_for_test(worker), + 10101, + 900, + 10050, + 1, + ); + worker_common::multisig::initialize_for_worker_test_only(worker, 1, vector[ + x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a" + ]); + worker_config::set_price_feed_delegate( + &make_call_ref_for_test(worker), + @5555, + ); + + let called = false; + assert!(!called, 0); + + let fee = get_dvn_fee_internal( + worker, + 10101, + |price_feed, feed_address, total_gas| { + called = true; + assert!(price_feed == @0xabcd, 0); + assert!(feed_address == @1234, 1); + // from the dvn_dst_config + assert!(total_gas == 900, 2); + + // 200_000 for APTOS 8-decimals - adjust for other native tokens + let native_price_usd = 200_000 * worker_config::get_native_decimals_rate() / 100_000_000; + (40000, 200, 100_000, native_price_usd) + }, + ); + + assert!(called, 1); + + // (10050 multiplier_bps) * (40000 chain_fee) / 10000 = 40200 + // vs. + // 40000 chain_fee + (1 floor margin) * (100_000_000 native_decimals_rate) / 200_000 native_price_usd = 40500 + // 40200 < 40500 + assert!(fee == 40500, 0); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::EWORKER_PAUSED)] + fun test_get_fee_will_fail_if_worker_paused() { + let worker = @1234; + initialize_native_token_for_test(); + worker_common::worker_config::initialize_for_worker_test_only( + worker, + WORKER_ID_DVN(), + worker, + @0x501ead, + vector[@111], + vector[@222], + @0xfee11b, + ); + worker_config::set_worker_pause(&make_call_ref_for_test(worker), true); + + get_dvn_fee( + @555, + worker, + 12, + @1001, + b"123", + x"1234567890123456789012345678901234567890123456789012345678901234", + 2, + b"1123" + ); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::ESENDER_DENIED)] + fun test_get_fee_will_fail_if_sender_not_allowed() { + let worker = @1234; + initialize_native_token_for_test(); + worker_common::worker_config::initialize_for_worker_test_only( + worker, + WORKER_ID_DVN(), + worker, + @0x501ead, + vector[@111], + vector[@222], + @0xfee11b, + ); + // create an allowlist without the sender + worker_config::set_allowlist(&make_call_ref_for_test(worker), @55555555, true); + + get_dvn_fee( + @555, + worker, + 12, + @1001, // not on allowlist + b"123", + x"1234567890123456789012345678901234567890123456789012345678901234", + 2, + b"1123" + ); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::EWORKER_AUTH_UNSUPPORTED_MSGLIB)] + fun test_get_fee_will_fail_if_msglib_not_supported() { + let worker = @1234; + initialize_native_token_for_test(); + worker_common::worker_config::initialize_for_worker_test_only( + worker, + WORKER_ID_DVN(), + worker, + @0x501ead, + vector[@111], + vector[@222], + @0xfee11b, + ); + + // not selecting msglib as supported + + get_dvn_fee( + @555, + worker, + 12, + @1991, + b"123", + x"1234567890123456789012345678901234567890123456789012345678901234", + 2, + b"1123" + ); + } + + #[test] + fun test_apply_premium_no_native_price_usd_or_floor_margin_usd_set() { + // if native_price_usd is not set or floor_margin_usd is not set, fee = fee_with_multiplier + // fee = 100_000_000 * 120% = 120_000_000 + let expected_fee = 120_000_000; + assert!(apply_premium(CHAIN_FEE, 0, 12000, 0, 10500, APTOS_NATIVE_DECIMAL_RATE) == expected_fee, 0); + assert!( + apply_premium( + CHAIN_FEE, + 500_000_000_000_000_000_000, + 12000, + 0, + 10500, + APTOS_NATIVE_DECIMAL_RATE, + ) == expected_fee, + 1, + ); + assert!( + apply_premium( + CHAIN_FEE, + 0, + 12000, + 10_000_000_000_000_000_000 /* 0.10 usd */, + 10500, + APTOS_NATIVE_DECIMAL_RATE, + ) == expected_fee, + 2, + ); + } + + #[test] + fun test_apply_premium_with_floor_margin_greater() { + // chain_fee = 100_000_000 (1 native) + // native_price_usd = 1 USD = 100_000_000_000_000_000_000 + // floor_margin_usd = 2 USD = 200_000_000_000_000_000_000 + // floor_margin_in_native = + // 100_000_000 (chain_fee) + 200_000_000_000_000_000_000 (floor_margin_usd) * 100_000_000 (native_decimals_rate) / 100_000_000_000_000_000_000 (native_price_usd) + // = 300_000_000 + let fee = apply_premium( + CHAIN_FEE, + 100_000_000_000_000_000_000, + 12000, 200_000_000_000_000_000_000 /* 2usd */, + 10500, + APTOS_NATIVE_DECIMAL_RATE, + ); + let expected_fee = 300_000_000; + assert!(fee == expected_fee, 0); + } + + #[test] + fun test_apply_premium_with_floor_margin_less() { + // chain_fee = 100_000_000 (1 native) + // native_price_usd = 1 USD = 100_000_000_000_000_000_000 + // floor_margin_usd = 0.02 USD = 2_000_000_000_000_000_000 + // floor_margin_in_native = + // 100_000_000 (chain_fee) + 100_000_000_000_000_000_000 (floor_margin_usd) * 100_000_000 (native_decimals_rate) / 100_000_000_000_000_000_000 (native_price_usd) + // = 300_000_000 + let fee = apply_premium( + CHAIN_FEE, + 100_000_000_000_000_000_000, + 12000, + 2_000_000_000_000_000_000 /* 2usd */, + 10500, + APTOS_NATIVE_DECIMAL_RATE, + ); + let expected_fee = 120_000_000; + assert!(fee == expected_fee, 0); + } + + #[test] + fun test_get_calldata_size_for_fee() { + let worker = @1234; + initialize_native_token_for_test(); + worker_common::worker_config::initialize_for_worker_test_only( + worker, + WORKER_ID_DVN(), + worker, + @0x501ead, + vector[@111], + vector[@222], + @0xfee11b, + ); + worker_common::multisig::initialize_for_worker_test_only(worker, 1, vector[ + x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a", + ]); + + get_calldata_size_for_fee(worker); + } +} diff --git a/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_libs/executor_fee_lib_0/Move.toml b/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_libs/executor_fee_lib_0/Move.toml new file mode 100644 index 00000000..b4503e57 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_libs/executor_fee_lib_0/Move.toml @@ -0,0 +1,34 @@ +[package] +name = "executor_fee_lib_0" +version = "1.0.0" + +[addresses] +executor_fee_lib_0 = "_" +endpoint_v2_common = "_" +endpoint_v2 = "_" +layerzero_admin = "_" +layerzero_treasury_admin = "_" +price_feed_router_0 = "_" +price_feed_router_1 = "_" +price_feed_module_0 = "_" + +worker_common = "_" +msglib_types = "_" + +[dev-addresses] +executor_fee_lib_0 = "0x3000a" +endpoint_v2_common = "0x9098" +endpoint_v2 = "0x12345678" +layerzero_admin = "0x18943124" +layerzero_treasury_admin = "0x1894312499" +worker_common = "0x3999" +msglib_types = "0x13242342" +price_feed_router_0 = "0x65DD71A" +price_feed_router_1 = "0x65DD71AB" +price_feed_module_0 = "0x65DD71" + +[dependencies] +endpoint_v2_common = { local = "../../../endpoint_v2_common" } +worker_common = { local = "../../worker_common" } +price_feed_router_0 = { local = "../../price_feed_routers/price_feed_router_0" } +msglib_types = { local = "../../../msglib/msglib_types" } \ No newline at end of file diff --git a/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_libs/executor_fee_lib_0/sources/executor_fee_lib.move b/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_libs/executor_fee_lib_0/sources/executor_fee_lib.move new file mode 100644 index 00000000..d5d985ee --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_libs/executor_fee_lib_0/sources/executor_fee_lib.move @@ -0,0 +1,199 @@ +module executor_fee_lib_0::executor_fee_lib { + use std::vector; + + use executor_fee_lib_0::executor_option::{Self, ExecutorOptions}; + use msglib_types::worker_options::EXECUTOR_WORKER_ID; + use price_feed_router_0::router as price_feed_router; + use worker_common::worker_config; + + #[test_only] + friend executor_fee_lib_0::executor_fee_lib_tests; + + #[view] + /// Get the total executor fee, including premiums for an Executor worker to send a message + /// This checks that the Message Library is supported by the worker, the sender is allowed, the worker is + /// unpaused, and that the worker is an executor + public fun get_executor_fee( + msglib: address, + worker: address, + dst_eid: u32, + sender: address, + message_size: u64, + options: vector, + ): (u64, address) { + worker_config::assert_fee_lib_supports_transaction(worker, EXECUTOR_WORKER_ID(), sender, msglib); + + let executor_options = executor_option::extract_executor_options(&options, &mut 0); + let fee = get_executor_fee_internal( + worker, + dst_eid, + executor_options, + // Price Feed Estimate Fee on Send - partially applying parameters available in scope + |price_feed, feed_address, total_gas| price_feed_router::estimate_fee_on_send( + price_feed, + feed_address, + dst_eid, + message_size, + total_gas, + ) + ); + let deposit_address = worker_config::get_deposit_address(worker); + (fee, deposit_address) + } + + /// Get the total executor fee, using a provided price feed fee estimation function + /// + /// @param worker: The worker address + /// @param dst_eid: The destination EID + /// @param options: The executor options + /// @param estimate_fee_on_send: fee estimator + /// |price_feed, feed_address, total_remote_gas| -> (local_chain_fee, price_ratio, denominator, native_price_usd) + /// @return The total fee + public(friend) inline fun get_executor_fee_internal( + worker: address, + dst_eid: u32, + options: ExecutorOptions, + estimate_fee_on_send: |address, address, u128| (u128, u128, u128, u128) + ): u64 { + let ( + lz_receive_base_gas, multiplier_bps, floor_margin_usd, native_cap, lz_compose_base_gas, + ) = worker_config::get_executor_dst_config_values(worker, dst_eid); + assert!(lz_receive_base_gas != 0, err_EEXECUTOR_EID_NOT_SUPPORTED()); + + let (total_dst_amount, total_gas) = calculate_executor_dst_amount_and_total_gas( + is_v1_eid(dst_eid), + lz_receive_base_gas, + lz_compose_base_gas, + native_cap, + options, + ); + let (price_feed, feed_address) = worker_config::get_effective_price_feed(worker); + let (chain_fee, price_ratio, denominator, native_price_usd) = estimate_fee_on_send( + price_feed, feed_address, total_gas, + ); + + let default_multiplier_bps = worker_config::get_default_multiplier_bps(worker); + let multiplier_bps = if (multiplier_bps == 0) default_multiplier_bps else multiplier_bps; + let native_decimals_rate = worker_common::worker_config::get_native_decimals_rate(); + let fee = apply_premium_to_gas( + chain_fee, + multiplier_bps, + floor_margin_usd, + native_price_usd, + native_decimals_rate, + ); + fee = fee + convert_and_apply_premium_to_value( + total_dst_amount, + price_ratio, + denominator, + multiplier_bps, + ); + (fee as u64) + } + + // ================================================ Internal Functions ================================================ + + /// Apply the premium to the fee, this will take the higher of the multiplier applied to the fee or the floor margin + /// added to the fee + public(friend) fun apply_premium_to_gas( + fee: u128, + multiplier_bps: u16, + margin_usd: u128, + native_price_usd: u128, + native_decimals_rate: u128, + ): u128 { + let fee_with_multiplier = (fee * (multiplier_bps as u128)) / 10000; + if (native_price_usd == 0 || margin_usd == 0) { + return fee_with_multiplier + }; + let fee_with_margin = (margin_usd * native_decimals_rate) / native_price_usd + fee; + if (fee_with_margin > fee_with_multiplier) { fee_with_margin } else { fee_with_multiplier } + } + + /// Convert the destination value to the local chain native token and apply a multiplier to the value + public(friend) fun convert_and_apply_premium_to_value( + value: u128, + ratio: u128, + denominator: u128, + multiplier_bps: u16, + ): u128 { + if (value > 0) { (((value * ratio) / denominator) * (multiplier_bps as u128)) / 10000 } else 0 + } + + /// Check whether the EID is a V1 EID + public(friend) fun is_v1_eid(eid: u32): bool { eid < 30000 } + + /// Calculate the Destination Amount and Total Gas for the Executor + /// @return (destination amount, total gas) + public(friend) fun calculate_executor_dst_amount_and_total_gas( + is_v1_eid: bool, + lz_receive_base_gas: u64, + lz_compose_base_gas: u64, + native_cap: u128, + options: ExecutorOptions, + ): (u128, u128) { + let ( + lz_receive_options, + native_drop_options, + lz_compose_options, + ordered_execution_option, + ) = executor_option::unpack_options(options); + + // The total value to to be sent to the destination + let dst_amount: u128 = 0; + // The total gas to be used for the transaction + let lz_receive_gas: u128 = 0; + + // Loop through LZ Receive options + for (i in 0..vector::length(&lz_receive_options)) { + let option = *vector::borrow(&lz_receive_options, i); + let (gas, value) = executor_option::unpack_lz_receive_option(option); + + assert!(!is_v1_eid || value == 0, EEV1_DOES_NOT_SUPPORT_LZ_RECEIVE_WITH_VALUE); + dst_amount = dst_amount + value; + lz_receive_gas = lz_receive_gas + gas; + }; + assert!(lz_receive_gas > 0, EEXECUTOR_ZERO_LZRECEIVE_GAS_PROVIDED); + let total_gas = (lz_receive_base_gas as u128) + lz_receive_gas; + + // Loop through LZ Compose options + for (i in 0..vector::length(&lz_compose_options)) { + let option = *vector::borrow(&lz_compose_options, i); + let (_index, gas, value) = executor_option::unpack_lz_compose_option(option); + // Endpoint V1 doesnot support LZ Compose + assert!(!is_v1_eid, EEV1_DOES_NOT_SUPPORT_LZ_COMPOSE_WITH_VALUE); + assert!(gas > 0, EEXECUTOR_ZERO_LZCOMPOSE_GAS_PROVIDED); + dst_amount = dst_amount + value; + // The LZ Compose base gas is required for each LZ Compose, which is represented by the count of indexes. + // However, this calculation is simplified to match the EVM calculation, which does not deduplicate based on + // the Lz Compose index. Therefore, if there are multiple LZ Compose Options for a specific index, the + // Lz Compose base gas will also be duplicated by the number of options on that index + total_gas = total_gas + gas + (lz_compose_base_gas as u128); + }; + + // Loop through Native Drop options + for (i in 0..vector::length(&native_drop_options)) { + let option = *vector::borrow(&native_drop_options, i); + let (amount, _receiver) = executor_option::unpack_native_drop_option(option); + dst_amount = dst_amount + amount; + }; + assert!(dst_amount <= native_cap, EEXECUTOR_NATIVE_AMOUNT_EXCEEDS_CAP); + + // If ordered execution is enabled, increase the gas by 2% + if (ordered_execution_option) { + total_gas = (total_gas * 102) / 100; + }; + (dst_amount, total_gas) + } + + // ================================================== Error Codes ================================================= + + const EEV1_DOES_NOT_SUPPORT_LZ_COMPOSE_WITH_VALUE: u64 = 1; + const EEV1_DOES_NOT_SUPPORT_LZ_RECEIVE_WITH_VALUE: u64 = 2; + const EEXECUTOR_EID_NOT_SUPPORTED: u64 = 3; + const EEXECUTOR_NATIVE_AMOUNT_EXCEEDS_CAP: u64 = 4; + const EEXECUTOR_ZERO_LZCOMPOSE_GAS_PROVIDED: u64 = 5; + const EEXECUTOR_ZERO_LZRECEIVE_GAS_PROVIDED: u64 = 6; + + public(friend) fun err_EEXECUTOR_EID_NOT_SUPPORTED(): u64 { EEXECUTOR_EID_NOT_SUPPORTED } +} \ No newline at end of file diff --git a/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_libs/executor_fee_lib_0/sources/types/executor_option.move b/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_libs/executor_fee_lib_0/sources/types/executor_option.move new file mode 100644 index 00000000..60b34a51 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_libs/executor_fee_lib_0/sources/types/executor_option.move @@ -0,0 +1,243 @@ +module executor_fee_lib_0::executor_option { + use std::vector; + + use endpoint_v2_common::bytes32::Bytes32; + use endpoint_v2_common::serde; + use msglib_types::worker_options::EXECUTOR_WORKER_ID; + + const OPTION_TYPE_LZ_RECEIVE: u8 = 1; + const OPTION_TYPE_NATIVE_DROP: u8 = 2; + const OPTION_TYPE_LZ_COMPOSE: u8 = 3; + const OPTION_TYPE_ORDERED_EXECUTION: u8 = 4; + + + /// ExecutorOptions is used to specify the options for an executor + struct ExecutorOptions has drop, copy, store { + // The gas and value delivered via the LZ Receive operation + lz_receive_options: vector, + // The amount and receiver for the Native Drop operation + native_drop_options: vector, + // The gas and value for each LZ Compose operation + lz_compose_options: vector, + // Whether or not the execution will require ordered execution + ordered_execution_option: bool, + } + + /// The gas and value for the LZ Receive operation + struct LzReceiveOption has drop, copy, store { + gas: u128, + value: u128, + } + + /// The amount and receiver for the Native Drop operation + struct NativeDropOption has drop, copy, store { + amount: u128, + receiver: Bytes32, + } + + /// The gas, value, and index of a specific LZ Compose operation + struct LzComposeOption has drop, copy, store { + index: u16, + gas: u128, + value: u128, + } + + /// Unpacks ExecutorOptions into its components + public fun unpack_options( + options: ExecutorOptions, + ): (vector, vector, vector, bool) { + let ExecutorOptions { + lz_receive_options, + native_drop_options, + lz_compose_options, + ordered_execution_option, + } = options; + (lz_receive_options, native_drop_options, lz_compose_options, ordered_execution_option) + } + + /// Unpacks LzReceiveOption into its components + /// @return (gas, value) + public fun unpack_lz_receive_option(option: LzReceiveOption): (u128, u128) { + let LzReceiveOption { gas, value } = option; + (gas, value) + } + + /// Unpacks NativeDropOption into its components + /// @return (amount, receiver) + public fun unpack_native_drop_option(option: NativeDropOption): (u128, Bytes32) { + let NativeDropOption { amount, receiver } = option; + (amount, receiver) + } + + /// Unpacks LzComposeOption into its components + /// @return (compose index, gas, value) + public fun unpack_lz_compose_option(option: LzComposeOption): (u16, u128, u128) { + let LzComposeOption { index, gas, value } = option; + (index, gas, value) + } + + /// Creates a new ExecutorOptions from its components + public fun new_executor_options( + lz_receive_options: vector, + native_drop_options: vector, + lz_compose_options: vector, + ordered_execution_option: bool, + ): ExecutorOptions { + ExecutorOptions { lz_receive_options, native_drop_options, lz_compose_options, ordered_execution_option } + } + + /// Creates a new LzReceiveOption from its components + public fun new_lz_receive_option(gas: u128, value: u128): LzReceiveOption { + LzReceiveOption { gas, value } + } + + /// Creates a new NativeDropOption from its components + public fun new_native_drop_option(amount: u128, receiver: Bytes32): NativeDropOption { + NativeDropOption { amount, receiver } + } + + /// Creates a new LzComposeOption from its components + public fun new_lz_compose_option(index: u16, gas: u128, value: u128): LzComposeOption { + LzComposeOption { index, gas, value } + } + + /// Extracts an ExecutorOptions from a byte buffer + public fun extract_executor_options(buf: &vector, pos: &mut u64): ExecutorOptions { + let options = ExecutorOptions { + lz_receive_options: vector[], + native_drop_options: vector[], + lz_compose_options: vector[], + ordered_execution_option: false, + }; + let len = vector::length(buf); + while (*pos < len) { + let _worker_id = serde::extract_u8(buf, pos); + // The serialized option_size includes 1 byte for the option_type. Subtracting 1 byte is the number of bytes + // that should be read after reading the option type + let option_size = serde::extract_u16(buf, pos) - 1; + let option_type = serde::extract_u8(buf, pos); + + if (option_type == OPTION_TYPE_LZ_RECEIVE) { + // LZ Receive + let option = extract_lz_receive_option(buf, option_size, pos); + vector::push_back(&mut options.lz_receive_options, option) + } else if (option_type == OPTION_TYPE_NATIVE_DROP) { + // Native Drop + let option = extract_native_drop_option(buf, option_size, pos); + vector::push_back(&mut options.native_drop_options, option) + } else if (option_type == OPTION_TYPE_LZ_COMPOSE) { + // LZ Compose + let option = extract_lz_compose_option(buf, option_size, pos); + vector::push_back(&mut options.lz_compose_options, option) + } else if (option_type == OPTION_TYPE_ORDERED_EXECUTION) { + // Ordered Execution + assert!(option_size == 0, EINVALID_ORDERED_EXECUTION_OPTION_LENGTH); + options.ordered_execution_option = true; + // Nothing else to read - continue to next + } else { + abort EUNSUPPORTED_OPTION + } + }; + options + } + + /// Appends an ExecutorOptions to a byte buffer + public fun append_executor_options(buf: &mut vector, options: &ExecutorOptions) { + vector::for_each_ref(&options.lz_receive_options, |option| { + serde::append_u8(buf, EXECUTOR_WORKER_ID()); + append_lz_receive_option(buf, option) + }); + vector::for_each_ref(&options.native_drop_options, |option| { + serde::append_u8(buf, EXECUTOR_WORKER_ID()); + append_native_drop_option(buf, option) + }); + vector::for_each_ref(&options.lz_compose_options, |option| { + serde::append_u8(buf, EXECUTOR_WORKER_ID()); + append_lz_compose_option(buf, option) + }); + if (options.ordered_execution_option) { + serde::append_u8(buf, EXECUTOR_WORKER_ID()); + append_ordered_execution_option(buf); + } + } + + /// Extracts a LzReceiveOption from a buffer and updates position to the end of the read + fun extract_lz_receive_option(option: &vector, size: u16, pos: &mut u64): LzReceiveOption { + let gas = serde::extract_u128(option, pos); + let value = if (size == 32) { + serde::extract_u128(option, pos) + } else if (size == 16) { + 0 + } else { + abort EINVALID_LZ_RECEIVE_OPTION_LENGTH + }; + LzReceiveOption { gas, value } + } + + /// Serializes a LzReceiveOption to the end of a buffer + fun append_lz_receive_option(output: &mut vector, lz_receive_option: &LzReceiveOption) { + let size = if (lz_receive_option.value == 0) { 17 } else { 33 }; + serde::append_u16(output, size); + serde::append_u8(output, OPTION_TYPE_LZ_RECEIVE); + serde::append_u128(output, lz_receive_option.gas); + if (lz_receive_option.value != 0) { + serde::append_u128(output, lz_receive_option.value); + } + } + + /// Extracts a NativeDropOption from a buffer and updates position to the end of the read + fun extract_native_drop_option(option: &vector, size: u16, pos: &mut u64): NativeDropOption { + assert!(size == 48, EINVALID_NATIVE_DROP_OPTION_LENGTH); + let amount = serde::extract_u128(option, pos); + let receiver = serde::extract_bytes32(option, pos); + NativeDropOption { amount, receiver } + } + + /// Serializes a NativeDropOption to the end of a buffer + fun append_native_drop_option(output: &mut vector, native_drop_option: &NativeDropOption) { + serde::append_u16(output, 49); + serde::append_u8(output, OPTION_TYPE_NATIVE_DROP); + serde::append_u128(output, native_drop_option.amount); + serde::append_bytes32(output, native_drop_option.receiver); + } + + /// Extracts a LzComposeOption from a buffer + fun extract_lz_compose_option(option: &vector, size: u16, pos: &mut u64): LzComposeOption { + let index = serde::extract_u16(option, pos); + let gas = serde::extract_u128(option, pos); + let value = if (size == 34) { + serde::extract_u128(option, pos) + } else if (size == 18) { + 0 + } else { + abort EINVALID_LZ_COMPOSE_OPTION_LENGTH + }; + LzComposeOption { index, gas, value } + } + + /// Serializes a LzComposeOption to the end of a buffer + fun append_lz_compose_option(output: &mut vector, lz_compose_option: &LzComposeOption) { + let size = if (lz_compose_option.value == 0) { 19 } else { 35 }; + serde::append_u16(output, size); + serde::append_u8(output, OPTION_TYPE_LZ_COMPOSE); + serde::append_u16(output, lz_compose_option.index); + serde::append_u128(output, lz_compose_option.gas); + if (lz_compose_option.value != 0) { + serde::append_u128(output, lz_compose_option.value); + } + } + + /// Serializes an ordered execution option into a buffer + fun append_ordered_execution_option(output: &mut vector) { + serde::append_u16(output, 1); // size = 1 + serde::append_u8(output, OPTION_TYPE_ORDERED_EXECUTION); + } + + // ================================================== Error Codes ================================================= + + const EINVALID_LZ_COMPOSE_OPTION_LENGTH: u64 = 1; + const EINVALID_LZ_RECEIVE_OPTION_LENGTH: u64 = 2; + const EINVALID_NATIVE_DROP_OPTION_LENGTH: u64 = 3; + const EINVALID_ORDERED_EXECUTION_OPTION_LENGTH: u64 = 4; + const EUNSUPPORTED_OPTION: u64 = 5; +} diff --git a/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_libs/executor_fee_lib_0/tests/executor_fee_lib_tests.move b/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_libs/executor_fee_lib_0/tests/executor_fee_lib_tests.move new file mode 100644 index 00000000..a40504d4 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_libs/executor_fee_lib_0/tests/executor_fee_lib_tests.move @@ -0,0 +1,709 @@ +#[test_only] +module executor_fee_lib_0::executor_fee_lib_tests { + use std::account; + use std::account::create_signer_for_test; + + use endpoint_v2_common::bytes32; + use endpoint_v2_common::contract_identity::make_call_ref_for_test; + use endpoint_v2_common::native_token_test_helpers::initialize_native_token_for_test; + use endpoint_v2_common::serde; + use executor_fee_lib_0::executor_fee_lib::{ + apply_premium_to_gas, calculate_executor_dst_amount_and_total_gas, convert_and_apply_premium_to_value, + get_executor_fee, get_executor_fee_internal, is_v1_eid, + }; + use executor_fee_lib_0::executor_option::{ + append_executor_options, new_executor_options, new_lz_compose_option, new_lz_receive_option, + new_native_drop_option, + }; + use price_feed_module_0::eid_model_pair; + use price_feed_module_0::eid_model_pair::{ + ARBITRUM_MODEL_TYPE, DEFAULT_MODEL_TYPE, EidModelPair, new_eid_model_pair, OPTIMISM_MODEL_TYPE, + }; + use price_feed_module_0::price; + use price_feed_module_0::price::{EidTaggedPrice, tag_price_with_eid}; + use worker_common::worker_config::{Self, set_executor_dst_config}; + + #[test] + fun test_get_fee() { + // 1. Set up the price feed (@price_feed_module_0, @1111) + use price_feed_module_0::feeds; + let feed = &create_signer_for_test(@1111); + let updater = &create_signer_for_test(@9999); + feeds::initialize(feed); + feeds::enable_feed_updater(feed, @9999); + + initialize_native_token_for_test(); + + // These prices are the same as used in the individual model tests + // We are testing whether we get the expected model response + // Using different price ratios for goerli, sepolia to see that arbitrum calcs are using the correct L2 price + let eth_price = price::new_price(4000, 51, 33); + let eth_goerli_price = price::new_price(40000, 51, 33); + let eth_sepolia_price = price::new_price(400000, 51, 33); + let arb_price = price::new_price(1222, 12, 3); + let opt_price = price::new_price(200, 43, 5); + + feeds::set_denominator(feed, 100); + feeds::set_arbitrum_compression_percent(feed, 47); + feeds::set_arbitrum_traits(updater, @1111, 5432, 11); + feeds::set_native_token_price_usd(updater, @1111, 6); + + // Test some non-hardcoded model types + let eid_model_pairs = vector[ + new_eid_model_pair( + 110, + DEFAULT_MODEL_TYPE() + ), // cannot override hardcoded type - this will still be "ARBITRUM" + new_eid_model_pair(11000, OPTIMISM_MODEL_TYPE()), // optimism using L1 sepolia + new_eid_model_pair(25555, ARBITRUM_MODEL_TYPE()), + new_eid_model_pair(26666, OPTIMISM_MODEL_TYPE()), + ]; + + let pairs_serialized = eid_model_pair::serialize_eid_model_pair_list(&eid_model_pairs); + feeds::set_eid_models(feed, pairs_serialized); + + let list = vector[ + tag_price_with_eid(101, eth_price), // First 6 EIDs are all of hardcoded types + tag_price_with_eid(110, arb_price), + tag_price_with_eid(111, opt_price), + tag_price_with_eid(10101, eth_price), + tag_price_with_eid(10143, arb_price), + tag_price_with_eid(10132, opt_price), + tag_price_with_eid(11000, opt_price), // optimism using L1 sepolia + tag_price_with_eid(10121, eth_goerli_price), // eth-goerli - used for arbitrum estimate + tag_price_with_eid(10161, eth_sepolia_price), // eth-sepolia - used for arbitrum estimate + + tag_price_with_eid(24444, eth_price), // not hardcoded and not set - should default to "DEFAULT" + tag_price_with_eid(25555, arb_price), // configured to "ARBITRUM" + tag_price_with_eid(26666, opt_price), // configured to "OPTIMISM" + tag_price_with_eid(20121, eth_goerli_price), // eth-goerli - used for arbitrum estimate + ]; + let prices_serialized = price::serialize_eid_tagged_price_list(&list); + feeds::set_price(updater, @1111, prices_serialized); + + let (fee, price_ratio, denominator, native_token_price) = feeds::estimate_fee_on_send( + @1111, + 10101, + 50, + 100, + ); + assert!(fee == 3570000, 0); + assert!(price_ratio == 4000, 1); + assert!(denominator == 100, 2); + assert!(native_token_price == 6, 3); + + // 2. Set up the worker (@1234) + let worker = @1234; + initialize_native_token_for_test(); + worker_config::initialize_for_worker_test_only( + worker, + 1, + worker, + @0x501ead, + vector[@111], + vector[@222], + @0xfee11b, + ); + worker_config::set_executor_dst_config( + &make_call_ref_for_test(worker), + 10101, + 1000, + 50, + 100, + 100000000000, + 100002132, + ); + worker_config::set_price_feed( + &make_call_ref_for_test(worker), + @price_feed_module_0, + @1111, + ); + + let options = serde::bytes_of(|buf| append_executor_options(buf, &new_executor_options( + vector[ + new_lz_receive_option(100, 0), + ], + vector[], + vector[], + false, + ))); + let (fee, deposit) = get_executor_fee( + @222, + worker, + 10101, + @5555, + 1000, + options, + ); + + assert!(fee != 0, 0); + // worker address + assert!(deposit == @1234, 1); + + // test after updating deposit address + account::create_account_for_test(@4321); + worker_config::set_deposit_address(&make_call_ref_for_test(worker), @4321); + + let options = serde::bytes_of(|buf| append_executor_options(buf, &new_executor_options( + vector[ + new_lz_receive_option(100, 0), + ], + vector[], + vector[], + false, + ))); + + let (_fee, deposit) = get_executor_fee( + @222, + worker, + 10101, + @5555, + 1000, + options, + ); + + assert!(deposit == @4321, 1); + } + + #[test] + fun test_get_fee_internal() { + initialize_native_token_for_test(); + // Set up the worker (@1234) + let worker = @1234; + worker_config::initialize_for_worker_test_only( + worker, + 1, + worker, + @0x501ead, + vector[@111], + vector[@222], + @0xfee11b, + ); + worker_config::set_executor_dst_config( + &make_call_ref_for_test(worker), + 10101, + 1000, + 50, + 100, + 100000000000, + 100002132, + ); + worker_config::set_price_feed( + &make_call_ref_for_test(worker), + @0x501ead, + @111, + ); + + let called = false; + assert!(!called, 0); + + let fee = get_executor_fee_internal( + worker, + 10101, + new_executor_options( + vector[ + new_lz_receive_option(100, 0), + ], + vector[], + vector[], + false, + ), + |price_feed_module, feed_address, total_gas| { + called = true; + assert!(price_feed_module == @0x501ead, 0); + assert!(feed_address == @111, 1); + + // [Calculate Executor DST Amount and Gas] 1000 (lz_receive base gas) + 100 (lz_receive gas) = 200 + assert!(total_gas == 1100, 1); + (20000 /*chain fee*/, 40 /*price_ratio*/, 1_000_000 /*denominator*/, 0 /*native_price_usd*/) + } + ); + assert!(called, 2); + + // [Apply Premium to gas] 20000 (fee) * 50 (multiplier) / 10000 = 100 + assert!(fee == 100, 0); + } + + #[test] + fun test_get_fee_works_with_delegated_price_feed() { + initialize_native_token_for_test(); + // other worker + worker_config::initialize_for_worker_test_only( + @5555, + 1, + @5555, + @0x501ead, + vector[@111], + vector[@222], + @0xfee11b, + ); + worker_config::set_price_feed( + &make_call_ref_for_test(@5555), + @0xabcd, + @1234, + ); + + // Set up the worker (@1234) + let worker = @1234; + worker_config::initialize_for_worker_test_only( + worker, + 1, + worker, + @0x501ead, + vector[@111], + vector[@222], + @0xfee11b, + ); + worker_config::set_executor_dst_config( + &make_call_ref_for_test(worker), + 10101, + 1000, + 50, + 100, + 100000000000, + 100002132, + ); + worker_config::set_price_feed_delegate( + &make_call_ref_for_test(worker), + @5555, + ); + + let called = false; + assert!(!called, 0); + + let fee = get_executor_fee_internal( + worker, + 10101, + new_executor_options( + vector[ + new_lz_receive_option(100, 0), + ], + vector[], + vector[], + false, + ), + |price_feed_module, feed_address, total_gas| { + called = true; + assert!(price_feed_module == @0xabcd, 0); + assert!(feed_address == @1234, 1); + + // [Calculate Executor DST Amount and Gas] 1000 (lz_receive base gas) + 100 (lz_receive gas) = 200 + assert!(total_gas == 1100, 1); + (20000 /*chain fee*/, 40 /*price_ratio*/, 1_000_000 /*denominator*/, 0 /*native_price_usd*/) + } + ); + assert!(called, 2); + + // [Apply Premium to gas] 20000 (fee) * 50 (multiplier) / 10000 = 100 + assert!(fee == 100, 0); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::EWORKER_PAUSED)] + fun test_get_fee_will_fail_if_worker_paused() { + let worker = @1234; + initialize_native_token_for_test(); + worker_common::worker_config::initialize_for_worker_test_only( + worker, + 1, + worker, + @0x501ead, + vector[@111], + vector[@222], + @0xfee11b, + ); + worker_config::set_worker_pause(&make_call_ref_for_test(worker), true); + + let options = serde::bytes_of(|buf| append_executor_options(buf, &new_executor_options( + vector[], + vector[], + vector[], + false, + ))); + + get_executor_fee( + @0xfee11b, + worker, + 12, + @555, + 1000, + options, + ); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::ESENDER_DENIED)] + fun test_get_fee_will_fail_if_sender_not_allowed() { + let worker = @1234; + initialize_native_token_for_test(); + worker_common::worker_config::initialize_for_worker_test_only( + worker, + 1, + worker, + @0x501ead, + vector[@111], + vector[@222], + @0xfee11b, + ); + // create an allowlist without the sender + worker_config::set_allowlist(&make_call_ref_for_test(worker), @55555555, true); + + worker_config::set_worker_fee_lib(&make_call_ref_for_test(worker), @0xfee11b); + set_executor_dst_config( + &make_call_ref_for_test(worker), + 12, + 1000, + 50, + 100, + 100000000000, + 100002132, + ); + + let options = serde::bytes_of(|buf| append_executor_options(buf, &new_executor_options( + vector[], + vector[], + vector[], + false, + ))); + + get_executor_fee( + @222, + worker, + 12, + @555, + 1000, + options, + ); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::EWORKER_AUTH_UNSUPPORTED_MSGLIB)] + fun test_get_fee_will_fail_if_msglib_not_supported() { + let worker = @1234; + initialize_native_token_for_test(); + worker_common::worker_config::initialize_for_worker_test_only( + worker, + 1, + worker, + @0x501ead, + vector[@111], + vector[@222], + @0xfee11b, + ); + + worker_config::set_worker_fee_lib(&make_call_ref_for_test(worker), @0xfee11b); + set_executor_dst_config( + &make_call_ref_for_test(worker), + 12, + 1000, + 50, + 100, + 100000000000, + 100002132, + ); + + let options = serde::bytes_of(|buf| append_executor_options(buf, &new_executor_options( + vector[], + vector[], + vector[], + false, + ))); + + get_executor_fee( + @1234, // not @222 + worker, + 12, + @555, + 1000, + options, + ); + } + + #[test] + fun test_apply_premium_to_gas_uses_multiplier_if_gt_fee_with_margin() { + let fee = apply_premium_to_gas( + 20000, + 10500, + 1, + 1, + 1, + ); + assert!(fee == 21000, 0); // 20000 * 10500 / 10000 + } + + #[test] + fun test_apply_premium_to_gas_uses_margin_if_gt_fee_with_multiplier() { + let fee = apply_premium_to_gas( + 20000, + 10500, + 6000, + 2000, + 1000, + ); + assert!(fee == 23000, 0); // 20000 + (6000 * 1000) / 2000 + } + + #[test] + fun test_apply_premium_to_gas_uses_margin_if_native_price_used_is_0() { + let fee = apply_premium_to_gas( + 20000, + 10500, + 6000, + 0, + 1000, + ); + assert!(fee == 21000, 0); // 20000 * 10500 / 10000; + } + + #[test] + fun test_apply_premium_to_gas_uses_multiplier_if_margin_usd_is_0() { + let fee = apply_premium_to_gas( + 20000, + 10500, + 0, + 1, + 1, + ); + assert!(fee == 21000, 0); // 20000 * 10500 / 10000 + } + + #[test] + fun test_convert_and_apply_premium_to_value() { + let fee = convert_and_apply_premium_to_value( + 9512000, + 123, + 1_000, + 600, // 6% + ); + assert!(fee == 70198, 0); // (((9512000*123)/1000) * 600) / 10000; + } + + #[test] + fun test_convert_and_apply_premium_to_value_returns_0_if_value_is_0() { + let fee = convert_and_apply_premium_to_value( + 0, + 112312323, + 1, + 1000, // 10% + ); + assert!(fee == 0, 0); + } + + #[test] + fun test_is_v1_eid() { + assert!(is_v1_eid(1), 0); + assert!(is_v1_eid(29999), 1); + assert!(!is_v1_eid(30000), 2); + assert!(!is_v1_eid(130000), 3); + } + + #[test] + fun test_calculate_executor_dst_amount_and_gas() { + let lz_receive_options = vector[ + new_lz_receive_option(100, 200), + new_lz_receive_option(300, 0), + ]; + let native_drop_options = vector[ + new_native_drop_option(100, bytes32::from_address(@123)), + new_native_drop_option(200, bytes32::from_address(@456)), + ]; + let lz_compose_options = vector[ + new_lz_compose_option(0, 400, 500), + new_lz_compose_option(1, 400, 500), + ]; + let options = new_executor_options( + lz_receive_options, + native_drop_options, + lz_compose_options, + false, + ); + + let (dst_amount, total_gas) = calculate_executor_dst_amount_and_total_gas( + false, // is_v1_eid + 100, // lz_receive_base_gas + 200, // lz_compose_base_gas + 100000000000, // native_cap + options, + ); + + let expected_total_gas = 0 + + 100 // lz_receive_base_gas + + 100 // lz_receive gas + + 300 // lz_receive gas + + 200 // lz_compose_base_gas + + 400 // lz_compose gas + + 200 // lz_compose_base_gas + + 400; // lz_compose gas + + let expected_dst_amount = 0 + + 200 // lz_receive value + + 0 // lz_receive value + + 100 // native_drop amount + + 200 // native_drop amount + + 500 // lz_compose value + + 500; // lz_compose value + + assert!(dst_amount == expected_dst_amount, 0); + assert!(total_gas == expected_total_gas, 1); + + // do again but use ordered execution option + let options = new_executor_options( + lz_receive_options, + native_drop_options, + lz_compose_options, + true, + ); + + let (dst_amount, total_gas) = calculate_executor_dst_amount_and_total_gas( + false, // is_v1_eid + 100, // lz_receive_base_gas + 200, // lz_compose_base_gas + 100000000000, // native_cap + options, + ); + + // 2% addtional fee, no change in dst_amount + let expected_total_gas = expected_total_gas * 102 / 100; + assert!(dst_amount == expected_dst_amount, 1); + assert!(total_gas == expected_total_gas, 2); + } + + #[test] + #[expected_failure(abort_code = executor_fee_lib_0::executor_fee_lib::EEXECUTOR_ZERO_LZRECEIVE_GAS_PROVIDED)] + fun test_calculate_executor_dst_amount_and_gas_should_fail_if_no_lz_receive_gas_provided() { + let lz_receive_options = vector[ + new_lz_receive_option(0, 200), + new_lz_receive_option(0, 0), + ]; + let native_drop_options = vector[]; + let lz_compose_options = vector[]; + + let options = new_executor_options( + lz_receive_options, + native_drop_options, + lz_compose_options, + false, + ); + + calculate_executor_dst_amount_and_total_gas( + false, // is_v1_eid + 100, // lz_receive_base_gas + 200, // lz_compose_base_gas + 100000000000, // native_cap + options, + ); + } + + #[test] + #[expected_failure(abort_code = executor_fee_lib_0::executor_fee_lib::EEXECUTOR_ZERO_LZCOMPOSE_GAS_PROVIDED)] + fun test_calculate_executor_dst_amount_and_gas_should_fail_if_lz_compose_gas_not_provided_on_any_of_the_options() { + let lz_receive_options = vector[ + new_lz_receive_option(100, 200), + new_lz_receive_option(300, 0), + ]; + let native_drop_options = vector[]; + let lz_compose_options = vector[ + new_lz_compose_option(0, 400, 500), + new_lz_compose_option(1, 0, 500), + ]; + + let options = new_executor_options( + lz_receive_options, + native_drop_options, + lz_compose_options, + false, + ); + + calculate_executor_dst_amount_and_total_gas( + false, // is_v1_eid + 100, // lz_receive_base_gas + 200, // lz_compose_base_gas + 100000000000, // native_cap + options, + ); + } + + #[test] + #[expected_failure(abort_code = executor_fee_lib_0::executor_fee_lib::EEXECUTOR_NATIVE_AMOUNT_EXCEEDS_CAP)] + fun test_calculate_executor_dst_amount_and_gas_should_fail_if_native_amount_exceeds_cap() { + let lz_receive_options = vector[ + new_lz_receive_option(100, 200), + new_lz_receive_option(300, 0), + ]; + let native_drop_options = vector[ + new_native_drop_option(100, bytes32::from_address(@123)), + new_native_drop_option(200, bytes32::from_address(@456)), + ]; + let lz_compose_options = vector[]; + + let options = new_executor_options( + lz_receive_options, + native_drop_options, + lz_compose_options, + false, + ); + + calculate_executor_dst_amount_and_total_gas( + false, // is_v1_eid + 100, // lz_receive_base_gas + 200, // lz_compose_base_gas + 100, // native_cap (less than the 500 total native amount) + options, + ); + } + + #[test] + #[expected_failure(abort_code = executor_fee_lib_0::executor_fee_lib::EEV1_DOES_NOT_SUPPORT_LZ_RECEIVE_WITH_VALUE)] + fun test_calculate_executor_dst_amount_and_gas_should_fail_if_v1_eid_and_lz_receive_value_provided() { + let lz_receive_options = vector[ + new_lz_receive_option(100, 200), + new_lz_receive_option(300, 0), + ]; + let native_drop_options = vector[]; + let lz_compose_options = vector[]; + + let options = new_executor_options( + lz_receive_options, + native_drop_options, + lz_compose_options, + false, + ); + + calculate_executor_dst_amount_and_total_gas( + true, // is_v1_eid + 100, // lz_receive_base_gas + 200, // lz_compose_base_gas + 100000000000, // native_cap + options, + ); + } + + #[test] + #[expected_failure(abort_code = executor_fee_lib_0::executor_fee_lib::EEV1_DOES_NOT_SUPPORT_LZ_COMPOSE_WITH_VALUE)] + fun test_calculate_executor_dst_amount_and_gas_should_fail_if_v1_eid_and_lz_compose_value_provided() { + let lz_receive_options = vector[ + new_lz_receive_option(100, 0), + new_lz_receive_option(300, 0), + ]; + let native_drop_options = vector[]; + let lz_compose_options = vector[ + new_lz_compose_option(0, 400, 500), + new_lz_compose_option(1, 400, 0), + ]; + + let options = new_executor_options( + lz_receive_options, + native_drop_options, + lz_compose_options, + true, + ); + + calculate_executor_dst_amount_and_total_gas( + true, // is_v1_eid + 100, // lz_receive_base_gas + 200, // lz_compose_base_gas + 100000000000, // native_cap + options, + ); + } +} diff --git a/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_libs/executor_fee_lib_0/tests/types/executor_option_tests.move b/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_libs/executor_fee_lib_0/tests/types/executor_option_tests.move new file mode 100644 index 00000000..d98fa970 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/worker_peripherals/fee_libs/executor_fee_lib_0/tests/types/executor_option_tests.move @@ -0,0 +1,156 @@ +#[test_only] +module executor_fee_lib_0::executor_option_tests { + use endpoint_v2_common::bytes32; + use endpoint_v2_common::serde::{Self, flatten}; + use executor_fee_lib_0::executor_option::{ + append_executor_options, extract_executor_options, new_executor_options, new_lz_compose_option, + new_lz_receive_option, new_native_drop_option, unpack_options, + }; + + #[test] + fun serializes_and_deserializes_to_the_same_input() { + let options = new_executor_options( + vector[ + new_lz_receive_option(333, 200), // serializes to a 32 length option + new_lz_receive_option(222, 0), // serializes to a 16 length option + ], + vector[ + new_native_drop_option(111, bytes32::from_address(@0x123456)), + new_native_drop_option(222, bytes32::from_address(@0x654321)), + ], + vector[ + new_lz_compose_option(1, 444, 1234), // serializes to a 34 length option + new_lz_compose_option(1, 555, 0), // serializes to a 18 length option + ], + true, + ); + + let serialized = serde::bytes_of(|buf| append_executor_options(buf, &options)); + + let deserialized = extract_executor_options(&serialized, &mut 0); + let ( + lz_receive_options, + native_drop_options, + lz_compose_options, + ordered_execution_option, + ) = unpack_options(deserialized); + + assert!(lz_receive_options == vector[ + new_lz_receive_option(333, 200), + new_lz_receive_option(222, 0), + ], 1); + + assert!(native_drop_options == vector[ + new_native_drop_option(111, bytes32::from_address(@0x123456)), + new_native_drop_option(222, bytes32::from_address(@0x654321)), + ], 2); + + assert!(lz_compose_options == vector[ + new_lz_compose_option(1, 444, 1234), + new_lz_compose_option(1, 555, 0), + ], 3); + + assert!(ordered_execution_option == true, 4); + + + // test having ordered_execution_option as false + let options = new_executor_options( + vector[], + vector[], + vector[], + false, + ); + + let serialized = serde::bytes_of(|buf| append_executor_options(buf, &options)); + let deserialized = extract_executor_options(&serialized, &mut 0); + let ( + _lz_receive_options, + _native_drop_options, + _lz_compose_options, + ordered_execution_option, + ) = unpack_options(deserialized); + + assert!(ordered_execution_option == false, 5); + } + + #[test] + #[expected_failure(abort_code = executor_fee_lib_0::executor_option::EUNSUPPORTED_OPTION)] + fun test_deserialize_executor_options_will_fail_if_provided_unsupported_option() { + let options = new_executor_options( + vector[ + new_lz_receive_option(333, 200), + new_lz_receive_option(222, 0), + ], + vector[ + new_native_drop_option(111, bytes32::from_address(@0x123456)), + new_native_drop_option(222, bytes32::from_address(@0x654321)), + ], + vector[ + new_lz_compose_option(1, 444, 1234), + new_lz_compose_option(1, 555, 0), + ], + true, + ); + let serialized = serde::bytes_of(|buf| append_executor_options(buf, &options)); + serialized = flatten(vector[ + serialized, + x"01", // worker_id = 1 + x"0001", // option_size = 1 + x"05", // option_type = 5 (invalid option type) + ]); + + extract_executor_options(&serialized, &mut 0); + } + + #[test] + #[expected_failure(abort_code = executor_fee_lib_0::executor_option::EINVALID_ORDERED_EXECUTION_OPTION_LENGTH)] + fun test_deserialize_executor_options_will_fail_if_provided_invalid_ordered_execution_option() { + let serialized = flatten(vector[ + x"04", // worker_id = 1 + x"0002", // option_size = 2 (should be 1) + x"04", // option_type = 4 (ordered execution option) + x"12", // option body + ]); + + extract_executor_options(&serialized, &mut 0); + } + + #[test] + #[expected_failure(abort_code = executor_fee_lib_0::executor_option::EINVALID_LZ_RECEIVE_OPTION_LENGTH)] + fun test_deserialize_executor_options_will_fail_if_provided_invalid_lz_receive_option() { + let serialized = flatten(vector[ + x"01", // worker_id = 1 + x"0015", // option_size = 21 (should be 17 or 33) + x"01", // option_type = 1 (lz receive option) + x"0101010101010101010101010101010101010101", // lz receive option + ]); + + extract_executor_options(&serialized, &mut 0); + } + + #[test] + #[expected_failure(abort_code = executor_fee_lib_0::executor_option::EINVALID_NATIVE_DROP_OPTION_LENGTH)] + fun test_deserialize_executor_options_will_fail_if_provided_invalid_native_drop_option() { + let serialized = flatten(vector[ + x"01", // worker_id = 1 + x"0011", // option_size = 17 (should be 49) + x"02", // option_type = 2 (native drop option) + x"01010101010101010101010101010101", // native drop option + ]); + + extract_executor_options(&serialized, &mut 0); + } + + #[test] + #[expected_failure(abort_code = executor_fee_lib_0::executor_option::EINVALID_LZ_COMPOSE_OPTION_LENGTH)] + fun test_deserialize_executor_options_will_fail_if_provided_invalid_lz_compose_option() { + let serialized = flatten(vector[ + x"01", // worker_id = 1 + x"0015", // option_size = 21 (should be 19 or 35) + x"03", // option_type = 3 (lz compose option) + x"0101010101010101010101010101010101010101", // lz compose option + ]); + + extract_executor_options(&serialized, &mut 0); + } +} diff --git a/packages/layerzero-v2/initia/contracts/worker_peripherals/price_feed_modules/price_feed_module_0/Move.toml b/packages/layerzero-v2/initia/contracts/worker_peripherals/price_feed_modules/price_feed_module_0/Move.toml new file mode 100644 index 00000000..6b9d355a --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/worker_peripherals/price_feed_modules/price_feed_module_0/Move.toml @@ -0,0 +1,26 @@ +[package] +name = "price_feed_module_0" +version = "1.0.0" +authors = [] + +[addresses] +endpoint_v2 = "_" +price_feed_module_0 = "_" +endpoint_v2_common = "_" +layerzero_admin = "_" +layerzero_treasury_admin = "_" + +[dev-addresses] +endpoint_v2 = "0x12345678" +price_feed_module_0 = "0x65DD71" +endpoint_v2_common = "0x9098" +layerzero_admin = "0x18943124" +layerzero_treasury_admin = "0x1894312499" + +[dependencies.InitiaStdlib] +git = "https://github.com/initia-labs/move-natives.git" +rev = "77d5f3e140143bdaa41f850115b3035c134193e3" +subdir = "initia_stdlib" + +[dependencies] +endpoint_v2_common = { local = "../../../endpoint_v2_common" } diff --git a/packages/layerzero-v2/initia/contracts/worker_peripherals/price_feed_modules/price_feed_module_0/sources/feeds.move b/packages/layerzero-v2/initia/contracts/worker_peripherals/price_feed_modules/price_feed_module_0/sources/feeds.move new file mode 100644 index 00000000..6217c8be --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/worker_peripherals/price_feed_modules/price_feed_module_0/sources/feeds.move @@ -0,0 +1,392 @@ +module price_feed_module_0::feeds { + use std::event::emit; + use std::signer::address_of; + use std::table::{Self, Table}; + use std::vector; + + use price_feed_module_0::eid_model_pair::{Self, ARBITRUM_MODEL_TYPE, DEFAULT_MODEL_TYPE, OPTIMISM_MODEL_TYPE}; + use price_feed_module_0::price::{Self, Price, split_eid_tagged_price}; + + #[test_only] + friend price_feed_module_0::feeds_tests; + + /// The data for a single price feed on this price feed module + struct Feed has key { + // The denominator for the price ratio remote price * price ratio / denominator = local price + denominator: u128, + // The compression percent for arbitrum (base 100) + arbitrum_compression_percent: u64, + // The model to use (which corresponds to chain type) for each destination EID + model_type: Table, + // The price ratio, gas price, and gas per byte for each destination EID + prices: Table, + // The base gas price for an Arbitrum L2 transaction + arbitrum_gas_per_l2_tx: u128, + // The gas price for an Arbitrum L1 calldata byte + arbitrum_gas_per_l1_calldata_byte: u128, + // The price, with `denominator` precision, of the native token in USD + native_token_price_usd: u128, + // The set of approved feed updaters (presence indicates approved, the value is always `true`) + updaters: Table, + } + + // =============================================== Only Feed Admins =============================================== + + /// Initializes a new feed under the signer's account address + public entry fun initialize(account: &signer) { + move_to(account, Feed { + denominator: 100_000_000_000_000_000_000, // 1e20 + arbitrum_compression_percent: 47, + model_type: table::new(), + prices: table::new(), + arbitrum_gas_per_l2_tx: 0, + arbitrum_gas_per_l1_calldata_byte: 0, + native_token_price_usd: 0, + updaters: table::new(), + }); + } + + /// Gives a feed updater permission to write to the signer's feed + public entry fun enable_feed_updater(account: &signer, updater: address) acquires Feed { + let feed_address = address_of(move account); + table::upsert(feed_updaters_mut(feed_address), updater, true); + emit(FeedUpdaterSet { feed_address, updater, enabled: true }); + } + + /// Revokes a feed updater's permission to write to the signer's feed + public entry fun disable_feed_updater(account: &signer, updater: address) acquires Feed { + let feed_address = address_of(move account); + table::remove(feed_updaters_mut(feed_address), updater); + emit(FeedUpdaterSet { feed_address, updater, enabled: false }); + } + + /// Sets the denominator for the feed + public entry fun set_denominator(account: &signer, denominator: u128) acquires Feed { + assert!(denominator > 0, EINVALID_DENOMNIATOR); + let feed_address = address_of(move account); + feed_data_mut(feed_address).denominator = denominator; + } + + /// Sets the arbitrum compression percent (base 100) for the feed + public entry fun set_arbitrum_compression_percent(account: &signer, percent: u64) acquires Feed { + let feed_address = address_of(move account); + feed_data_mut(feed_address).arbitrum_compression_percent = percent; + } + + /// Sets the model type for multiple given destination EIDs + /// The params are a serialized list of EidModelPair + public entry fun set_eid_models(account: &signer, params: vector) acquires Feed { + let feed_address = address_of(move account); + + let eid_to_model_list = eid_model_pair::deserialize_eid_model_pair_list(¶ms); + let model_type_mut = &mut feed_data_mut(feed_address).model_type; + for (i in 0..vector::length(&eid_to_model_list)) { + let eid_model = vector::borrow(&eid_to_model_list, i); + let dst_eid = eid_model_pair::get_dst_eid(eid_model); + let model_type = eid_model_pair::get_model_type(eid_model); + assert!(eid_model_pair::is_valid_model_type(model_type), EINVALID_MODEL_TYPE); + table::upsert(model_type_mut, dst_eid, model_type); + } + } + + #[view] + /// Gets the model type for a given destination EID + /// @dev the model type can be default (0), arbitrum (1), or optimism (2) + public fun get_model_type(feed_address: address, dst_eid: u32): u16 acquires Feed { + let feed = feed_data(feed_address); + *table::borrow_with_default(&feed.model_type, dst_eid, &DEFAULT_MODEL_TYPE()) + } + + // ============================================== Only Feed Updaters ============================================== + + /// Asserts that a feed updater is approved to write to a specific feed + fun assert_valid_fee_updater(updater: address, feed: address) acquires Feed { + assert!(is_price_updater(updater, feed), EUNAUTHORIZED_UPDATER); + } + + /// Sets the price (serialized EidTagged) for a given destination EID + public entry fun set_price(updater: &signer, feed: address, prices: vector) acquires Feed { + let updater = address_of(move updater); + assert_valid_fee_updater(updater, feed); + + let price_list = price::deserialize_eid_tagged_price_list(&prices); + let prices_mut = &mut feed_data_mut(feed).prices; + for (i in 0..vector::length(&price_list)) { + let eid_tagged_price = vector::borrow(&price_list, i); + let (eid, price) = split_eid_tagged_price(eid_tagged_price); + table::upsert(prices_mut, eid, price); + } + } + + /// Sets the arbitrum traits for the feed + public entry fun set_arbitrum_traits( + updater: &signer, + feed_address: address, + gas_per_l2_tx: u128, + gas_per_l1_calldata_byte: u128, + ) acquires Feed { + let updater = address_of(move updater); + assert_valid_fee_updater(updater, feed_address); + + let feed_data_mut = feed_data_mut(feed_address); + feed_data_mut.arbitrum_gas_per_l2_tx = gas_per_l2_tx; + feed_data_mut.arbitrum_gas_per_l1_calldata_byte = gas_per_l1_calldata_byte; + } + + /// Sets the native token price in USD for the feed denominated in `denominator` precision + public entry fun set_native_token_price_usd( + updater: &signer, + feed_address: address, + native_token_price_usd: u128, + ) acquires Feed { + let updater = address_of(move updater); + assert_valid_fee_updater(updater, feed_address); + + feed_data_mut(feed_address).native_token_price_usd = native_token_price_usd; + } + + // ===================================================== View ===================================================== + + #[view] + /// Gets the denominator used for price ratios for the feed + public fun get_price_ratio_denominator(feed_address: address): u128 acquires Feed { + feed_data(feed_address).denominator + } + + #[view] + /// Checks if a feed updater has the permission to write to a feed + public fun is_price_updater(updater: address, feed: address): bool acquires Feed { + table::contains(feed_updaters(feed), updater) + } + + #[view] + /// Gets the native token price in USD for the feed (denominated in `denominator` precision) + public fun get_native_token_price_usd(feed_address: address): u128 acquires Feed { + feed_data(feed_address).native_token_price_usd + } + + #[view] + /// Gets the arbitrum compression percent (base 100) for the feed + public fun get_arbitrum_compression_percent(feed_address: address): u64 acquires Feed { + feed_data(feed_address).arbitrum_compression_percent + } + + #[view] + /// Gets the arbitrum traits for the feed + /// @return (gas per L2 transaction, gas per L1 calldata byte) + public fun get_arbitrum_price_traits(feed_address: address): (u128, u128) acquires Feed { + let feed = feed_data(feed_address); + (feed.arbitrum_gas_per_l2_tx, feed.arbitrum_gas_per_l1_calldata_byte) + } + + #[view] + /// Gets the price data for a given destination EID + /// @return (price ratio, gas price in unit, gas price per byte) + public fun get_price(feed_address: address, _dst_eid: u32): (u128, u64, u32) acquires Feed { + let prices = &feed_data(feed_address).prices; + + assert!(table::contains(prices, _dst_eid), EEID_DOES_NOT_EXIST); + let eid_price = table::borrow(prices, _dst_eid); + (price::get_price_ratio(eid_price), price::get_gas_price_in_unit(eid_price), price::get_gas_per_byte(eid_price)) + } + + // ================================================ Fee Estimation ================================================ + + #[view] + /// Estimates the fee for a send transaction, considering the eid, gas, and call data size + /// This selects the appropriate model and prices inputs based on the destination EID + /// @return (fee, price ratio, denominator, native token price in USD) + public fun estimate_fee_on_send( + feed_address: address, + dst_eid: u32, + call_data_size: u64, + gas: u128, + ): (u128, u128, u128, u128) acquires Feed { + // v2 EIDs are the v1 EIDs + 30,000 + // We anticipate that each subsequent eid will be 30,000 more than the prior (but on the same chain) + let dst_eid_mod = dst_eid % 30_000; + + let feed = feed_data(feed_address); + let denominator = feed.denominator; + let native_token_price_usd = feed.native_token_price_usd; + + let type = table::borrow_with_default(&feed.model_type, dst_eid_mod, &DEFAULT_MODEL_TYPE()); + assert!(table::contains(&feed.prices, dst_eid_mod), EPRICE_FEED_NOT_CONFIGURED_FOR_EID); + let dst_pricing = table::borrow(&feed.prices, dst_eid_mod); + + let fee = if (dst_eid_mod == 110 || dst_eid_mod == 10143 || dst_eid_mod == 20143 || type == &ARBITRUM_MODEL_TYPE( + )) { + // Arbitrum Type + estimate_fee_with_arbitrum_model( + call_data_size, + gas, + dst_pricing, + feed.denominator, + feed.arbitrum_compression_percent, + feed.arbitrum_gas_per_l1_calldata_byte, + feed.arbitrum_gas_per_l2_tx, + ) + } else if (dst_eid_mod == 111 || dst_eid_mod == 10132 || dst_eid_mod == 20132 || type == &OPTIMISM_MODEL_TYPE( + )) { + // Optimism Type + let ethereum_id = get_l1_lookup_id_for_optimism_model(dst_eid_mod); + assert!(table::contains(&feed.prices, ethereum_id), EPRICE_FEED_NOT_CONFIGURED_FOR_EID_ETH_L1); + let ethereum_pricing = table::borrow(&feed.prices, ethereum_id); + estimate_fee_with_optimism_model( + call_data_size, + gas, + ethereum_pricing, + dst_pricing, + feed.denominator, + ) + } else { + // Default + estimate_fee_with_default_model(call_data_size, gas, dst_pricing, feed.denominator) + }; + + let price_ratio = price::get_price_ratio(dst_pricing); + (fee, price_ratio, denominator, native_token_price_usd) + } + + /// Estimates the fee for a send transaction using the default model + public(friend) fun estimate_fee_with_default_model( + call_data_size: u64, + gas: u128, + dst_pricing: &Price, + denominator: u128, + ): u128 { + let gas_per_byte = price::get_gas_per_byte_u128(dst_pricing); + let gas_price_in_unit = price::get_gas_price_in_unit_u128(dst_pricing); + let gas_for_call_data = (call_data_size as u128) * gas_per_byte; + let remote_fee = (gas_for_call_data + gas) * gas_price_in_unit; + + let fee = (remote_fee * price::get_price_ratio(dst_pricing)) / denominator; + fee + } + + /// Estimates the fee for a send transaction using the arbitrum model + public(friend) fun estimate_fee_with_arbitrum_model( + call_data_size: u64, + gas: u128, + dst_pricing: &Price, + denominator: u128, + arbitrum_compression_percent: u64, + arbitrum_gas_per_l1_call_data_byte: u128, + arbitrum_gas_per_l2_tx: u128, + ): u128 { + let arbitrum_gas_per_byte = price::get_gas_per_byte_u128(dst_pricing); + let gas_price_in_unit = price::get_gas_price_in_unit_u128(dst_pricing); + let price_ratio = price::get_price_ratio(dst_pricing); + + let gas_for_l1_call_data = ((call_data_size as u128) * (arbitrum_compression_percent as u128) / 100) + * arbitrum_gas_per_l1_call_data_byte; + let gas_for_l2_call_data = (call_data_size as u128) * arbitrum_gas_per_byte; + let gas_fee = (gas + + arbitrum_gas_per_l2_tx + + gas_for_l1_call_data + gas_for_l2_call_data) + * gas_price_in_unit; + + gas_fee * price_ratio / denominator + } + + /// Estimates the fee for a send transaction using the optimism model + public(friend) fun estimate_fee_with_optimism_model( + call_data_size: u64, + gas: u128, + ethereum_pricing: &Price, + optimism_pricing: &Price, + denominator: u128, + ): u128 { + // L1 Fee + let gas_per_byte_eth = price::get_gas_per_byte_u128(ethereum_pricing); + let gas_price_in_unit_eth = price::get_gas_price_in_unit_u128(ethereum_pricing); + let gas_for_l1_call_data = (call_data_size as u128) * gas_per_byte_eth + 3188; + let l1_fee = gas_for_l1_call_data * gas_price_in_unit_eth; + + // L2 Fee + let gas_per_byte_opt = price::get_gas_per_byte_u128(optimism_pricing); + let gas_price_in_unit_opt = price::get_gas_price_in_unit_u128(optimism_pricing); + let gas_for_l2_call_data = (call_data_size as u128) * gas_per_byte_opt; + let l2_fee = (gas_for_l2_call_data + gas) * gas_price_in_unit_opt; + + let gas_price_ratio_eth = price::get_price_ratio(ethereum_pricing); + let gas_price_ratio_opt = price::get_price_ratio(optimism_pricing); + let l1_fee_in_src_price = (l1_fee * gas_price_ratio_eth) / denominator; + let l2_fee_in_src_price = (l2_fee * gas_price_ratio_opt) / denominator; + + l1_fee_in_src_price + l2_fee_in_src_price + } + + /// Gets the L1 lookup ID for the optimism model + /// This is a hardcoded lookup for the L1 chain for the optimism model and it differs based on network + public(friend) fun get_l1_lookup_id_for_optimism_model(l2_eid: u32): u32 { + if (l2_eid < 10_000) { + 101 + } else if (l2_eid < 20_000) { + if (l2_eid == 10132) { + 10121 // ethereum-goerli + } else { + 10161 // ethereum-sepolia + } + } else { + 20121 // ethereum-goerli + } + } + + // ==================================================== Helpers =================================================== + + /// Asserts that a feed exists + inline fun assert_feed_exists(feed: address) { assert!(exists(feed), EFEED_DOES_NOT_EXIST); } + + /// Borrow the feed data for a single feed + inline fun feed_data(feed: address): &Feed { + assert_feed_exists(feed); + borrow_global(feed) + } + + /// Borrow the feed data for a single feed mutably + inline fun feed_data_mut(feed: address): &mut Feed { + assert_feed_exists(feed); + borrow_global_mut(feed) + } + + /// Borrow the updaters for a single feed + inline fun feed_updaters(feed: address): &Table { + assert_feed_exists(feed); + &feed_data(feed).updaters + } + + /// Borrow the updaters for a single feed mutably + inline fun feed_updaters_mut(feed: address): &mut Table { + assert_feed_exists(feed); + &mut feed_data_mut(feed).updaters + } + + // ==================================================== Events ==================================================== + + #[event] + struct FeedUpdaterSet has drop, store { + feed_address: address, + updater: address, + enabled: bool, + } + + #[test_only] + public fun feed_updater_set_event(feed_address: address, updater: address, enabled: bool): FeedUpdaterSet { + FeedUpdaterSet { + feed_address, + updater, + enabled, + } + } + + // ================================================== Error Codes ================================================= + + const EEID_DOES_NOT_EXIST: u64 = 1; + const EFEED_DOES_NOT_EXIST: u64 = 2; + const EINVALID_DENOMNIATOR: u64 = 3; + const EINVALID_MODEL_TYPE: u64 = 4; + const EPRICE_FEED_NOT_CONFIGURED_FOR_EID: u64 = 5; + const EPRICE_FEED_NOT_CONFIGURED_FOR_EID_ETH_L1: u64 = 6; + const EUNAUTHORIZED_UPDATER: u64 = 7; +} \ No newline at end of file diff --git a/packages/layerzero-v2/initia/contracts/worker_peripherals/price_feed_modules/price_feed_module_0/sources/types/eid_model_pair.move b/packages/layerzero-v2/initia/contracts/worker_peripherals/price_feed_modules/price_feed_module_0/sources/types/eid_model_pair.move new file mode 100644 index 00000000..8da951fe --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/worker_peripherals/price_feed_modules/price_feed_module_0/sources/types/eid_model_pair.move @@ -0,0 +1,77 @@ +module price_feed_module_0::eid_model_pair { + use std::vector; + + use endpoint_v2_common::serde::{append_u16, append_u32, extract_u16, extract_u32}; + + public inline fun DEFAULT_MODEL_TYPE(): u16 { 0 } + + public inline fun ARBITRUM_MODEL_TYPE(): u16 { 1 } + + public inline fun OPTIMISM_MODEL_TYPE(): u16 { 2 } + + /// A pair containing the destination EID and pricing model type (default, arbitrum, or optimism) + struct EidModelPair has store, copy, drop { + dst_eid: u32, + model_type: u16, + } + + // Constructor for EidModelPair + public fun new_eid_model_pair(dst_eid: u32, model_type: u16): EidModelPair { + EidModelPair { + dst_eid, + model_type, + } + } + + /// Get the destination EID from the EidModelPair + public fun get_dst_eid(pair: &EidModelPair): u32 { pair.dst_eid } + + /// Get the model type from the EidModelPair + public fun get_model_type(pair: &EidModelPair): u16 { pair.model_type } + + /// Check if the model type is valid + /// @dev The model type must be one of the following: default 0, arbitrum 1, or optimism 2 + public fun is_valid_model_type(model_type: u16): bool { + model_type == DEFAULT_MODEL_TYPE() || + model_type == ARBITRUM_MODEL_TYPE() || + model_type == OPTIMISM_MODEL_TYPE() + } + + /// Serialize EidModelPair to the end of a byte buffer + public fun append_eid_model_pair(buf: &mut vector, obj: &EidModelPair) { + append_u32(buf, obj.dst_eid); + append_u16(buf, obj.model_type); + } + + /// Serialize a list of EidModelPair + /// This is a series of EidModelPairs serialized one after the other + public fun serialize_eid_model_pair_list(objs: &vector): vector { + let buf = vector[]; + for (i in 0..vector::length(objs)) { + append_eid_model_pair(&mut buf, vector::borrow(objs, i)); + }; + buf + } + + /// Deserialize EidModelPair from a byte buffer at a given position + /// The position to be updated to the next position after the deserialized EidModelPair + public fun extract_eid_model_pair(buf: &vector, position: &mut u64): EidModelPair { + let dst_eid = extract_u32(buf, position); + let model_type = extract_u16(buf, position); + EidModelPair { + dst_eid, + model_type, + } + } + + /// Deserialize a list of EidModelPair + /// This accepts a series of EidModelPairs serialized one after the other + public fun deserialize_eid_model_pair_list(buf: &vector): vector { + let result = vector[]; + let position = 0; + while (position < vector::length(buf)) { + vector::push_back(&mut result, extract_eid_model_pair(buf, &mut position)); + }; + result + } +} diff --git a/packages/layerzero-v2/initia/contracts/worker_peripherals/price_feed_modules/price_feed_module_0/sources/types/price.move b/packages/layerzero-v2/initia/contracts/worker_peripherals/price_feed_modules/price_feed_module_0/sources/types/price.move new file mode 100644 index 00000000..83f5ecb1 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/worker_peripherals/price_feed_modules/price_feed_module_0/sources/types/price.move @@ -0,0 +1,108 @@ +module price_feed_module_0::price { + use std::vector; + + use endpoint_v2_common::serde; + + /// This struct carries the EID specific price and gas information required to calculate gas for a given chain and + /// convert gas, value, and fees to the current chain's native token. + /// The price_ratio is relative to a DENOMINATOR that is defined in the price feed module + struct Price has copy, drop, store { + price_ratio: u128, + gas_price_in_unit: u64, + gas_per_byte: u32, + } + + struct EidTaggedPrice has copy, drop, store { + eid: u32, + price: Price, + } + + // Creates a new price struct + public fun new_price(price_ratio: u128, gas_price_in_unit: u64, gas_per_byte: u32): Price { + Price { + price_ratio, + gas_price_in_unit, + gas_per_byte, + } + } + + public fun tag_price_with_eid(eid: u32, price: Price): EidTaggedPrice { + EidTaggedPrice { eid, price } + } + + public fun split_eid_tagged_price(eid_price: &EidTaggedPrice): (u32, Price) { + (eid_price.eid, eid_price.price) + } + + // Gets the price ratio + public fun get_price_ratio(price: &Price): u128 { price.price_ratio } + + // Gets the gas price in unit + public fun get_gas_price_in_unit(price: &Price): u64 { price.gas_price_in_unit } + + // Gets the gas price in unit as a u128 (for use in arithmetic) + public fun get_gas_price_in_unit_u128(price: &Price): u128 { (price.gas_price_in_unit as u128) } + + // Gets the gas price per byte in the native token + public fun get_gas_per_byte(price: &Price): u32 { price.gas_per_byte } + + /// Gets the gas price per byte in the native token as a u128 (for use in arithmetic) + public fun get_gas_per_byte_u128(price: &Price): u128 { (price.gas_per_byte as u128) } + + // Append Price to the end of a byte buffer + public fun append_price(buf: &mut vector, price: &Price) { + serde::append_u128(buf, price.price_ratio); + serde::append_u64(buf, price.gas_price_in_unit); + serde::append_u32(buf, price.gas_per_byte); + } + + /// Append an Eid-tagged Price to the end of a byte buffer + public fun append_eid_tagged_price(buf: &mut vector, eid_price: &EidTaggedPrice) { + serde::append_u32(buf, eid_price.eid); + append_price(buf, &eid_price.price); + } + + /// Serialize a list of Eid-tagged Prices into a byte vector + /// This will be a series of Eid-tagged Prices serialized one after the other + public fun serialize_eid_tagged_price_list(eid_prices: &vector): vector { + let buf = vector[]; + for (i in 0..vector::length(eid_prices)) { + append_eid_tagged_price(&mut buf, vector::borrow(eid_prices, i)); + }; + buf + } + + /// Extract a Price from a byte buffer at a given position + /// The position to be updated to the next position after the deserialized Price + public fun extract_price(buf: &vector, position: &mut u64): Price { + let price_ratio = serde::extract_u128(buf, position); + let gas_price_in_unit = serde::extract_u64(buf, position); + let gas_per_byte = serde::extract_u32(buf, position); + Price { + price_ratio, + gas_price_in_unit, + gas_per_byte, + } + } + + /// Extract an Eid-tagged Price from a byte buffer at a given position + /// The position to be updated to the next position after the deserialized Eid-tagged Price + public fun extract_eid_tagged_price(buf: &vector, position: &mut u64): EidTaggedPrice { + let eid = serde::extract_u32(buf, position); + EidTaggedPrice { + eid, + price: extract_price(buf, position), + } + } + + /// Deserialize a list of Eid-tagged Prices from a byte buffer + /// This will extract a series of one-after-another Eid-tagged Prices from the buffer + public fun deserialize_eid_tagged_price_list(buf: &vector): vector { + let result = vector[]; + let position = 0; + while (position < vector::length(buf)) { + vector::push_back(&mut result, extract_eid_tagged_price(buf, &mut position)); + }; + result + } +} diff --git a/packages/layerzero-v2/initia/contracts/worker_peripherals/price_feed_modules/price_feed_module_0/tests/feeds_tests.move b/packages/layerzero-v2/initia/contracts/worker_peripherals/price_feed_modules/price_feed_module_0/tests/feeds_tests.move new file mode 100644 index 00000000..0b08151d --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/worker_peripherals/price_feed_modules/price_feed_module_0/tests/feeds_tests.move @@ -0,0 +1,301 @@ +#[test_only] +module price_feed_module_0::feeds_tests { + use std::event::was_event_emitted; + use std::signer::address_of; + + use price_feed_module_0::eid_model_pair; + use price_feed_module_0::eid_model_pair::{ + ARBITRUM_MODEL_TYPE, DEFAULT_MODEL_TYPE, EidModelPair, new_eid_model_pair, OPTIMISM_MODEL_TYPE, + }; + use price_feed_module_0::feeds::{ + disable_feed_updater, enable_feed_updater, estimate_fee_on_send, estimate_fee_with_arbitrum_model, + estimate_fee_with_default_model, estimate_fee_with_optimism_model, feed_updater_set_event, + get_arbitrum_compression_percent, get_arbitrum_price_traits, get_l1_lookup_id_for_optimism_model, + get_model_type, get_native_token_price_usd, get_price, get_price_ratio_denominator, initialize, + is_price_updater, set_arbitrum_compression_percent, set_arbitrum_traits, set_denominator, set_eid_models, + set_native_token_price_usd, set_price, + }; + use price_feed_module_0::price::{Self, EidTaggedPrice, tag_price_with_eid}; + + #[test(feed = @1111)] + fun test_enable_disable_feed_updater(feed: &signer) { + initialize(feed); + + let feed_address = address_of(feed); + assert!(!is_price_updater(@3333, feed_address), 1); + assert!(!is_price_updater(@4444, feed_address), 2); + + enable_feed_updater(feed, @3333); + assert!(was_event_emitted(&feed_updater_set_event(@1111, @3333, true)), 0); + assert!(is_price_updater(@3333, feed_address), 1); + assert!(!is_price_updater(@4444, feed_address), 2); + + enable_feed_updater(feed, @4444); + assert!(was_event_emitted(&feed_updater_set_event(@1111, @4444, true)), 0); + assert!(is_price_updater(@3333, feed_address), 1); + assert!(is_price_updater(@4444, feed_address), 2); + + disable_feed_updater(feed, @3333); + assert!(was_event_emitted(&feed_updater_set_event(@1111, @3333, false)), 0); + assert!(!is_price_updater(@3333, feed_address), 1); + assert!(is_price_updater(@4444, feed_address), 2); + + disable_feed_updater(feed, @4444); + assert!(was_event_emitted(&feed_updater_set_event(@1111, @4444, false)), 0); + assert!(!is_price_updater(@3333, feed_address), 1); + assert!(!is_price_updater(@4444, feed_address), 2); + } + + #[test(feed = @1111)] + fun test_set_denominator(feed: &signer) { + initialize(feed); + let feed_address = address_of(feed); + assert!(get_price_ratio_denominator(feed_address) == 100_000_000_000_000_000_000, 1); // default + set_denominator(feed, 1_0000_0000); + assert!(get_price_ratio_denominator(feed_address) == 1_0000_0000, 2); + } + + #[test(feed = @1111)] + fun test_set_arbitrum_compression_percent(feed: &signer) { + initialize(feed); + let feed_address = address_of(feed); + + // check default value + assert!(get_arbitrum_compression_percent(feed_address) == 47, 1); + + // set value and check + set_arbitrum_compression_percent(feed, 50); + assert!(get_arbitrum_compression_percent(feed_address) == 50, 2); + } + + #[test(feed = @1111)] + fun test_set_eid_to_model_type(feed: &signer) { + initialize(feed); + let feed_address = address_of(feed); + + let list = vector[ + new_eid_model_pair(101, DEFAULT_MODEL_TYPE()), + new_eid_model_pair(102, OPTIMISM_MODEL_TYPE()), + new_eid_model_pair(103, ARBITRUM_MODEL_TYPE()), + ]; + + let params = eid_model_pair::serialize_eid_model_pair_list(&list); + set_eid_models(feed, params); + + assert!(get_model_type(feed_address, 101) == DEFAULT_MODEL_TYPE(), 1); + assert!(get_model_type(feed_address, 102) == OPTIMISM_MODEL_TYPE(), 2); + assert!(get_model_type(feed_address, 103) == ARBITRUM_MODEL_TYPE(), 3); + } + + #[test(feed = @1111, updater = @9999, feed_2 = @2222)] + fun test_set_price(feed: &signer, updater: &signer, feed_2: &signer) { + initialize(feed); + let feed_address = address_of(feed); + enable_feed_updater(feed, @9999); + + // unrelated feed that with the same updater enabled + initialize(feed_2); + let feed_address_2 = address_of(feed_2); + enable_feed_updater(feed_2, @9999); + + // Serialize and set prices + let list = vector[ + tag_price_with_eid(101, price::new_price(1, 2, 3)), + tag_price_with_eid(102, price::new_price(4, 5, 6)), + tag_price_with_eid(103, price::new_price(7, 8, 9)), + ]; + let prices = price::serialize_eid_tagged_price_list(&list); + set_price(updater, feed_address, prices); + + // Set price on another feed to make sure there isn't interference + let list = vector[ + tag_price_with_eid(102, price::new_price(400, 500, 600)), + ]; + let prices = price::serialize_eid_tagged_price_list(&list); + set_price(updater, feed_address_2, prices); + + // Feed should be updated + let (price_ratio, _gas_price_in_unit, _gas_per_byte) = get_price(feed_address, 101); + assert!(price_ratio == 1, 1); + let (_price_ratio, gas_price_in_unit, _gas_per_byte) = get_price(feed_address, 102); + assert!(gas_price_in_unit == 5, 2); + let (_price_ratio, _gas_price_in_unit, gas_per_byte) = get_price(feed_address, 103); + assert!(gas_per_byte == 9, 3); + } + + #[test(feed = @1111, updater = @9999)] + fun test_set_arbitrum_traits(feed: &signer, updater: &signer) { + initialize(feed); + let feed_address = address_of(feed); + enable_feed_updater(feed, @9999); + + set_arbitrum_traits(updater, feed_address, 100, 200); + + let (gas_per_l2_tx, gas_per_l1_calldata_byte) = get_arbitrum_price_traits(feed_address); + assert!(gas_per_l2_tx == 100, 1); + assert!(gas_per_l1_calldata_byte == 200, 2); + } + + #[test(feed = @1111, updater = @9999)] + fun test_set_native_token_price_usd(feed: &signer, updater: &signer) { + initialize(feed); + let feed_address = address_of(feed); + enable_feed_updater(feed, @9999); + + set_native_token_price_usd(updater, feed_address, 100); + assert!(get_native_token_price_usd(feed_address) == 100, 1); + } + + #[test(feed = @1111, updater = @9999)] + fun test_estimate_fee_on_send(feed: &signer, updater: &signer) { + initialize(feed); + enable_feed_updater(feed, @9999); + + // These prices are the same as used in the individual model tests + // We are testing whether we get the expected model response + // Using different price ratios for goerli, sepolia to see that arbitrum calcs are using the correct L2 price + let eth_price = price::new_price(4000, 51, 33); + let eth_goerli_price = price::new_price(40000, 51, 33); + let eth_sepolia_price = price::new_price(400000, 51, 33); + let arb_price = price::new_price(1222, 12, 3); + let opt_price = price::new_price(200, 43, 5); + + set_denominator(feed, 100); + set_arbitrum_compression_percent(feed, 47); + set_arbitrum_traits(updater, @1111, 5432, 11); + set_native_token_price_usd(updater, @1111, 6); + + // Test some non-hardcoded model types + let eid_model_pairs = vector[ + new_eid_model_pair( + 110, + DEFAULT_MODEL_TYPE() + ), // cannot override hardcoded type - this will still be "ARBITRUM" + new_eid_model_pair(11000, OPTIMISM_MODEL_TYPE()), // optimism using L1 sepolia + new_eid_model_pair(25555, ARBITRUM_MODEL_TYPE()), + new_eid_model_pair(26666, OPTIMISM_MODEL_TYPE()), + ]; + + let pairs_serialized = eid_model_pair::serialize_eid_model_pair_list(&eid_model_pairs); + set_eid_models(feed, pairs_serialized); + + let list = vector[ + tag_price_with_eid(101, eth_price), // First 6 EIDs are all of hardcoded types + tag_price_with_eid(110, arb_price), + tag_price_with_eid(111, opt_price), + tag_price_with_eid(10101, eth_price), + tag_price_with_eid(10143, arb_price), + tag_price_with_eid(10132, opt_price), + tag_price_with_eid(11000, opt_price), // optimism using L1 sepolia + tag_price_with_eid(10121, eth_goerli_price), // eth-goerli - used for arbitrum estimate + tag_price_with_eid(10161, eth_sepolia_price), // eth-sepolia - used for arbitrum estimate + + tag_price_with_eid(24444, eth_price), // not hardcoded and not set - should default to "DEFAULT" + tag_price_with_eid(25555, arb_price), // configured to "ARBITRUM" + tag_price_with_eid(26666, opt_price), // configured to "OPTIMISM" + tag_price_with_eid(20121, eth_goerli_price), // eth-goerli - used for arbitrum estimate + ]; + let prices_serialized = price::serialize_eid_tagged_price_list(&list); + set_price(updater, @1111, prices_serialized); + + // Variety of tests to make sure that the correct model is used and that the parameters are correctly provided + // to each model. Testing for different networks (10000 intervals) and also for different versions (30000 + // intervals). + // For the arbitrum calculations, we need to make sure that it is able to pull from the right l1 chain + // Also, testing if the % 30000 is working by adding multiples of 30000 to EIDs + + // Default (101 + 30000) + let (fee, price_ratio, denominator, native_token_price_usd) = estimate_fee_on_send(@1111, 30101, 50, 100); + assert!(fee == 3570000, 1); + assert!(price_ratio == 4000, 2); + assert!(denominator == 100, 3); + assert!(native_token_price_usd == 6, 4); + + // Default (10101) + let (fee, _pr, _d, _ntp) = estimate_fee_on_send(@1111, 10101, 50, 100); + assert!(fee == 3570000, 5); + + // Default (24444 + 60000) + let (fee, _pr, _d, _ntp) = estimate_fee_on_send(@1111, 84444, 50, 100); + assert!(fee == 3570000, 6); + + // Arbitrum (110 + 60000) + let (fee, _pr, _d, _ntp) = estimate_fee_on_send(@1111, 60110, 50, 232); + assert!(fee == 889664, 7); + + // Arbitrum (10143) + let (fee, _pr, _d, _ntp) = estimate_fee_on_send(@1111, 10143, 50, 232); + assert!(fee == 889664, 8); + + // Arbitrum (25555) + let (fee, _pr, _d, _ntp) = estimate_fee_on_send(@1111, 25555, 50, 232); + assert!(fee == 889664, 9); + + // Optimism (111 + 90000) + let (fee, _pr, _d, _ntp) = estimate_fee_on_send(@1111, 90111, 2100, 232); + assert!(fee == 148798472, 10); + + // Optimism (10132 + 30000) + let (fee, _pr, _d, _ntp) = estimate_fee_on_send(@1111, 40132, 2100, 232); + assert!(fee == 1479678152, 11); // goreli 10x + + // Optimism (11000 + 30000) + let (fee, _pr, _d, _ntp) = estimate_fee_on_send(@1111, 41000, 2100, 232); + assert!(fee == 14788474952, 11); // sepolia 100x + + // Optimism (26666) + let (fee, _pr, _d, _ntp) = estimate_fee_on_send(@1111, 26666, 2100, 232); + assert!(fee == 1479678152, 12); // goerli 10x + } + + #[test] + fun test_estimate_fee_with_default_model() { + let price = price::new_price(100000000000000000000, 1000000000, 16); + let fee = estimate_fee_with_default_model(1000, 500, &price, 100000000000000000000); + // gas: ((call data: 1000) * 16) + (gas: 500) = 16500 + // fee: (gas: 16500) * (gas price: 1000000000) * (price ratio: 100000000000000000000) / (denom: 100000000000000000000) = 16500000000000 + assert!(fee == 16500000000000, 0); + } + + #[test] + fun test_estimate_fee_with_arbitrum_model() { + let price = price::new_price(100000000000000000000, 10000000, 16); + let fee = estimate_fee_with_arbitrum_model( + 1000, + 500, + &price, + 100000000000000000000, + 47, + 29, + 4176, + ); + assert!(fee == 343060000000, 0); + // compressed calldata size = floor((calldata_size: 1000) * (47: compression_percent) / 100) = 470 + // l1 calldata gas = (compressed_size: 470) * (arb_gas_per_l1_calldata_byte: 29) = 13630 + // l2 calldata gas = (calldata_size: 1000) * (arb_gas_per_byte: 16) = 16000 + // total gas = (gas: 500) + (arb_gas_per_l2_tx: 4176) + (l1: 13630) + (l2: 16000) = 34306 + // total fee = (total gas: 34306) * (gas_price: 10000000) * (price_ratio: 100000000000000000000) / (denominator: 100000000000000000000) = 343060000000 + } + + #[test] + fun test_estimate_fee_with_optimism_model() { + let ethereum_price = price::new_price(100000000000000000000, 646718991, 8); + let optimism_price = price::new_price(100000000000000000000, 2231118, 16); + let fee = estimate_fee_with_optimism_model( + 1000, + 500, + ðereum_price, + &optimism_price, + 100000000000000000000, + ); + assert!(fee == 7272305518308, 0); + } + + #[test] + fun test_get_l1_lookup_id_for_optimism_model() { + assert!(get_l1_lookup_id_for_optimism_model(111) == 101, 1); // eth + assert!(get_l1_lookup_id_for_optimism_model(10132) == 10121, 2); // eth-goerli + assert!(get_l1_lookup_id_for_optimism_model(10500) == 10161, 2); // eth-sepolia + assert!(get_l1_lookup_id_for_optimism_model(20132) == 20121, 3); // eth-goerli + } +} diff --git a/packages/layerzero-v2/initia/contracts/worker_peripherals/price_feed_modules/price_feed_module_0/tests/types/eid_model_pair_tests.move b/packages/layerzero-v2/initia/contracts/worker_peripherals/price_feed_modules/price_feed_module_0/tests/types/eid_model_pair_tests.move new file mode 100644 index 00000000..438d7a31 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/worker_peripherals/price_feed_modules/price_feed_module_0/tests/types/eid_model_pair_tests.move @@ -0,0 +1,38 @@ +#[test_only] +module price_feed_module_0::eid_model_pair_tests { + use std::vector; + + use price_feed_module_0::eid_model_pair::{ + append_eid_model_pair, deserialize_eid_model_pair_list, EidModelPair, extract_eid_model_pair, + get_dst_eid, get_model_type, new_eid_model_pair, serialize_eid_model_pair_list, + }; + + #[test] + fun test_eid_to_model() { + let obj = new_eid_model_pair(123, 456); + let buf = vector[]; + append_eid_model_pair(&mut buf, &obj); + let pos = 0; + let obj2 = extract_eid_model_pair(&buf, &mut pos); + + // test getters + assert!(get_dst_eid(&obj) == get_dst_eid(&obj2), 0); + assert!(get_model_type(&obj) == get_model_type(&obj2), 1); + + // test object + assert!(obj == obj2, 3); + } + + #[test] + fun test_eid_to_model_list() { + let objs = vector[]; + vector::push_back(&mut objs, new_eid_model_pair(123, 456)); + vector::push_back(&mut objs, new_eid_model_pair(789, 1)); + + let buf = serialize_eid_model_pair_list(&objs); + let objs2 = deserialize_eid_model_pair_list(&buf); + + // test object + assert!(objs == objs2, 1); + } +} diff --git a/packages/layerzero-v2/initia/contracts/worker_peripherals/price_feed_modules/price_feed_module_0/tests/types/price_tests.move b/packages/layerzero-v2/initia/contracts/worker_peripherals/price_feed_modules/price_feed_module_0/tests/types/price_tests.move new file mode 100644 index 00000000..fa8510aa --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/worker_peripherals/price_feed_modules/price_feed_module_0/tests/types/price_tests.move @@ -0,0 +1,40 @@ +#[test_only] +module price_feed_module_0::price_tests { + use std::vector; + + use price_feed_module_0::price::{ + append_eid_tagged_price, + deserialize_eid_tagged_price_list, EidTaggedPrice, extract_eid_tagged_price, new_price, + serialize_eid_tagged_price_list, tag_price_with_eid, + }; + + #[test] + fun test_append_extract_eid_tagged_price() { + let obj = tag_price_with_eid( + 123, + new_price(456, 789, 101112), + ); + let buf = vector[]; + append_eid_tagged_price(&mut buf, &obj); + let pos = 0; + let obj2 = extract_eid_tagged_price(&buf, &mut pos); + assert!(obj == obj2, 1); + } + + #[test] + fun test_append_extract_eid_tagged_price_list() { + let objs = vector[]; + vector::push_back(&mut objs, tag_price_with_eid( + 123, + new_price(456, 789, 101112), + )); + vector::push_back(&mut objs, tag_price_with_eid( + 321, + new_price(654, 987, 111210), + )); + + let serialized = serialize_eid_tagged_price_list(&objs); + let objs2 = deserialize_eid_tagged_price_list(&serialized); + assert!(objs == objs2, 1); + } +} diff --git a/packages/layerzero-v2/initia/contracts/worker_peripherals/price_feed_routers/price_feed_router_0/Move.toml b/packages/layerzero-v2/initia/contracts/worker_peripherals/price_feed_routers/price_feed_router_0/Move.toml new file mode 100644 index 00000000..dd6272e8 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/worker_peripherals/price_feed_routers/price_feed_router_0/Move.toml @@ -0,0 +1,29 @@ +[package] +name = "price_feed_router_0" +version = "1.0.0" + +[addresses] +price_feed_router_0 = "_" +price_feed_router_1 = "_" +price_feed_module_0 = "_" +endpoint_v2_common = "_" +endpoint_v2 = "_" +layerzero_admin = "_" +layerzero_treasury_admin = "_" +worker_common = "_" + +[dev-addresses] +price_feed_router_0 = "0x65DD71A" +price_feed_router_1 = "0x65DD71AB" +price_feed_module_0 = "0x65DD71" +endpoint_v2_common = "0x9098" +endpoint_v2 = "0x12345678" +layerzero_admin = "0x18943124" +layerzero_treasury_admin = "0x1894312499" +worker_common = "0x3999" + +[dependencies] +endpoint_v2_common = { local = "../../../endpoint_v2_common" } +worker_common = { local = "../../worker_common" } +price_feed_module_0 = { local = "../../price_feed_modules/price_feed_module_0" } +price_feed_router_1 = { local = "../../price_feed_routers/price_feed_router_1_placeholder" } \ No newline at end of file diff --git a/packages/layerzero-v2/initia/contracts/worker_peripherals/price_feed_routers/price_feed_router_0/sources/router.move b/packages/layerzero-v2/initia/contracts/worker_peripherals/price_feed_routers/price_feed_router_0/sources/router.move new file mode 100644 index 00000000..386bfe96 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/worker_peripherals/price_feed_routers/price_feed_router_0/sources/router.move @@ -0,0 +1,21 @@ +module price_feed_router_0::router { + public fun estimate_fee_on_send( + price_feed: address, + feed_address: address, + dst_eid: u32, + call_data_size: u64, + gas: u128, + ): (u128, u128, u128, u128) { + if (price_feed == @price_feed_module_0) { + price_feed_module_0::feeds::estimate_fee_on_send(feed_address, dst_eid, call_data_size, gas) + } else { + price_feed_router_1::router::estimate_fee_on_send( + price_feed, + feed_address, + dst_eid, + call_data_size, + gas, + ) + } + } +} \ No newline at end of file diff --git a/packages/layerzero-v2/initia/contracts/worker_peripherals/price_feed_routers/price_feed_router_1_placeholder/Move.toml b/packages/layerzero-v2/initia/contracts/worker_peripherals/price_feed_routers/price_feed_router_1_placeholder/Move.toml new file mode 100644 index 00000000..39a77f26 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/worker_peripherals/price_feed_routers/price_feed_router_1_placeholder/Move.toml @@ -0,0 +1,14 @@ +[package] +name = "price_feed_router_1" +version = "1.0.0" + +[addresses] +price_feed_router_1 = "_" + +[dev-addresses] +price_feed_router_1 = "0x2142134" + +[dependencies.InitiaStdlib] +git = "https://github.com/initia-labs/move-natives.git" +rev = "77d5f3e140143bdaa41f850115b3035c134193e3" +subdir = "initia_stdlib" diff --git a/packages/layerzero-v2/initia/contracts/worker_peripherals/price_feed_routers/price_feed_router_1_placeholder/sources/router.move b/packages/layerzero-v2/initia/contracts/worker_peripherals/price_feed_routers/price_feed_router_1_placeholder/sources/router.move new file mode 100644 index 00000000..59dca70b --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/worker_peripherals/price_feed_routers/price_feed_router_1_placeholder/sources/router.move @@ -0,0 +1,15 @@ +module price_feed_router_1::router { + public fun estimate_fee_on_send( + _price_feed: address, + _feed_address: address, + _dst_eid: u32, + _call_data_size: u64, + _gas: u128, + ): (u128, u128, u128, u128) { + abort EUNKNOWN_PRICE_FEED + } + + // ================================================== Error Codes ================================================= + + const EUNKNOWN_PRICE_FEED: u64 = 1; +} \ No newline at end of file diff --git a/packages/layerzero-v2/initia/contracts/worker_peripherals/worker_common/Move.toml b/packages/layerzero-v2/initia/contracts/worker_peripherals/worker_common/Move.toml new file mode 100644 index 00000000..c7be39a2 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/worker_peripherals/worker_common/Move.toml @@ -0,0 +1,26 @@ +[package] +name = "worker_common" +version = "1.0.0" + +[addresses] +endpoint_v2 = "_" +endpoint_v2_common = "_" +layerzero_admin = "_" +layerzero_treasury_admin = "_" +worker_common = "_" +native_token_metadata_address = "0x8e4733bdabcf7d4afc3d14f0dd46c9bf52fb0fce9e4b996c939e195b8bc891d9" + +[dev-addresses] +endpoint_v2 = "0x13241234" +endpoint_v2_common = "0x9098" +layerzero_admin = "0x18943124" +layerzero_treasury_admin = "0x1894312499" +worker_common = "0x3999" + +[dependencies.InitiaStdlib] +git = "https://github.com/initia-labs/move-natives.git" +rev = "77d5f3e140143bdaa41f850115b3035c134193e3" +subdir = "initia_stdlib" + +[dependencies] +endpoint_v2_common = { local = "../../endpoint_v2_common" } \ No newline at end of file diff --git a/packages/layerzero-v2/initia/contracts/worker_peripherals/worker_common/sources/internal/multisig_store.move b/packages/layerzero-v2/initia/contracts/worker_peripherals/worker_common/sources/internal/multisig_store.move new file mode 100644 index 00000000..79e2e0fc --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/worker_peripherals/worker_common/sources/internal/multisig_store.move @@ -0,0 +1,198 @@ +module worker_common::multisig_store { + use std::option; + use std::signer::address_of; + use std::table::{Self, Table}; + use std::vector; + + use endpoint_v2_common::assert_no_duplicates::assert_no_duplicates; + use endpoint_v2_common::bytes32::{Bytes32, from_bytes32}; + + friend worker_common::multisig; + + #[test_only] + friend worker_common::signing_store_tests; + + const PUBKEY_SIZE: u64 = 64; + const SIGNATURE_SIZE: u64 = 65; // 64 bytes signature + 1 byte recovery id + + struct SigningStore has key { + quorum: u64, + signers: vector>, + used_hashes: Table, + } + + public(friend) fun initialize_for_worker(worker_account: &signer) { + let worker_address = address_of(worker_account); + assert!(!exists(worker_address), EWORKER_ALREADY_INITIALIZED); + move_to(move worker_account, SigningStore { + quorum: 0, + signers: vector[], + used_hashes: table::new(), + }); + } + + /// Mark the hash as used after asserting that the hash is not expired, signatures are verified, and the hash has + /// not been previously used + public(friend) fun assert_all_and_add_to_history( + worker: address, + signatures: &vector, + expiry: u64, + hash: Bytes32, + ) acquires SigningStore { + assert_not_expired(expiry); + assert_signatures_verified(worker, signatures, hash); + assert!(!was_hash_used(worker, hash), EHASH_ALREADY_USED); + add_hash_to_used(worker, hash); + } + + /// Asserts that multiple signatures match the provided pub keys at the provided quorum threshold + fun assert_signatures_verified( + worker: address, + signatures_joined: &vector, + hash: Bytes32, + ) acquires SigningStore { + let signatures = &split_signatures(signatures_joined); + let quorum = get_quorum(worker); + let signers = get_multisig_signers(worker); + assert_signatures_verified_internal(signatures, hash, &signers, quorum); + } + + /// Internal - asserts that multiple signatures match the provided pub keys at the provided quorum threshold + public(friend) fun assert_signatures_verified_internal( + signatures: &vector>, + hash: Bytes32, + multisig_signers: &vector>, + quorum: u64, + ) { + let signatures_count = vector::length(signatures); + assert!(signatures_count >= quorum, EDVN_LESS_THAN_QUORUM); + + let pub_keys_verified = &mut vector[]; + for (i in 0..signatures_count) { + let pubkey_bytes = get_pubkey(vector::borrow(signatures, i), hash); + assert!(vector::contains(multisig_signers, &pubkey_bytes), EDVN_INCORRECT_SIGNATURE); + assert!(!vector::contains(pub_keys_verified, &pubkey_bytes), EDVN_DUPLICATE_PK); + vector::push_back(pub_keys_verified, pubkey_bytes); + } + } + + /// Internal - gets the pubkey given a signature + public(friend) fun get_pubkey(signature_with_recovery: &vector, hash: Bytes32): vector { + let signature = vector::slice(signature_with_recovery, 0, 64); + let recovery_id = *vector::borrow(signature_with_recovery, 64); + + let ecdsa_signature = std::secp256k1::ecdsa_signature_from_bytes(signature); + let pubkey = std::secp256k1::ecdsa_recover( + from_bytes32(hash), + recovery_id, + &ecdsa_signature, + ); + assert!(std::option::is_some(&pubkey), EDVN_INCORRECT_SIGNATURE); + std::secp256k1::ecdsa_raw_public_key_to_bytes(option::borrow(&pubkey)) + } + + /// Internal - splits a vector of signatures into a vector of vectors of signatures + public(friend) fun split_signatures(signatures: &vector): vector> { + let bytes_length = vector::length(signatures); + assert!(bytes_length % SIGNATURE_SIZE == 0, EINVALID_SIGNATURE_LENGTH); + + let signatures_vector = vector[]; + let i = 0; + while (i < bytes_length) { + let signature = vector::slice(signatures, i, i + SIGNATURE_SIZE); + vector::push_back(&mut signatures_vector, signature); + i = i + SIGNATURE_SIZE; + }; + signatures_vector + } + + public(friend) fun assert_not_expired(expiration: u64) { + let current_time = std::timestamp::now_seconds(); + assert!(expiration > current_time, EEXPIRED_SIGNATURE); + } + + public(friend) fun set_quorum(worker: address, quorum: u64) acquires SigningStore { + let store = signing_store_mut(worker); + let signer_count = vector::length(&store.signers); + assert!(quorum > 0, EZERO_QUORUM); + assert!(quorum <= signer_count, ESIGNERS_LESS_THAN_QUORUM); + store.quorum = quorum; + } + + public(friend) fun get_quorum(worker: address): u64 acquires SigningStore { + signing_store(worker).quorum + } + + public(friend) fun set_multisig_signers( + worker: address, + multisig_signers: vector>, + ) acquires SigningStore { + assert_no_duplicates(&multisig_signers); + vector::for_each_ref(&multisig_signers, |signer| { + assert!(vector::length(signer) == PUBKEY_SIZE, EINVALID_SIGNER_LENGTH); + }); + let store = signing_store_mut(worker); + assert!(store.quorum <= vector::length(&multisig_signers), ESIGNERS_LESS_THAN_QUORUM); + store.signers = multisig_signers; + } + + public(friend) fun add_multisig_signer(worker: address, multisig_signer: vector) acquires SigningStore { + let multisig_signers = &mut signing_store_mut(worker).signers; + assert!(!vector::contains(multisig_signers, &multisig_signer), ESIGNER_ALREADY_EXISTS); + assert!(vector::length(&multisig_signer) == PUBKEY_SIZE, EINVALID_SIGNER_LENGTH); + vector::push_back(multisig_signers, multisig_signer); + } + + public(friend) fun remove_multisig_signer(worker: address, multisig_signer: vector) acquires SigningStore { + let (found, index) = vector::index_of(&signing_store(worker).signers, &multisig_signer); + assert!(found, ESIGNER_NOT_FOUND); + vector::swap_remove(&mut signing_store_mut(worker).signers, index); + let store = signing_store(worker); + assert!(vector::length(&store.signers) >= store.quorum, ESIGNERS_LESS_THAN_QUORUM); + } + + public(friend) fun is_multisig_signer(worker: address, multisig_signer: vector): bool acquires SigningStore { + let multisig_signers = &signing_store(worker).signers; + vector::contains(multisig_signers, &multisig_signer) + } + + public(friend) fun get_multisig_signers(worker: address): vector> acquires SigningStore { + signing_store(worker).signers + } + + public(friend) fun was_hash_used(worker: address, hash: Bytes32): bool acquires SigningStore { + let dvn_used_hashes = &signing_store(worker).used_hashes; + table::contains(dvn_used_hashes, hash) + } + + public(friend) fun add_hash_to_used(worker: address, hash: Bytes32) acquires SigningStore { + let dvn_used_hashes = &mut signing_store_mut(worker).used_hashes; + table::add(dvn_used_hashes, hash, true); + } + + public(friend) fun assert_initialized(worker: address) { + assert!(exists(worker), EWORKER_MULTISIG_NOT_REGISTERED); + } + + // ==================================================== Helpers =================================================== + + inline fun signing_store(worker: address): &SigningStore { borrow_global(worker) } + + inline fun signing_store_mut(worker: address): &mut SigningStore { borrow_global_mut(worker) } + + // ================================================== Error Codes ================================================= + + const EWORKER_ALREADY_INITIALIZED: u64 = 1; + const EDVN_INCORRECT_SIGNATURE: u64 = 2; + const EWORKER_MULTISIG_NOT_REGISTERED: u64 = 3; + const EDVN_DUPLICATE_PK: u64 = 4; + const EDVN_LESS_THAN_QUORUM: u64 = 5; + const EINVALID_SIGNATURE_LENGTH: u64 = 6; + const EEXPIRED_SIGNATURE: u64 = 7; + const EHASH_ALREADY_USED: u64 = 8; + const ESIGNERS_LESS_THAN_QUORUM: u64 = 9; + const ESIGNER_NOT_FOUND: u64 = 10; + const ESIGNER_ALREADY_EXISTS: u64 = 11; + const EINVALID_SIGNER_LENGTH: u64 = 12; + const EZERO_QUORUM: u64 = 13; +} diff --git a/packages/layerzero-v2/initia/contracts/worker_peripherals/worker_common/sources/internal/worker_config_store.move b/packages/layerzero-v2/initia/contracts/worker_peripherals/worker_common/sources/internal/worker_config_store.move new file mode 100644 index 00000000..99bbef7e --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/worker_peripherals/worker_common/sources/internal/worker_config_store.move @@ -0,0 +1,406 @@ +module worker_common::worker_config_store { + use std::option::{Self, Option}; + use std::signer::address_of; + use std::table::{Self, Table}; + use std::vector; + + friend worker_common::worker_config; + + #[test_only] + friend worker_common::worker_config_store_tests; + + struct WorkerStore has key { + // The worker ID (either Executor: 1 or DVN: 2) + worker_id: u8, + // The feelib that should be used for this worker + fee_lib: address, + // The admins of this worker + admins: vector
, + // The role admins of this worker - role admins can add/remove admins and other role admins + role_admins: vector
, + // The supported message libraries for this worker + supported_msglibs: vector
, + // The allowlist of senders - if not empty, no senders not on allowlist will be allowed + allowlist: Table, + // The denylist of senders - senders on the denylist will not be allowed + denylist: Table, + // The number of items on the allowlist. This is needed to check whether the allowlist is empty + allowlist_count: u64, + // Whether the worker has been paused. If the worker is paused, any send transaction that involves this worker + // will fail + paused: bool, + // The optional price feed module and feed selection for this worker + price_feed_selection: Option, + // The optional price feed delegate selection for this worker + price_feed_delegate_selection: Option
, + // The number of workers delegating to this worker for price feed selection + count_price_feed_delegating_workers: u64, + // The deposit address for this worker: worker payments will be sent to this address + deposit_address: address, + // The default multiplier bps that will be used to calculate premiums for this worker + default_multiplier_bps: u16, + // The serialized supported option types for this worker + supported_option_types: vector, + // Per destination EID configuration (for executors only) + executor_dst_config: Table, + // Per destination EID configuration (for DVNs only) + dvn_dst_config: Table, + } + + struct PriceFeedSelection has drop, copy, store { + price_feed_module: address, + price_feed: address, + } + + struct ExecutorDstConfig has store, copy, drop { + lz_receive_base_gas: u64, + multiplier_bps: u16, + floor_margin_usd: u128, + native_cap: u128, + lz_compose_base_gas: u64, + } + + struct DvnDstConfig has store, copy, drop { + gas: u64, + multiplier_bps: u16, + floor_margin_usd: u128, + } + + public(friend) fun initialize_store_for_worker( + worker_account: &signer, + worker_id: u8, + deposit_address: address, + role_admin: address, + admins: vector
, + supported_msglibs: vector
, + fee_lib: address, + ) { + let worker_address = address_of(worker_account); + assert!(!exists(worker_address), EWORKER_ALREADY_INITIALIZED); + assert!(vector::length(&admins) > 0, ENO_ADMINS_PROVIDED); + move_to(move worker_account, WorkerStore { + worker_id, + fee_lib, + role_admins: vector[role_admin], + admins, + supported_msglibs, + allowlist: table::new(), + denylist: table::new(), + allowlist_count: 0, + paused: false, + price_feed_selection: option::none(), + price_feed_delegate_selection: option::none(), + count_price_feed_delegating_workers: 0, + deposit_address, + default_multiplier_bps: 0, + supported_option_types: vector[], + executor_dst_config: table::new(), + dvn_dst_config: table::new(), + }) + } + + public(friend) fun assert_initialized(worker: address) { + assert!(exists(worker), EWORKER_NOT_REGISTERED); + } + + public(friend) fun is_worker_initialized(worker: address): bool { + exists(worker) + } + + public(friend) fun get_worker_id(worker: address): u8 acquires WorkerStore { + worker_store(worker).worker_id + } + + // ================================================ Worker General ================================================ + + public(friend) fun set_supported_msglibs(worker: address, msglibs: vector
) acquires WorkerStore { + worker_store_mut(worker).supported_msglibs = msglibs; + } + + public(friend) fun get_supported_msglibs(worker: address): vector
acquires WorkerStore { + worker_store(worker).supported_msglibs + } + + public(friend) fun is_supported_msglib(worker: address, msglib: address): bool acquires WorkerStore { + vector::contains(&worker_store(worker).supported_msglibs, &msglib) + } + + public(friend) fun set_fee_lib(worker: address, fee_lib: address) acquires WorkerStore { + worker_store_mut(worker).fee_lib = fee_lib; + } + + public(friend) fun get_fee_lib(worker: address): address acquires WorkerStore { + worker_store(worker).fee_lib + } + + public(friend) fun set_pause_status(worker: address, paused: bool) acquires WorkerStore { + worker_store_mut(worker).paused = paused; + } + + public(friend) fun is_paused(worker: address): bool acquires WorkerStore { + worker_store(worker).paused + } + + public(friend) fun set_deposit_address(worker: address, deposit_address: address) acquires WorkerStore { + worker_store_mut(worker).deposit_address = deposit_address; + } + + public(friend) fun get_deposit_address(worker: address): address acquires WorkerStore { + worker_store(worker).deposit_address + } + + public(friend) fun set_default_multiplier_bps(worker: address, default_multiplier_bps: u16) acquires WorkerStore { + worker_store_mut(worker).default_multiplier_bps = default_multiplier_bps; + } + + public(friend) fun get_default_multiplier_bps(worker: address): u16 acquires WorkerStore { + worker_store(worker).default_multiplier_bps + } + + public(friend) fun set_supported_option_types(worker: address, option_types: vector) acquires WorkerStore { + worker_store_mut(worker).supported_option_types = option_types; + } + + public(friend) fun get_supported_option_types(worker: address): vector acquires WorkerStore { + worker_store(worker).supported_option_types + } + + // =================================================== Executor =================================================== + + public(friend) fun set_executor_dst_config( + worker: address, + dst_eid: u32, + lz_receive_base_gas: u64, + multiplier_bps: u16, + floor_margin_usd: u128, + native_cap: u128, + lz_compose_base_gas: u64, + ) acquires WorkerStore { + let executor_dst_config = &mut worker_store_mut(worker).executor_dst_config; + table::upsert(executor_dst_config, dst_eid, ExecutorDstConfig { + lz_receive_base_gas, + multiplier_bps, + floor_margin_usd, + native_cap, + lz_compose_base_gas, + }); + } + + public(friend) fun get_executor_dst_config_values( + worker: address, + dst_eid: u32, + ): (u64, u16, u128, u128, u64) acquires WorkerStore { + let config_store = &worker_store(worker).executor_dst_config; + assert!(table::contains(config_store, dst_eid), EEXECUTOR_DST_EID_NOT_CONFIGURED); + let executor_dst_config = table::borrow(config_store, dst_eid); + ( + executor_dst_config.lz_receive_base_gas, + executor_dst_config.multiplier_bps, + executor_dst_config.floor_margin_usd, + executor_dst_config.native_cap, + executor_dst_config.lz_compose_base_gas, + ) + } + + // ====================================================== DVN ===================================================== + + public(friend) fun set_dvn_dst_config( + worker: address, + dst_eid: u32, + gas: u64, + multiplier_bps: u16, + floor_margin_usd: u128, + ) acquires WorkerStore { + let dvn_dst_config = &mut worker_store_mut(worker).dvn_dst_config; + table::upsert(dvn_dst_config, dst_eid, DvnDstConfig { gas, multiplier_bps, floor_margin_usd }); + } + + public(friend) fun get_dvn_dst_config_values( + worker: address, + dst_eid: u32, + ): (u64, u16, u128) acquires WorkerStore { + let config_store = &worker_store(worker).dvn_dst_config; + assert!(table::contains(config_store, dst_eid), EDVN_DST_EID_NOT_CONFIGURED); + let dvn_dst_config = table::borrow(&worker_store(worker).dvn_dst_config, dst_eid); + (dvn_dst_config.gas, dvn_dst_config.multiplier_bps, dvn_dst_config.floor_margin_usd) + } + + // ================================================== Price Feed ================================================== + + public(friend) fun set_price_feed( + worker: address, + price_feed_module: address, + price_feed: address, + ) acquires WorkerStore { + worker_store_mut(worker).price_feed_selection = option::some(PriceFeedSelection { + price_feed_module, + price_feed, + }); + } + + public(friend) fun has_price_feed(worker: address): bool acquires WorkerStore { + option::is_some(&worker_store(worker).price_feed_selection) + } + + public(friend) fun get_price_feed(worker: address): (address, address) acquires WorkerStore { + let price_feed_selection = &worker_store(worker).price_feed_selection; + assert!(option::is_some(price_feed_selection), ENO_PRICE_FEED_CONFIGURED); + let selection = option::borrow(price_feed_selection); + (selection.price_feed_module, selection.price_feed) + } + + public(friend) fun set_price_feed_delegate(worker: address, delegate: address) acquires WorkerStore { + let price_feed_delegate_selection = &mut worker_store_mut(worker).price_feed_delegate_selection; + let prior_delegate = *price_feed_delegate_selection; + + assert!(option::is_none(&prior_delegate) || *option::borrow(&prior_delegate) != delegate, EUNCHANGED); + *price_feed_delegate_selection = option::some(delegate); + + // subtract from prior delegate's count if it exists + if (option::is_some(&prior_delegate)) { + let prior = *option::borrow(&prior_delegate); + let count_delegating = &mut worker_store_mut(prior).count_price_feed_delegating_workers; + *count_delegating = *count_delegating - 1; + }; + + // add to new delegate's count + let count_delegating = &mut worker_store_mut(delegate).count_price_feed_delegating_workers; + *count_delegating = *count_delegating + 1; + } + + public(friend) fun unset_price_feed_delegate(worker: address) acquires WorkerStore { + let price_feed_delegate_selection = &mut worker_store_mut(worker).price_feed_delegate_selection; + assert!(option::is_some(price_feed_delegate_selection), ENOT_DELEGATING); + + let prior_delegate = *option::borrow(price_feed_delegate_selection); + *price_feed_delegate_selection = option::none(); + + // subtract from prior delegate's count + let count_delegating = &mut worker_store_mut(prior_delegate).count_price_feed_delegating_workers; + *count_delegating = *count_delegating - 1; + } + + public(friend) fun has_price_feed_delegate(worker: address): bool acquires WorkerStore { + option::is_some(&worker_store(worker).price_feed_delegate_selection) + } + + public(friend) fun get_price_feed_delegate(worker: address): address acquires WorkerStore { + *option::borrow(&worker_store(worker).price_feed_delegate_selection) + } + + public(friend) fun get_count_price_feed_delegate_dependents(worker: address): u64 acquires WorkerStore { + worker_store(worker).count_price_feed_delegating_workers + } + + // ==================================================== Admins ==================================================== + + public(friend) fun add_role_admin(worker: address, role_admin: address) acquires WorkerStore { + let role_admins = &mut worker_store_mut(worker).role_admins; + assert!(!vector::contains(role_admins, &role_admin), EROLE_ADMIN_ALREADY_EXISTS); + vector::push_back(role_admins, role_admin); + } + + public(friend) fun remove_role_admin(worker: address, role_admin: address) acquires WorkerStore { + let (found, index) = vector::index_of(&worker_store(worker).role_admins, &role_admin); + assert!(found, EROLE_ADMIN_NOT_FOUND); + vector::swap_remove(&mut worker_store_mut(worker).role_admins, index); + } + + public(friend) fun get_role_admins(worker: address): vector
acquires WorkerStore { + worker_store(worker).role_admins + } + + public(friend) fun is_role_admin(worker: address, role_admin: address): bool acquires WorkerStore { + vector::contains(&worker_store(worker).role_admins, &role_admin) + } + + public(friend) fun add_admin(worker: address, admin: address) acquires WorkerStore { + let admins = &mut worker_store_mut(worker).admins; + assert!(!vector::contains(admins, &admin), EADMIN_ALREADY_EXISTS); + vector::push_back(admins, admin); + } + + public(friend) fun remove_admin(worker: address, admin: address) acquires WorkerStore { + let (found, index) = vector::index_of(&worker_store(worker).admins, &admin); + assert!(found, EADMIN_NOT_FOUND); + let admins = &mut worker_store_mut(worker).admins; + vector::swap_remove(admins, index); + assert!(vector::length(admins) > 0, EATTEMPING_TO_REMOVE_ONLY_ADMIN); + } + + public(friend) fun get_admins(worker: address): vector
acquires WorkerStore { + worker_store(worker).admins + } + + public(friend) fun is_admin(worker: address, admin: address): bool acquires WorkerStore { + vector::contains(&worker_store(worker).admins, &admin) + } + + // ====================================================== ACL ===================================================== + + public(friend) fun add_to_allowlist(worker: address, sender: address) acquires WorkerStore { + let allowlist = &mut worker_store_mut(worker).allowlist; + assert!(!table::contains(allowlist, sender), EWORKER_ALREADY_ON_ALLOWLIST); + table::add(allowlist, sender, true); + let count_allowed = &mut worker_store_mut(worker).allowlist_count; + *count_allowed = *count_allowed + 1; + } + + public(friend) fun remove_from_allowlist(worker: address, sender: address) acquires WorkerStore { + let allowlist = &mut worker_store_mut(worker).allowlist; + assert!(table::contains(allowlist, sender), EWORKER_NOT_ON_ALLOWLIST); + table::remove(allowlist, sender); + let count_allowed = &mut worker_store_mut(worker).allowlist_count; + *count_allowed = *count_allowed - 1; + } + + public(friend) fun add_to_denylist(worker: address, sender: address) acquires WorkerStore { + let denylist = &mut worker_store_mut(worker).denylist; + assert!(!table::contains(denylist, sender), EWORKER_ALREADY_ON_DENYLIST); + table::add(denylist, sender, true); + } + + public(friend) fun remove_from_denylist(worker: address, sender: address) acquires WorkerStore { + let denylist = &mut worker_store_mut(worker).denylist; + assert!(table::contains(denylist, sender), EWORKER_NOT_ON_DENYLIST); + table::remove(denylist, sender); + } + + public(friend) fun is_on_allowlist(worker: address, sender: address): bool acquires WorkerStore { + table::contains(&worker_store(worker).allowlist, sender) + } + + public(friend) fun is_on_denylist(worker: address, sender: address): bool acquires WorkerStore { + table::contains(&worker_store(worker).denylist, sender) + } + + public(friend) fun has_allowlist(worker: address): bool acquires WorkerStore { + worker_store(worker).allowlist_count > 0 + } + + // ==================================================== Helpers =================================================== + + inline fun worker_store(worker: address): &WorkerStore { borrow_global(worker) } + + inline fun worker_store_mut(worker: address): &mut WorkerStore { borrow_global_mut(worker) } + + // ==================================================Error Codes ================================================== + + const EADMIN_ALREADY_EXISTS: u64 = 1; + const EADMIN_NOT_FOUND: u64 = 2; + const EATTEMPING_TO_REMOVE_ONLY_ADMIN: u64 = 3; + const EDVN_DST_EID_NOT_CONFIGURED: u64 = 4; + const EEXECUTOR_DST_EID_NOT_CONFIGURED: u64 = 5; + const ENOT_DELEGATING: u64 = 6; + const ENO_ADMINS_PROVIDED: u64 = 7; + const ENO_PRICE_FEED_CONFIGURED: u64 = 8; + const EROLE_ADMIN_ALREADY_EXISTS: u64 = 9; + const EROLE_ADMIN_NOT_FOUND: u64 = 10; + const EUNCHANGED: u64 = 11; + const EWORKER_ALREADY_INITIALIZED: u64 = 12; + const EWORKER_ALREADY_ON_ALLOWLIST: u64 = 13; + const EWORKER_ALREADY_ON_DENYLIST: u64 = 14; + const EWORKER_NOT_ON_ALLOWLIST: u64 = 15; + const EWORKER_NOT_ON_DENYLIST: u64 = 16; + const EWORKER_NOT_REGISTERED: u64 = 17; +} diff --git a/packages/layerzero-v2/initia/contracts/worker_peripherals/worker_common/sources/multisig.move b/packages/layerzero-v2/initia/contracts/worker_peripherals/worker_common/sources/multisig.move new file mode 100644 index 00000000..bdebbeb3 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/worker_peripherals/worker_common/sources/multisig.move @@ -0,0 +1,147 @@ +// This multisig module was developed for use with DVN workers. It is used to manage the quorum and signers for +// a worker and to verify that the required number of signatures are present and valid when a change is made. It +// keeps track of used hashes to prevent the same command being replayed (all hashes should be hashed with an expiration +// time, which allows two of the otherwise same command to be called at different times). +// The important place where the configuration is used beyond direct multisig activity is that the fee_lib_0 depends on +// the quorum number set here to compute the send fee for the DVN, which scale with the quorum of signers required. +module worker_common::multisig { + use std::event::emit; + use std::signer::address_of; + + use endpoint_v2_common::bytes32::{Bytes32, to_bytes32}; + use endpoint_v2_common::contract_identity::{ + CallRef, + get_call_ref_caller, + }; + use worker_common::multisig_store; + + struct WorkerMultisigTarget {} + + /// Initialize the worker signing / multisig store + /// The signers provided are the public keys of the signers that are allowed to sign for the worker + /// A quorum of signatures is required to sign for a transaction to succeed + public fun initialize_for_worker(account: &signer, quorum: u64, signers: vector>) { + multisig_store::initialize_for_worker(account); + let worker = address_of(move account); + multisig_store::set_multisig_signers(worker, signers); + emit(SetSigners { worker, multisig_signers: signers }); + multisig_store::set_quorum(worker, quorum); + emit(SetQuorum { worker, quorum }); + } + + #[test_only] + /// Initialize the worker signing / multisig store for testing purposes + public fun initialize_for_worker_test_only(worker: address, quorum: u64, signers: vector>) { + let account = &std::account::create_signer_for_test(worker); + initialize_for_worker(account, quorum, signers); + } + + /// Assert that all signatures are valid, it is not expired, and add the hash to the history or abort if it is + /// present. This will abort if the hash has already been used, expired, or the signatures are invalid + /// *Important*: The `hash` must be the result of a hash operation to be secure. The caller should ensure that the + /// expiry time provided is a component of the hash provided + public fun assert_all_and_add_to_history( + call_ref: &CallRef, + signatures: &vector, + expiration: u64, + hash: Bytes32, + ) { + let worker = get_call_ref_caller(call_ref); + multisig_store::assert_initialized(worker); + multisig_store::assert_all_and_add_to_history(worker, signatures, expiration, hash); + } + + /// Set the quorum required for a worker + public fun set_quorum(call_ref: &CallRef, quorum: u64) { + let worker = get_call_ref_caller(call_ref); + multisig_store::assert_initialized(worker); + multisig_store::set_quorum(worker, quorum); + emit(SetQuorum { worker, quorum }); + } + + /// Set the signers (public keys) for a worker + public fun set_signers(call_ref: &CallRef, multisig_signers: vector>) { + let worker = get_call_ref_caller(call_ref); + multisig_store::assert_initialized(worker); + multisig_store::set_multisig_signers(worker, multisig_signers); + emit(SetSigners { worker, multisig_signers }); + } + + /// Set a signer (public key) for a worker + /// `active` param refers to whether the signer should be added (true) or deleted (false) + public fun set_signer(call_ref: &CallRef, multisig_signer: vector, active: bool) { + let worker = get_call_ref_caller(call_ref); + multisig_store::assert_initialized(worker); + if (active) { + multisig_store::add_multisig_signer(worker, multisig_signer); + } else { + multisig_store::remove_multisig_signer(worker, multisig_signer); + }; + emit(UpdateSigner { worker, multisig_signer, active }); + } + + #[view] + /// Get the quorum required for a worker + public fun get_quorum(worker: address): u64 { + multisig_store::assert_initialized(worker); + multisig_store::get_quorum(worker) + } + + #[view] + /// Get the signers (public keys) for a worker + public fun get_signers(worker: address): vector> { + multisig_store::assert_initialized(worker); + multisig_store::get_multisig_signers(worker) + } + + #[view] + /// Check if a signer (public key) is a signer for a worker + public fun is_signer(worker: address, multisig_signer: vector): bool { + multisig_store::assert_initialized(worker); + multisig_store::is_multisig_signer(worker, multisig_signer) + } + + #[view] + /// Check if a specific hash has already been used to submit a transaction + public fun was_hash_used(worker: address, hash: vector): bool { + multisig_store::assert_initialized(worker); + multisig_store::was_hash_used(worker, to_bytes32(hash)) + } + + + // ==================================================== Events ==================================================== + + #[event] + /// Emitted when the quorum is set for a worker + struct SetQuorum has store, drop { worker: address, quorum: u64 } + + #[event] + /// Emitted when the signers are set for a worker + struct SetSigners has store, drop { worker: address, multisig_signers: vector> } + + #[event] + /// Emitted when a signer is added or removed for a worker + struct UpdateSigner has store, drop { + worker: address, + multisig_signer: vector, + active: bool, + } + + #[test_only] + /// Generates a SetQuorum event for testing + public fun set_quorum_event(worker: address, quorum: u64): SetQuorum { + SetQuorum { worker, quorum } + } + + #[test_only] + /// Generates a SetSigners event for testing + public fun set_signers_event(worker: address, multisig_signers: vector>): SetSigners { + SetSigners { worker, multisig_signers } + } + + #[test_only] + /// Generates an UpdateSigner event for testing + public fun update_signer_event(worker: address, multisig_signer: vector, active: bool): UpdateSigner { + UpdateSigner { worker, multisig_signer, active } + } +} diff --git a/packages/layerzero-v2/initia/contracts/worker_peripherals/worker_common/sources/worker_config.move b/packages/layerzero-v2/initia/contracts/worker_peripherals/worker_common/sources/worker_config.move new file mode 100644 index 00000000..e797e0d2 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/worker_peripherals/worker_common/sources/worker_config.move @@ -0,0 +1,753 @@ +/// Worker Config is a shared module that provides configuration for workers +module worker_common::worker_config { + use std::account; + use std::event::emit; + use std::fungible_asset::{Self, Metadata}; + use std::math128::pow; + use std::object::address_to_object; + + use endpoint_v2_common::contract_identity::{ + CallRef, + get_call_ref_caller, + }; + use worker_common::worker_config_store; + + #[test_only] + use std::account::create_signer_for_test; + + #[test_only] + friend worker_common::worker_config_tests; + + // Worker Ids + public inline fun WORKER_ID_EXECUTOR(): u8 { 1 } + + public inline fun WORKER_ID_DVN(): u8 { 2 } + + // ================================================ Initialization ================================================ + + #[test_only] + /// Initialize the worker for testing purposes (does not require a signer, and accepts fewer params) + public fun initialize_for_worker_test_only( + worker: address, + worker_id: u8, + deposit_address: address, + role_admin: address, + admins: vector
, + supported_msglibs: vector
, + fee_lib: address, + ) { + let account = &create_signer_for_test(worker); + worker_config_store::initialize_store_for_worker( + account, + worker_id, + deposit_address, + role_admin, + admins, + supported_msglibs, + fee_lib, + ); + } + + /// Initialize the worker - this must be called an can be called most once + public fun initialize_for_worker( + account: &signer, + worker_id: u8, + deposit_address: address, + role_admin: address, + admins: vector
, + supported_msglibs: vector
, + fee_lib: address, + ) { + assert!(account::exists_at(deposit_address), ENOT_AN_ACCOUNT); + worker_config_store::initialize_store_for_worker( + account, + worker_id, + deposit_address, + role_admin, + admins, + supported_msglibs, + fee_lib, + ); + } + + #[view] + // Check whether the worker is initialized + public fun is_worker_initialized(worker: address): bool { + worker_config_store::is_worker_initialized(worker) + } + + /// Assert that the fee lib supports a transaction + /// This checks that the worker is initialized, unpaused, the sender is allowed, that the msglib is supported, and + /// that the worker id matches the expected worker id + public fun assert_fee_lib_supports_transaction(worker: address, worker_id: u8, sender: address, msglib: address) { + worker_config_store::assert_initialized(worker); + assert_worker_unpaused(worker); + assert_allowed(worker, sender); + assert_supported_msglib(worker, msglib); + assert!(worker_config_store::get_worker_id(worker) == worker_id, EUNEXPECTED_WORKER_ID); + } + + #[view] + /// Get the worker id for the given worker address + public fun get_worker_id_from_worker_address(worker: address): u8 { + worker_config_store::assert_initialized(worker); + worker_config_store::get_worker_id(worker) + } + + #[view] + /// Get the worker's price feed and feed address + public fun get_worker_price_feed_config(worker: address): (address, address) { + worker_config_store::assert_initialized(worker); + worker_config_store::get_price_feed(worker) + } + + // ==================================================== Admins ==================================================== + + struct WorkerAdminsTarget {} + + /// Set the admin status for the given address using the worker's CallRef + public fun set_worker_admin( + call_ref: &CallRef, + admin: address, + active: bool, + ) { + let worker = get_call_ref_caller(call_ref); + worker_config_store::assert_initialized(worker); + if (active) { + worker_config_store::add_admin(worker, admin); + } else { + worker_config_store::remove_admin(worker, admin); + }; + emit(SetWorkerAdmin { worker, admin, active }); + } + + #[view] + /// Check if the given address is an admin for the worker + public fun is_worker_admin(worker: address, admin: address): bool { + worker_config_store::assert_initialized(worker); + worker_config_store::is_admin(worker, admin) + } + + #[view] + /// Get a list of the worker's admins + public fun get_worker_admins(worker: address): vector
{ + worker_config_store::assert_initialized(worker); + worker_config_store::get_admins(worker) + } + + /// Assert that the given address is an admin for the worker + public fun assert_worker_admin(worker: address, admin: address) { + worker_config_store::assert_initialized(worker); + assert!(worker_config_store::is_admin(worker, admin), EUNAUTHORIZED); + } + + /// Set a role admin for the worker using the worker's CallRef + public fun set_worker_role_admin( + call_ref: &CallRef, + role_admin: address, + active: bool, + ) { + let worker = get_call_ref_caller(call_ref); + worker_config_store::assert_initialized(worker); + if (active) { + worker_config_store::add_role_admin(worker, role_admin); + } else { + worker_config_store::remove_role_admin(worker, role_admin); + }; + emit(SetWorkerRoleAdmin { worker, role_admin, active }); + } + + #[view] + /// Check if the given address is a role admin for the worker + public fun is_worker_role_admin(worker: address, role_admin: address): bool { + worker_config_store::assert_initialized(worker); + worker_config_store::is_role_admin(worker, role_admin) + } + + #[view] + /// Get a list of the role admins for the worker + public fun get_worker_role_admins(worker: address): vector
{ + worker_config_store::assert_initialized(worker); + worker_config_store::get_role_admins(worker) + } + + /// Assert that the given address is a role admin for the worker + public fun assert_worker_role_admin(worker: address, role_admin: address) { + worker_config_store::assert_initialized(worker); + assert!(worker_config_store::is_role_admin(worker, role_admin), EUNAUTHORIZED); + } + + // ==================================================== Pausing =================================================== + + struct WorkerPauseTarget {} + + /// Pauses the worker on the send side - will cause get_fee() functions to abort + public fun set_worker_pause( + call_ref: &CallRef, + paused: bool, + ) { + let worker = get_call_ref_caller(call_ref); + let previous_status = worker_config_store::is_paused(worker); + assert!(previous_status != paused, EPAUSE_STATUS_UNCHANGED); + worker_config_store::set_pause_status(worker, paused); + if (paused) { + emit(Paused { worker }); + } else { + emit(Unpaused { worker }); + } + } + + /// Assert that the worker is not paused + public fun assert_worker_unpaused(worker: address) { + assert!(!is_worker_paused(worker), EWORKER_PAUSED); + } + + #[view] + /// Check if the worker is paused + public fun is_worker_paused(worker: address): bool { + worker_config_store::assert_initialized(worker); + worker_config_store::is_paused(worker) + } + + + // =============================================== Message Libraries ============================================== + + struct WorkerMsgLibsTarget {} + + /// Set the supported message libraries for the worker + public fun set_supported_msglibs( + call_ref: &CallRef, + msglibs: vector
, + ) { + let worker = get_call_ref_caller(call_ref); + worker_config_store::set_supported_msglibs(worker, msglibs); + emit(SetSupportedMsglibs { worker, msglibs }); + } + + #[view] + /// Get the supported message libraries for the worker + public fun get_supported_msglibs(worker: address): vector
{ + worker_config_store::assert_initialized(worker); + worker_config_store::get_supported_msglibs(worker) + } + + /// Assert that the worker supports the given message library + public fun assert_supported_msglib(worker: address, msglib: address) { + assert!( + worker_config_store::is_supported_msglib(worker, msglib), + EWORKER_AUTH_UNSUPPORTED_MSGLIB, + ); + } + + // ================================================ Deposit Address =============================================== + + struct WorkerDepositAddressTarget {} + + /// Set the deposit address for the worker + public fun set_deposit_address(call_ref: &CallRef, deposit_address: address) { + let worker = get_call_ref_caller(call_ref); + assert!(account::exists_at(deposit_address), ENOT_AN_ACCOUNT); + worker_config_store::set_deposit_address(worker, deposit_address); + emit(SetDepositAddress { worker, deposit_address }); + } + + #[view] + /// Get the deposit address for the worker + public fun get_deposit_address(worker: address): address { + worker_config_store::assert_initialized(worker); + worker_config_store::get_deposit_address(worker) + } + + // ================================================== Price Feed ================================================== + + struct WorkerPriceFeedTarget {} + + /// Set the price feed module and price feed for the worker + public fun set_price_feed( + call_ref: &CallRef, + price_feed: address, + feed_address: address, + ) { + let worker = get_call_ref_caller(call_ref); + worker_config_store::set_price_feed(worker, price_feed, feed_address); + emit(SetPriceFeed { + worker, + price_feed, + feed_address, + }); + } + + #[view] + /// Get the effective price feed module and price feed for the worker, providing the delegated price feed details + /// if the worker has delegated the price feed; otherwise it provides what is directly configured for the worker + public fun get_effective_price_feed(worker: address): (address, address) { + worker_config_store::assert_initialized(worker); + if (worker_config_store::has_price_feed_delegate(worker)) { + let delegate = worker_config_store::get_price_feed_delegate(worker); + assert!(worker_config_store::has_price_feed(delegate), EWORKER_PRICE_FEED_DELEGATE_NOT_CONFIGURED); + worker_config_store::get_price_feed(delegate) + } else if (worker_config_store::has_price_feed(worker)) { + worker_config_store::get_price_feed(worker) + } else { + abort EWORKER_PRICE_FEED_NOT_CONFIGURED + } + } + + /// Sets a price feed delegate for the worker. This is another worker's address that has a price feed configured. + /// If the delegate is set to @0x0, the delegate is unset. When a worker has delegated to another worker, it will + /// use whatever is configured for the delegate worker when a fee is calculated + public fun set_price_feed_delegate(call_ref: &CallRef, delegate: address) { + let worker = get_call_ref_caller(call_ref); + if (delegate == @0x0) { + // Unset + assert!(worker_config_store::has_price_feed_delegate(worker), ENO_DELEGATE_TO_UNSET); + worker_config_store::unset_price_feed_delegate(worker); + } else { + // Set + worker_config_store::assert_initialized(delegate); + let (price_feed, feed_address) = worker_config_store::get_price_feed(delegate); + assert!(price_feed != @0x0, EDELEGATE_PRICE_FEED_NOT_CONFIGURED); + assert!(feed_address != @0x0, EDELEGATE_FEED_ADDRESS_NOT_CONFIGURED); + worker_config_store::set_price_feed_delegate(worker, delegate); + }; + emit(SetPriceFeedDelegate { worker, delegate }); + } + + #[view] + /// Get the price feed delegate for the worker + /// This will return @0x0 if the worker does not have a price feed delegate + public fun get_price_feed_delegate(worker: address): address { + worker_config_store::assert_initialized(worker); + if (!worker_config_store::has_price_feed_delegate(worker)) { + @0x0 + } else { + worker_config_store::get_price_feed_delegate(worker) + } + } + + #[view] + /// Get the count of other workers delegating to a worker for the price feed configuration + public fun get_count_price_feed_delegate_dependents(worker: address): u64 { + worker_config_store::get_count_price_feed_delegate_dependents(worker) + } + + // ============================================ Fee Libs Worker Config ============================================ + + struct WorkerFeeLibTarget {} + + /// Set the fee lib used for the worker + public fun set_worker_fee_lib(call_ref: &CallRef, fee_lib: address) { + let worker = get_call_ref_caller(call_ref); + worker_config_store::set_fee_lib(worker, fee_lib); + emit(WorkerFeeLibUpdated { worker, fee_lib }); + } + + #[view] + /// Get the fee lib for the worker + public fun get_worker_fee_lib(worker: address): address { + worker_config_store::assert_initialized(worker); + worker_config_store::get_fee_lib(worker) + } + + /// Set the default basis-points multiplier (for premium calculation) for the worker + public fun set_default_multiplier_bps(call_ref: &CallRef, default_multiplier_bps: u16) { + let worker = get_call_ref_caller(call_ref); + worker_config_store::assert_initialized(worker); + worker_config_store::set_default_multiplier_bps(worker, default_multiplier_bps); + emit(SetMultiplierBps { worker, default_multiplier_bps }); + } + + #[view] + /// Get the default basis-points multiplier for the worker + public fun get_default_multiplier_bps(worker: address): u16 { + worker_config_store::assert_initialized(worker); + worker_config_store::get_default_multiplier_bps(worker) + } + + /// Set the supported option types for the worker + public fun set_supported_option_types(call_ref: &CallRef, option_types: vector) { + let worker = get_call_ref_caller(call_ref); + worker_config_store::assert_initialized(worker); + worker_config_store::set_supported_option_types(worker, option_types); + emit(SetSupportedOptionTypes { worker, option_types }); + } + + #[view] + /// Get the supported option types for the worker + public fun get_supported_option_types(worker: address): vector { + worker_config_store::assert_initialized(worker); + worker_config_store::get_supported_option_types(worker) + } + + #[view] + /// Get the native decimals rate for the gas token on this chain + public fun get_native_decimals_rate(): u128 { + let decimals = fungible_asset::decimals(address_to_object(@native_token_metadata_address)); + pow(10, (decimals as u128)) + } + + // ================================================ Executor Config =============================================== + + struct WorkerExecutorTarget {} + + /// Set the executor destination config for the worker + /// @param call_ref The CallRef for the worker (should be addressed to @worker_common) + /// @param dst_eid The destination EID + /// @param lz_receive_base_gas The base gas for receiving a message + /// @param multiplier_bps The multiplier in basis points + /// @param floor_margin_usd The floor margin in USD + /// @param native_cap The native cap + /// @param lz_compose_base_gas The base gas for composing a message + public fun set_executor_dst_config( + call_ref: &CallRef, + dst_eid: u32, + lz_receive_base_gas: u64, + multiplier_bps: u16, + floor_margin_usd: u128, + native_cap: u128, + lz_compose_base_gas: u64, + ) { + let worker = get_call_ref_caller(call_ref); + worker_config_store::assert_initialized(worker); + worker_config_store::set_executor_dst_config( + worker, + dst_eid, + lz_receive_base_gas, + multiplier_bps, + floor_margin_usd, + native_cap, + lz_compose_base_gas, + ); + emit(SetExecutorDstConfig { + worker, + dst_eid, + lz_receive_base_gas, + multiplier_bps, + floor_margin_usd, + native_cap, + lz_compose_base_gas, + }); + } + + #[view] + /// Get the executor destination config for the worker + /// @return (lz_receive_base_gas, multiplier_bps, floor_margin_usd, native_cap, lz_compose_base_gas) + public fun get_executor_dst_config_values( + worker: address, + dst_eid: u32, + ): (u64, u16, u128, u128, u64) { + worker_config_store::assert_initialized(worker); + worker_config_store::get_executor_dst_config_values(worker, dst_eid) + } + + // ================================================== DVN Config ================================================== + + struct WorkerDvnTarget {} + + /// Set the DVN destination config for the worker + /// @param call_ref The CallRef for the worker (should be addressed to @worker_common) + /// @param dst_eid The destination EID + /// @param gas The gas + /// @param multiplier_bps The multiplier in basis points + /// @param floor_margin_usd The floor margin in USD + public fun set_dvn_dst_config( + call_ref: &CallRef, + dst_eid: u32, + gas: u64, + multiplier_bps: u16, + floor_margin_usd: u128, + ) { + let worker = get_call_ref_caller(call_ref); + worker_config_store::assert_initialized(worker); + worker_config_store::set_dvn_dst_config( + worker, + dst_eid, + gas, + multiplier_bps, + floor_margin_usd, + ); + emit(SetDvnDstConfig { + worker, + dst_eid, + gas, + multiplier_bps, + floor_margin_usd, + }); + } + + #[view] + /// Get the DVN destination config for the worker and destination EID + /// @return (gas, multiplier_bps, floor_margin_usd) + public fun get_dvn_dst_config_values( + worker: address, + dst_eid: u32, + ): (u64, u16, u128) { + worker_config_store::assert_initialized(worker); + worker_config_store::get_dvn_dst_config_values(worker, dst_eid) + } + + // ====================================================== ACL ===================================================== + + struct WorkerAclTarget {} + + /// Add or remove a sender from the worker allowlist + /// If the allowlist is empty, any sender, except those on the denylist, are allowed + /// Once there is at least one sender on the allowlist, only those on the allowlist are allowed, minus any that are + /// also on the denylist + public fun set_allowlist( + call_ref: &CallRef, + sender: address, + allowed: bool, + ) { + let worker = get_call_ref_caller(call_ref); + if (allowed) { + worker_config_store::add_to_allowlist(worker, sender); + } else { + worker_config_store::remove_from_allowlist(worker, sender); + }; + emit(SetAllowList { worker, sender, allowed }); + } + + /// Add or remove a sender from the worker denylist + /// Any sender on the denylist will not be allowed, regardless of whether they are also on the allowlist + public fun set_denylist(call_ref: &CallRef, sender: address, denied: bool) { + let worker = get_call_ref_caller(call_ref); + if (denied) { + worker_config_store::add_to_denylist(worker, sender); + } else { + worker_config_store::remove_from_denylist(worker, sender); + }; + emit(SetDenyList { worker, sender, denied }); + } + + #[view] + /// Check if a sender is allowed to use the worker based on the allowlist and denylist configuration + public fun is_allowed(worker: address, sender: address): bool { + if (worker_config_store::is_on_denylist(worker, sender)) { + false + } else if (worker_config_store::is_on_allowlist(worker, sender)) { + true + } else { + // if there is no allow list, an unlisted sender is allowed, otherwise they must be on the allow list + !worker_config_store::has_allowlist(worker) + } + } + + #[view] + /// Check if a sender is on the worker allowlist + public fun allowlist_contains(worker: address, sender: address): bool { + worker_config_store::is_on_allowlist(worker, sender) + } + + #[view] + /// Check if a sender is on the worker denylist + public fun denylist_contains(worker: address, sender: address): bool { + worker_config_store::is_on_denylist(worker, sender) + } + + /// Assert that the sender is allowed to use the worker + public fun assert_allowed(worker: address, sender: address) { + assert!(is_allowed(worker, sender), ESENDER_DENIED); + } + + // ==================================================== Events ==================================================== + + #[event] + /// Event emitted when the worker admin status is set + struct SetWorkerAdmin has store, drop { worker: address, admin: address, active: bool } + + #[event] + /// Event emitted when the worker role admin status is set + struct SetWorkerRoleAdmin has store, drop { worker: address, role_admin: address, active: bool } + + #[event] + /// Event emitted when the worker deposit address is set + struct SetDepositAddress has store, drop { worker: address, deposit_address: address } + + #[event] + /// Event emitted when the worker is paused + struct Paused has store, drop { worker: address } + + #[event] + /// Event emitted when the worker is unpaused + struct Unpaused has store, drop { worker: address } + + #[event] + /// Event emitted when the worker supported message libraries are set + struct SetSupportedMsglibs has store, drop { worker: address, msglibs: vector
} + + #[event] + /// Event emitted when the worker price feed is set + struct SetPriceFeed has store, drop { worker: address, price_feed: address, feed_address: address } + + #[event] + /// Event emitted when the worker price feed delegate is set + struct SetPriceFeedDelegate has store, drop { worker: address, delegate: address } + + #[event] + /// Event emitted when the worker default multiplier is set + struct SetMultiplierBps has store, drop { worker: address, default_multiplier_bps: u16 } + + #[event] + /// Event emitted when the worker supported option types are set + struct SetSupportedOptionTypes has store, drop { worker: address, option_types: vector } + + #[event] + /// Event emitted when the worker executor destination config is set + struct SetExecutorDstConfig has store, drop { + worker: address, + dst_eid: u32, + lz_receive_base_gas: u64, + multiplier_bps: u16, + floor_margin_usd: u128, + native_cap: u128, + lz_compose_base_gas: u64, + } + + #[event] + /// Event emitted when the worker DVN destination config is set + struct SetDvnDstConfig has store, drop { + worker: address, + dst_eid: u32, + gas: u64, + multiplier_bps: u16, + floor_margin_usd: u128, + } + + #[event] + /// Event emitted when worker adds/removes an oapp sender to allowlist + /// allowed = false means the sender is removed from the allowlist + struct SetAllowList has store, drop { worker: address, sender: address, allowed: bool } + + #[event] + /// Event emitted when the worker DVN destination config is set + /// denied = false means the sender is removed from the denylist + struct SetDenyList has store, drop { worker: address, sender: address, denied: bool } + + #[event] + /// Event emitted when the worker fee lib is set + struct WorkerFeeLibUpdated has store, drop { worker: address, fee_lib: address } + + // ============================================== Event Test Helpers ============================================== + + #[test_only] + /// Generate a SetWorkerAdmin event for testing + public fun set_worker_admin_event(worker: address, admin: address, active: bool): SetWorkerAdmin { + SetWorkerAdmin { worker, admin, active } + } + + #[test_only] + /// Generate a SetWorkerRoleAdmin event for testing + public fun set_worker_role_admin_event(worker: address, role_admin: address, active: bool): SetWorkerRoleAdmin { + SetWorkerRoleAdmin { worker, role_admin, active } + } + + #[test_only] + /// Generate a SetDepositAddress event for testing + public fun set_deposit_address_event(worker: address, deposit_address: address): SetDepositAddress { + SetDepositAddress { worker, deposit_address } + } + + #[test_only] + /// Generate a Paused event for testing + public fun paused_event(worker: address): Paused { + Paused { worker } + } + + #[test_only] + /// Generate a Unpaused event for testing + public fun unpaused_event(worker: address): Unpaused { + Unpaused { worker } + } + + #[test_only] + /// Generate a SetSupportedMsglibs event for testing + public fun set_supported_msglibs_event(worker: address, msglibs: vector
): SetSupportedMsglibs { + SetSupportedMsglibs { worker, msglibs } + } + + #[test_only] + /// Generate a SetPriceFeed event for testing + public fun set_price_feed_event(worker: address, price_feed: address, feed_address: address): SetPriceFeed { + SetPriceFeed { worker, price_feed, feed_address } + } + + #[test_only] + /// Generate a SetPriceFeedDelegate event for testing + public fun set_price_feed_delegate_event(worker: address, delegate: address): SetPriceFeedDelegate { + SetPriceFeedDelegate { worker, delegate } + } + + #[test_only] + /// Generate a SetMultiplierBps event for testing + public fun set_multiplier_bps_event(worker: address, default_multiplier_bps: u16): SetMultiplierBps { + SetMultiplierBps { worker, default_multiplier_bps } + } + + #[test_only] + /// Generate a SetSupportedOptionTypes event for testing + public fun set_supported_option_types_event(worker: address, option_types: vector): SetSupportedOptionTypes { + SetSupportedOptionTypes { worker, option_types } + } + + #[test_only] + /// Generate a SetExecutorDstConfig event for testing + public fun set_executor_dst_config_event( + worker: address, + dst_eid: u32, + lz_receive_base_gas: u64, + multiplier_bps: u16, + floor_margin_usd: u128, + native_cap: u128, + lz_compose_base_gas: u64, + ): SetExecutorDstConfig { + SetExecutorDstConfig { + worker, + dst_eid, + lz_receive_base_gas, + multiplier_bps, + floor_margin_usd, + native_cap, + lz_compose_base_gas, + } + } + + #[test_only] + /// Generate a SetDvnDstConfig event for testing + public fun set_dvn_dst_config_event( + worker: address, + dst_eid: u32, + gas: u64, + multiplier_bps: u16, + floor_margin_usd: u128, + ): SetDvnDstConfig { + SetDvnDstConfig { + worker, + dst_eid, + gas, + multiplier_bps, + floor_margin_usd, + } + } + + #[test_only] + /// Generate a WorkerFeeLibUpdated event for testing + public fun worker_fee_lib_updated_event(worker: address, fee_lib: address): WorkerFeeLibUpdated { + WorkerFeeLibUpdated { worker, fee_lib } + } + + // ================================================== Error Codes ================================================= + + const EDELEGATE_FEED_ADDRESS_NOT_CONFIGURED: u64 = 1; + const EDELEGATE_PRICE_FEED_NOT_CONFIGURED: u64 = 2; + const ENOT_AN_ACCOUNT: u64 = 3; + const ENO_DELEGATE_TO_UNSET: u64 = 4; + const EPAUSE_STATUS_UNCHANGED: u64 = 5; + const ESENDER_DENIED: u64 = 6; + const EUNAUTHORIZED: u64 = 7; + const EUNEXPECTED_WORKER_ID: u64 = 8; + const EWORKER_AUTH_UNSUPPORTED_MSGLIB: u64 = 9; + const EWORKER_PAUSED: u64 = 10; + const EWORKER_PRICE_FEED_DELEGATE_NOT_CONFIGURED: u64 = 11; + const EWORKER_PRICE_FEED_NOT_CONFIGURED: u64 = 12; +} diff --git a/packages/layerzero-v2/initia/contracts/worker_peripherals/worker_common/tests/internal/signing_store_tests.move b/packages/layerzero-v2/initia/contracts/worker_peripherals/worker_common/tests/internal/signing_store_tests.move new file mode 100644 index 00000000..6795fd8c --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/worker_peripherals/worker_common/tests/internal/signing_store_tests.move @@ -0,0 +1,142 @@ +#[test_only] +module worker_common::signing_store_tests { + use std::account::create_signer_for_test; + use std::vector; + + use endpoint_v2_common::bytes32::Bytes32; + use endpoint_v2_common::serde::{Self, flatten}; + use worker_common::multisig_store::{ + assert_not_expired, + assert_signatures_verified_internal, + get_pubkey, + split_signatures, + }; + + const DVN_WORKER_ID: u8 = 2; + + #[test] + public fun test_assert_not_expired() { + let native_framework = &create_signer_for_test(@std); + std::timestamp::set_time_has_started_for_testing(native_framework); + std::timestamp::update_global_time_for_test_secs(1000); + + assert_not_expired(1000000); + assert_not_expired(1500); + assert_not_expired(1001); + } + + #[test] + #[expected_failure(abort_code = worker_common::multisig_store::EEXPIRED_SIGNATURE)] + public fun test_assert_not_expired_fails_if_expired() { + let native_framework = &create_signer_for_test(@std); + std::timestamp::set_time_has_started_for_testing(native_framework); + std::timestamp::update_global_time_for_test_secs(1000); + + assert_not_expired(1000); + } + + #[test] + fun test_get_pubkey() { + let expiration = 1677465966; + let expected_pubkey = x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a"; + let signature_with_recovery = x"ee6c9646b2c55672734f06acb7347548f605046adcdf9ff080287ed0699779f6246c167cd30c6630cf5aa2cb157398b7b74237b18415cbd2e66bc0b2bff08f1a00"; + let quorum = 2; + let hash = create_set_quorum_hash(quorum, expiration); + let generated_pubkey = get_pubkey(&signature_with_recovery, hash); + assert!(generated_pubkey == expected_pubkey, 0); + } + + + #[test] + fun test_assert_signatures_verified_internal() { + let expiration = 1677465966; + let pubkey1 = x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a"; + let pubkey2 = x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d"; + let pubkey3 = x"37bdab42a45e9d6cc56f7d0cc7897e871a0357bce3f0f4c99c93c54291b259a29a92111167a25ae188ef49b2f3df880d8aae8522e29cb6c299745258a200cfff"; + let dvn_signers = &mut vector>[pubkey1, pubkey2, pubkey3]; + + let signature1 = x"ee6c9646b2c55672734f06acb7347548f605046adcdf9ff080287ed0699779f6246c167cd30c6630cf5aa2cb157398b7b74237b18415cbd2e66bc0b2bff08f1a00"; + let signature2 = x"b3d37d05832d808934f88e3f53ff2002e71125031543806964c0b1537f3abb694593f3b6b49f87caa1ca96e0cb955c24cb0865be8fc331b44a0afaf95480031f01"; + let signature3 = x"535e6f18117f1940ba3afef15d72dfc28cd0ab88ffa6d276c04c9d639f744352224f1c5a52046ef3962d444d15c6462dfe123665586e704b74704be22bcc8e1c00"; + let signatures = vector[signature1, signature2, signature3]; + + let quorum = 2; + let hash = create_set_quorum_hash(quorum, expiration); + assert_signatures_verified_internal(&signatures, hash, dvn_signers, quorum); + } + + #[test] + #[expected_failure(abort_code = worker_common::multisig_store::EDVN_LESS_THAN_QUORUM)] + fun test_assert_signatures_verified_internal_fails_if_quorum_not_met() { + let expiration = 1677465966; + let pubkey1 = x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a"; + let pubkey2 = x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d"; + let pubkey3 = x"37bdab42a45e9d6cc56f7d0cc7897e871a0357bce3f0f4c99c93c54291b259a29a92111167a25ae188ef49b2f3df880d8aae8522e29cb6c299745258a200cfff"; + let dvn_signers = &mut vector[pubkey1, pubkey2, pubkey3]; + + let signature1 = x"ee6c9646b2c55672734f06acb7347548f605046adcdf9ff080287ed0699779f6246c167cd30c6630cf5aa2cb157398b7b74237b18415cbd2e66bc0b2bff08f1a00"; + let signatures = vector[signature1]; + + let quorum = 2; + let hash = create_set_quorum_hash(quorum, expiration); + assert_signatures_verified_internal(&signatures, hash, dvn_signers, quorum); + } + + #[test] + #[expected_failure(abort_code = worker_common::multisig_store::EDVN_INCORRECT_SIGNATURE)] + fun test_assert_signatures_verified_internal_fails_if_any_signature_is_invalid() { + let expiration = 1677465966; + let pubkey1 = x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a"; + let pubkey2 = x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d"; + let pubkey3 = x"37bdab42a45e9d6cc56f7d0cc7897e871a0357bce3f0f4c99c93c54291b259a29a92111167a25ae188ef49b2f3df880d8aae8522e29cb6c299745258a200cfff"; + let dvn_signers = &mut vector[pubkey1, pubkey2, pubkey3]; + + let signature1 = x"ee6c9646b2c55672734f06acb7347548f605046adcdf9ff080287ed0699779f6246c167cd30c6630cf5aa2cb157398b7b74237b18415cbd2e66bc0b2bff08f1a00"; + let signature2 = x"aaaa7d05832d808934f88e3f53ff2002e71125031543806964c0b1537f3abb694593f3b6b49f87caa1ca96e0cb955c24cb0865be8fc331b44a0afaf95480031f01"; // invalid + let signature3 = x"535e6f18117f1940ba3afef15d72dfc28cd0ab88ffa6d276c04c9d639f744352224f1c5a52046ef3962d444d15c6462dfe123665586e704b74704be22bcc8e1c00"; + let signatures = vector[signature1, signature2, signature3]; + + let quorum = 2; + let hash = create_set_quorum_hash(quorum, expiration); + assert_signatures_verified_internal(&signatures, hash, dvn_signers, quorum); + } + + #[test] + fun test_split_and_join_signatures() { + let signature1 = x"ee6c9646b2c55672734f06acb7347548f605046adcdf9ff080287ed0699779f6246c167cd30c6630cf5aa2cb157398b7b74237b18415cbd2e66bc0b2bff08f1a00"; + let signature2 = x"b3d37d05832d808934f88e3f53ff2002e71125031543806964c0b1537f3abb694593f3b6b49f87caa1ca96e0cb955c24cb0865be8fc331b44a0afaf95480031f01"; + let signature3 = x"535e6f18117f1940ba3afef15d72dfc28cd0ab88ffa6d276c04c9d639f744352224f1c5a52046ef3962d444d15c6462dfe123665586e704b74704be22bcc8e1c00"; + let joined = flatten(vector[signature1, signature2, signature3]); + + let expected = vector[signature1, signature2, signature3]; + assert!(split_signatures(&joined) == expected, 0); + } + + + #[test] + #[expected_failure(abort_code = worker_common::multisig_store::EINVALID_SIGNATURE_LENGTH)] + fun test_split_signatures_fails_if_invalid_length() { + // 64 bytes instead of 65 + let signatures = x"ee6c9646b2c55672734f06acb7347548f605046adcdf9ff080287ed0699779f6246c167cd30c6630cf5aa2cb157398b7b74237b18415cbd2e66bc0b2bff08f1a"; + split_signatures(&signatures); + } + + + public fun create_set_quorum_hash(quorum: u64, expiration: u64): Bytes32 { + endpoint_v2_common::bytes32::keccak256( + build_set_quorum_payload(quorum, expiration) + ) + } + + fun build_set_quorum_payload(quorum: u64, expiration: u64): vector { + let payload = vector[]; + serde::append_bytes(&mut payload, get_function_signature(b"set_quorum")); + serde::append_u64(&mut payload, quorum); + serde::append_u64(&mut payload, expiration); + payload + } + + fun get_function_signature(function_name: vector): vector { + vector::slice(&std::aptos_hash::keccak256(std::bcs::to_bytes(&function_name)), 0, 4) + } +} diff --git a/packages/layerzero-v2/initia/contracts/worker_peripherals/worker_common/tests/internal/worker_config_store_tests.move b/packages/layerzero-v2/initia/contracts/worker_peripherals/worker_common/tests/internal/worker_config_store_tests.move new file mode 100644 index 00000000..b4416c87 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/worker_peripherals/worker_common/tests/internal/worker_config_store_tests.move @@ -0,0 +1,37 @@ +#[test_only] +module worker_common::worker_config_store_tests { + use std::account::create_signer_for_test; + + use worker_common::worker_config_store; + + #[test] + #[expected_failure(abort_code = worker_common::worker_config_store::ENO_ADMINS_PROVIDED)] + fun test_initialize_for_worker_fails_if_no_admins_provided() { + let account = &create_signer_for_test(@0x1111); + worker_config_store::initialize_store_for_worker(account, 1, @0x1111, @0xdefad, vector[], vector[], @0xfee11b); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config_store::EWORKER_ALREADY_INITIALIZED)] + fun test_initialize_for_worker_fails_if_already_initialized() { + let account = &create_signer_for_test(@0x1111); + worker_config_store::initialize_store_for_worker( + account, + 1, + @0xdefad, + @0x1111, + vector[@100], + vector[], + @0xfee11b, + ); + worker_config_store::initialize_store_for_worker( + account, + 1, + @0xdefad, + @0x1111, + vector[@200], + vector[], + @0xfee11b, + ); + } +} diff --git a/packages/layerzero-v2/initia/contracts/worker_peripherals/worker_common/tests/multisig_tests.move b/packages/layerzero-v2/initia/contracts/worker_peripherals/worker_common/tests/multisig_tests.move new file mode 100644 index 00000000..c2751750 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/worker_peripherals/worker_common/tests/multisig_tests.move @@ -0,0 +1,246 @@ +#[test_only] +module worker_common::multisig_tests { + use std::event::was_event_emitted; + + use endpoint_v2_common::contract_identity::make_call_ref_for_test; + use worker_common::multisig::{ + get_quorum, + is_signer, + set_quorum, + set_signer, + set_signers, + }; + use worker_common::multisig::{ + initialize_for_worker_test_only as initialize_multisig, set_quorum_event, set_signers_event, + update_signer_event, + }; + use worker_common::worker_config::{initialize_for_worker_test_only as initialize_worker, WORKER_ID_DVN}; + + const WORKER: address = @123456; + + #[test] + fun test_set_and_get_quorum() { + initialize_worker(WORKER, WORKER_ID_DVN(), WORKER, @0x501ead, vector[@123], vector[], @0xfee11b); + + let signers = vector[ + x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a", + x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d" + ]; + initialize_multisig(WORKER, 1, signers); + assert!(was_event_emitted(&set_signers_event(WORKER, signers)), 0); + assert!(was_event_emitted(&set_quorum_event(WORKER, 1)), 0); + assert!(get_quorum(WORKER) == 1, 0); + set_quorum(&make_call_ref_for_test(WORKER), 2); + assert!(was_event_emitted(&set_quorum_event(WORKER, 2)), 0); + assert!(get_quorum(WORKER) == 2, 0); + } + + #[test] + #[expected_failure(abort_code = worker_common::multisig_store::ESIGNERS_LESS_THAN_QUORUM)] + fun test_initialize_fails_if_signers_less_than_quorum() { + initialize_worker(WORKER, WORKER_ID_DVN(), WORKER, @0x501ead, vector[@123], vector[], @0xfee11b); + + let signers = vector[ + x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a", + x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d" + ]; + let quorum = 3; + initialize_multisig(WORKER, quorum, signers); + } + + #[test] + #[expected_failure(abort_code = worker_common::multisig_store::ESIGNERS_LESS_THAN_QUORUM)] + fun test_set_quorum_fails_if_signers_less_than_quorum() { + initialize_worker(WORKER, WORKER_ID_DVN(), WORKER, @0x501ead, vector[@123], vector[], @0xfee11b); + + let signers = vector[ + x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a", + x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d" + ]; + let quorum = 1; + initialize_multisig(WORKER, quorum, signers); + set_quorum(&make_call_ref_for_test(WORKER), 3); + } + + #[test] + #[expected_failure(abort_code = worker_common::multisig_store::EZERO_QUORUM)] + fun test_set_quorum_fails_if_zero() { + initialize_worker(WORKER, WORKER_ID_DVN(), WORKER, @0x501ead, vector[@123], vector[], @0xfee11b); + + let signers = vector[ + x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a", + x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d" + ]; + let quorum = 1; + initialize_multisig(WORKER, quorum, signers); + set_quorum(&make_call_ref_for_test(WORKER), 0); + } + + #[test] + fun test_set_signers() { + initialize_worker(WORKER, WORKER_ID_DVN(), WORKER, @0x501ead, vector[@123], vector[], @0xfee11b); + let signers = vector[ + x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a", + ]; + initialize_multisig(WORKER, 1, signers); + assert!(was_event_emitted(&set_signers_event(WORKER, signers)), 0); + assert!(was_event_emitted(&set_quorum_event(WORKER, 1)), 0); + let signers = vector[ + x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a", + x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d" + ]; + set_signers(&make_call_ref_for_test(WORKER), signers); + assert!(was_event_emitted(&set_signers_event(WORKER, signers)), 0); + assert!( + is_signer( + WORKER, + x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a" + ), + 0, + ); + assert!( + is_signer( + WORKER, + x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d" + ), + 1, + ); + } + + #[test] + #[expected_failure(abort_code = endpoint_v2_common::assert_no_duplicates::EDUPLICATE_ITEM)] + fun test_initialize_multisig_fails_if_duplicate_item() { + initialize_worker(WORKER, WORKER_ID_DVN(), WORKER, @0x501ead, vector[@123], vector[], @0xfee11b); + let signers = vector[ + x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a", + x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d", + x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a", + ]; + initialize_multisig(WORKER, 1, signers); + } + + #[test] + #[expected_failure(abort_code = worker_common::multisig_store::EINVALID_SIGNER_LENGTH)] + fun test_initialize_fails_if_invalid_length() { + initialize_worker(WORKER, WORKER_ID_DVN(), WORKER, @0x501ead, vector[@123], vector[], @0xfee11b); + let signers = vector[ + x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a", + x"1234567890", // invalid + x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d", + ]; + initialize_multisig(WORKER, 1, signers); + } + + #[test] + fun test_set_signer() { + initialize_worker(WORKER, WORKER_ID_DVN(), WORKER, @0x501ead, vector[@123], vector[], @0xfee11b); + let signers = vector[ + x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a", + x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d" + ]; + initialize_multisig(WORKER, 1, signers); + + + // turn second signer on and off + assert!( + is_signer( + WORKER, + x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d" + ), + 0, + ); + set_signer( + &make_call_ref_for_test(WORKER), + x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d", + false, + ); + assert!( + was_event_emitted( + &update_signer_event( + WORKER, + x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d", + false, + ) + ), + 0, + ); + assert!( + !is_signer( + WORKER, + x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d" + ), + 0, + ); + set_signer( + &make_call_ref_for_test(WORKER), + x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d", + true, + ); + assert!( + was_event_emitted( + &update_signer_event( + WORKER, + x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d", + true, + ) + ), + 1, + ); + assert!( + is_signer( + WORKER, + x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d" + ), + 0, + ); + } + + #[test] + #[expected_failure(abort_code = worker_common::multisig_store::ESIGNERS_LESS_THAN_QUORUM)] + fun test_set_signer_fails_if_less_than_quorum() { + initialize_worker(WORKER, WORKER_ID_DVN(), WORKER, @0x501ead, vector[@123], vector[], @0xfee11b); + let signers = vector[ + x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a", + x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d" + ]; + initialize_multisig(WORKER, 2, signers); + // try to turn second signer off: fails + assert!( + is_signer( + WORKER, + x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d" + ), + 0, + ); + set_signer( + &make_call_ref_for_test(WORKER), + x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d", + false, + ); + } + + #[test] + #[expected_failure(abort_code = worker_common::multisig_store::EINVALID_SIGNER_LENGTH)] + fun test_set_signer_fails_if_incorrect_length() { + initialize_worker(WORKER, WORKER_ID_DVN(), WORKER, @0x501ead, vector[@123], vector[], @0xfee11b); + let signers = vector[ + x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a", + x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d" + ]; + initialize_multisig(WORKER, 2, signers); + set_signer(&make_call_ref_for_test(WORKER), x"1234567890", true); + } + + #[test] + #[expected_failure(abort_code = worker_common::multisig_store::ESIGNER_ALREADY_EXISTS)] + fun test_set_signer_fails_if_assigned_twice() { + initialize_worker(WORKER, WORKER_ID_DVN(), WORKER, @0x501ead, vector[@123], vector[], @0xfee11b); + let signers = vector[ + x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a", + ]; + initialize_multisig(WORKER, 1, signers); + let signer = x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d"; + set_signer(&make_call_ref_for_test(WORKER), signer, true); + set_signer(&make_call_ref_for_test(WORKER), signer, true); + } +} diff --git a/packages/layerzero-v2/initia/contracts/worker_peripherals/worker_common/tests/worker_config_tests.move b/packages/layerzero-v2/initia/contracts/worker_peripherals/worker_common/tests/worker_config_tests.move new file mode 100644 index 00000000..8261bbc5 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/worker_peripherals/worker_common/tests/worker_config_tests.move @@ -0,0 +1,807 @@ +#[test_only] +module worker_common::worker_config_tests { + use std::account; + use std::event::was_event_emitted; + use std::vector; + + use endpoint_v2_common::contract_identity::make_call_ref_for_test; + use worker_common::worker_config::{ + Self, + allowlist_contains, + assert_allowed, + assert_supported_msglib, + denylist_contains, + is_allowed, + set_allowlist, + set_denylist, + WORKER_ID_EXECUTOR, + }; + + const WORKER: address = @123456; + const DVN_WORKER_ID: u8 = 2; + + #[test] + fun test_set_worker_pause() { + let worker_address = @3001; + worker_common::worker_config::initialize_for_worker_test_only( + worker_address, + WORKER_ID_EXECUTOR(), + worker_address, + @0x501ead, + vector[@111], + vector[], + @0xfee11b1, + ); + // default state is unpaused + assert!(!worker_config::is_worker_paused(worker_address), 0); + + assert!(!worker_config::is_worker_paused(worker_address), 0); + worker_config::set_worker_pause(&make_call_ref_for_test(worker_address), true); + assert!(was_event_emitted(&worker_config::paused_event(worker_address)), 0); + assert!(!was_event_emitted(&worker_config::unpaused_event(worker_address)), 0); + assert!(worker_config::is_worker_paused(worker_address), 0); + + worker_config::set_worker_pause(&make_call_ref_for_test(worker_address), false); + assert!(was_event_emitted(&worker_config::unpaused_event(worker_address)), 0); + assert!(!worker_config::is_worker_paused(worker_address), 0); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::EPAUSE_STATUS_UNCHANGED)] + fun test_set_pause_fails_if_no_state_change() { + let worker_address = @3001; + worker_common::worker_config::initialize_for_worker_test_only( + worker_address, + WORKER_ID_EXECUTOR(), + worker_address, + @0x501ead, + vector[@111], + vector[], + @0xfee11b1, + ); + worker_config::set_worker_pause(&make_call_ref_for_test(worker_address), true); + // fails on state change to the prior value + worker_config::set_worker_pause(&make_call_ref_for_test(worker_address), true); + } + + #[test] + fun test_set_and_get_supported_msglibs() { + let worker_address = @3001; + let msglib1 = @1234; + let msglib2 = @2345; + let msglib3 = @3456; + worker_common::worker_config::initialize_for_worker_test_only( + worker_address, + WORKER_ID_EXECUTOR(), + worker_address, + @0x501ead, + vector[@111], + vector[msglib1, msglib2], + @0xfee11b1, + ); + + let supported_msglibs = worker_config::get_supported_msglibs(worker_address); + assert!(vector::contains(&supported_msglibs, &msglib1), 0); + assert!(vector::contains(&supported_msglibs, &msglib2), 0); + assert!(!vector::contains(&supported_msglibs, &msglib3), 0); + assert_supported_msglib(worker_address, msglib1); + assert_supported_msglib(worker_address, msglib2); + + worker_config::set_supported_msglibs(&make_call_ref_for_test(worker_address), vector[msglib1, msglib3]); + let supported_msglibs = worker_config::get_supported_msglibs(worker_address); + assert!(vector::contains(&supported_msglibs, &msglib1), 0); + assert!(!vector::contains(&supported_msglibs, &msglib2), 0); + assert!(vector::contains(&supported_msglibs, &msglib3), 0); + assert_supported_msglib(worker_address, msglib1); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::EWORKER_AUTH_UNSUPPORTED_MSGLIB)] + fun test_assert_supported_msglib_fails_if_not_supported() { + let worker_address = @3001; + let msglib1 = @1234; + let msglib2 = @2345; + worker_common::worker_config::initialize_for_worker_test_only( + worker_address, + WORKER_ID_EXECUTOR(), + worker_address, + @0x501ead, + vector[@111], + vector[msglib1], + @0xfee11b1, + ); + assert_supported_msglib(worker_address, msglib2); + } + + #[test] + fun test_get_and_set_deposit_address() { + let worker_address = @3001; + let deposit_address = @1234; + account::create_account_for_test(deposit_address); + + worker_common::worker_config::initialize_for_worker_test_only( + worker_address, + WORKER_ID_EXECUTOR(), + worker_address, + @0x501ead, + vector[@111], + vector[], + @0xfee11b1, + ); + // initializes to worker address + assert!(worker_config::get_deposit_address(worker_address) == worker_address, 0); + + worker_config::set_deposit_address(&make_call_ref_for_test(worker_address), deposit_address); + assert!(was_event_emitted(&worker_config::set_deposit_address_event(worker_address, deposit_address)), 0); + let deposit_address_result = worker_config::get_deposit_address(worker_address); + assert!(deposit_address == deposit_address_result, 0); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::ENOT_AN_ACCOUNT)] + fun set_deposit_address_fails_if_invalid_account() { + let worker_address = @3001; + let deposit_address = @1234; + worker_common::worker_config::initialize_for_worker_test_only( + worker_address, + WORKER_ID_EXECUTOR(), + worker_address, + @0x501ead, + vector[@111], + vector[], + @0xfee11b1, + ); + + // Attempt to set deposit address to an invalid account (expected failure) + worker_config::set_deposit_address(&make_call_ref_for_test(worker_address), deposit_address); + } + + #[test] + fun test_set_and_get_price_feed() { + let worker_address = @3001; + let price_feed = @1234; + let feed_address = @2345; + worker_common::worker_config::initialize_for_worker_test_only( + worker_address, + WORKER_ID_EXECUTOR(), + worker_address, + @0x501ead, + vector[@111], + vector[], + @0xfee11b1, + ); + + worker_config::set_price_feed(&make_call_ref_for_test(worker_address), price_feed, feed_address); + assert!(was_event_emitted(&worker_config::set_price_feed_event(worker_address, price_feed, feed_address)), 0); + let (price_feed_result, feed_address_result) = worker_config::get_effective_price_feed(worker_address); + assert!(price_feed == price_feed_result, 0); + assert!(feed_address == feed_address_result, 0); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::EWORKER_PRICE_FEED_NOT_CONFIGURED)] + fun test_get_effective_price_feed_fails_if_not_configured() { + let worker_address = @3001; + worker_common::worker_config::initialize_for_worker_test_only( + worker_address, + WORKER_ID_EXECUTOR(), + worker_address, + @0x501ead, + vector[@111], + vector[], + @0xfee11b1, + ); + + worker_config::get_effective_price_feed(worker_address); + } + + + #[test] + fun test_set_and_get_price_feed_delegate() { + // register worker + let first_worker_address = @3001; + worker_common::worker_config::initialize_for_worker_test_only( + first_worker_address, + WORKER_ID_EXECUTOR(), + first_worker_address, + @0x501ead, + vector[@111], + vector[], + @0xfee11b1, + ); + + // register delegate worker + let second_worker_address = @3002; + worker_common::worker_config::initialize_for_worker_test_only( + second_worker_address, + WORKER_ID_EXECUTOR(), + second_worker_address, + @0x501ead, + vector[@111], + vector[], + @0xfee11b1, + ); + + // register third worker + let third_worker_address = @3003; + worker_common::worker_config::initialize_for_worker_test_only( + third_worker_address, + WORKER_ID_EXECUTOR(), + third_worker_address, + @0x501ead, + vector[@111], + vector[], + @0xfee11b1, + ); + + // set price feed for second worker + worker_config::set_price_feed(&make_call_ref_for_test(second_worker_address), @10002, @20002); + assert!(was_event_emitted(&worker_config::set_price_feed_event(second_worker_address, @10002, @20002)), 0); + let (price_feed_result, feed_address_result) = worker_config::get_effective_price_feed(second_worker_address); + assert!(price_feed_result == @10002, 0); + assert!(feed_address_result == @20002, 0); + + // delegate for the first worker not yet set + let delegate_result = worker_config::get_price_feed_delegate(first_worker_address); + assert!(delegate_result == @0x0, 0); + let delegate_count = worker_config::get_count_price_feed_delegate_dependents(second_worker_address); + assert!(delegate_count == 0, 0); + + // set delegate for first worker + worker_config::set_price_feed_delegate(&make_call_ref_for_test(first_worker_address), second_worker_address); + assert!( + was_event_emitted( + &worker_config::set_price_feed_delegate_event(first_worker_address, second_worker_address) + ), + 0, + ); + let delegate_result = worker_config::get_price_feed_delegate(first_worker_address); + assert!(delegate_result == second_worker_address, 0); + let delegate_count = worker_config::get_count_price_feed_delegate_dependents(second_worker_address); + assert!(delegate_count == 1, 0); + + let (price_feed_result, feed_address_result) = worker_config::get_effective_price_feed(first_worker_address); + assert!(price_feed_result == @10002, 0); + assert!(feed_address_result == @20002, 0); + + // set the price feed for the first worker (should not override the delegate configuration) + worker_config::set_price_feed(&make_call_ref_for_test(first_worker_address), @10001, @20001); + assert!(was_event_emitted(&worker_config::set_price_feed_event(first_worker_address, @10001, @20001)), 0); + let (price_feed_result, feed_address_result) = worker_config::get_effective_price_feed(first_worker_address); + assert!(price_feed_result == @10002, 0); + assert!(feed_address_result == @20002, 0); + + // have the third worker delegate to the first worker + // the effective price feed should be the price feed of the first worker, not the price feed of the first + // worker's delegate (no delegate chaining) + worker_config::set_price_feed_delegate(&make_call_ref_for_test(third_worker_address), first_worker_address); + assert!( + was_event_emitted( + &worker_config::set_price_feed_delegate_event(third_worker_address, first_worker_address) + ), + 0, + ); + let delegate_result = worker_config::get_price_feed_delegate(third_worker_address); + assert!(delegate_result == first_worker_address, 0); + + let first_worker_delegate_count = worker_config::get_count_price_feed_delegate_dependents(first_worker_address); + assert!(first_worker_delegate_count == 1, 0); + let second_worker_delegate_count = worker_config::get_count_price_feed_delegate_dependents( + second_worker_address, + ); + assert!(second_worker_delegate_count == 1, 0); + let third_worker_delegate_count = worker_config::get_count_price_feed_delegate_dependents(third_worker_address); + assert!(third_worker_delegate_count == 0, 0); + + let (price_feed_result, feed_address_result) = worker_config::get_effective_price_feed(third_worker_address); + assert!(price_feed_result == @10001, 0); + assert!(feed_address_result == @20001, 0); + + // Set the third worker to delegate to the second worker (which will then have 2 delegates) + worker_config::set_price_feed_delegate(&make_call_ref_for_test(third_worker_address), second_worker_address); + assert!( + was_event_emitted( + &worker_config::set_price_feed_delegate_event(third_worker_address, second_worker_address) + ), + 0, + ); + let delegate_result = worker_config::get_price_feed_delegate(third_worker_address); + assert!(delegate_result == second_worker_address, 0); + + let first_worker_delegate_count = worker_config::get_count_price_feed_delegate_dependents(first_worker_address); + assert!(first_worker_delegate_count == 0, 0); + let second_worker_delegate_count = worker_config::get_count_price_feed_delegate_dependents( + second_worker_address, + ); + assert!(second_worker_delegate_count == 2, 0); + let third_worker_delegate_count = worker_config::get_count_price_feed_delegate_dependents(third_worker_address); + assert!(third_worker_delegate_count == 0, 0); + + worker_config::set_price_feed(&make_call_ref_for_test(third_worker_address), @10003, @20003); + + // swap delegate of first to point to third (instead of second) + worker_config::set_price_feed_delegate(&make_call_ref_for_test(first_worker_address), third_worker_address); + assert!( + was_event_emitted( + &worker_config::set_price_feed_delegate_event(first_worker_address, third_worker_address) + ), + 0, + ); + let first_worker_delegate_count = worker_config::get_count_price_feed_delegate_dependents(first_worker_address); + assert!(first_worker_delegate_count == 0, 0); + let second_worker_delegate_count = worker_config::get_count_price_feed_delegate_dependents( + second_worker_address, + ); + assert!(second_worker_delegate_count == 1, 0); + let third_worker_delegate_count = worker_config::get_count_price_feed_delegate_dependents(third_worker_address); + assert!(third_worker_delegate_count == 1, 0); + + // remove the delegate + worker_config::set_price_feed_delegate(&make_call_ref_for_test(first_worker_address), @0x0); + assert!(was_event_emitted(&worker_config::set_price_feed_delegate_event(first_worker_address, @0x0)), 0); + let delegate_result = worker_config::get_price_feed_delegate(first_worker_address); + assert!(delegate_result == @0x0, 0); + + let first_worker_delegate_count = worker_config::get_count_price_feed_delegate_dependents(first_worker_address); + assert!(first_worker_delegate_count == 0, 0); + let second_worker_delegate_count = worker_config::get_count_price_feed_delegate_dependents( + second_worker_address, + ); + assert!(second_worker_delegate_count == 1, 0); + let third_worker_delegate_count = worker_config::get_count_price_feed_delegate_dependents(third_worker_address); + assert!(third_worker_delegate_count == 0, 0); + + // the effective price feed should be the price feed of the delegate worker should be unaffected + let (price_feed_result, feed_address_result) = worker_config::get_effective_price_feed(second_worker_address); + assert!(price_feed_result == @10002, 0); + assert!(feed_address_result == @20002, 0); + + // the effective price feed should be the price feed of the worker should revert to its own + let (price_feed_result, feed_address_result) = worker_config::get_effective_price_feed(first_worker_address); + assert!(price_feed_result == @10001, 0); + assert!(feed_address_result == @20001, 0); + + // the effective price feed should be the price feed of the send (delegate) worker should be unaffected + let (price_feed_result, feed_address_result) = worker_config::get_effective_price_feed(third_worker_address); + assert!(price_feed_result == @10002, 0); + assert!(feed_address_result == @20002, 0); + } + + + #[test] + fun test_is_worker_admin() { + let admins = vector[@100, @200, @300]; + worker_config::initialize_for_worker_test_only(@0x1111, 1, @0x1111, @0x501ead, admins, vector[], @0xfee11b); + assert!(worker_config::is_worker_admin(@0x1111, @100), 0); + assert!(worker_config::is_worker_admin(@0x1111, @200), 1); + assert!(worker_config::is_worker_admin(@0x1111, @300), 2); + + // Does not approve non-admin + assert!(!worker_config::is_worker_admin(@0x1111, @11), 3); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config_store::EWORKER_NOT_REGISTERED)] + fun test_is_worker_admin_fails_if_worker_not_initialized() { + // Initialize one workerworker_config_store + let admins = vector[@100, @200, @300]; + worker_config::initialize_for_worker_test_only(@0x1111, 1, @0x1111, @0x501ead, admins, vector[], @0xfee11b); + + // Attempt to check admin status of a different worker (expected failure) + worker_config::is_worker_admin(@1112, @100); + } + + #[test] + fun test_assert_worker_admin_succeeds_if_is_admin() { + let admins = vector[@100, @200, @300]; + worker_config::initialize_for_worker_test_only(@0x1111, 1, @0x1111, @0x501ead, admins, vector[], @0xfee11b); + worker_config::assert_worker_admin(@0x1111, @100); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::EUNAUTHORIZED)] + fun test_assert_worker_admin_fails_if_not_admin() { + let admins = vector[@100, @200, @300]; + worker_config::initialize_for_worker_test_only(@0x1111, 1, @0x1111, @0x501ead, admins, vector[], @0xfee11b); + worker_config::assert_worker_admin(@0x1111, @150); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config_store::EWORKER_NOT_REGISTERED)] + fun test_asset_worker_admin_fails_if_worker_not_registered() { + let admins = vector[@100, @200, @300]; + worker_config::initialize_for_worker_test_only(@0x1111, 1, @0x1111, @0x501ead, admins, vector[], @0xfee11b); + + // different worker + worker_config::assert_worker_admin(@2222, @100); + } + + #[test] + fun test_set_worker_admin_internal() { + let admins = vector[@100, @200, @300]; + worker_config::initialize_for_worker_test_only(@0x1111, 1, @0x1111, @0x501ead, admins, vector[], @0xfee11b); + + // Add new admin + worker_config::set_worker_admin(&make_call_ref_for_test(@0x1111), @400, true); + assert!(was_event_emitted(&worker_config::set_worker_admin_event(@0x1111, @400, true)), 0); + assert!(worker_config::is_worker_admin(@0x1111, @100), 0); + assert!(worker_config::is_worker_admin(@0x1111, @400), 0); + assert!(!worker_config::is_worker_admin(@0x1111, @500), 1); + + // Remove admin + worker_config::set_worker_admin(&make_call_ref_for_test(@0x1111), @400, false); + assert!(was_event_emitted(&worker_config::set_worker_admin_event(@0x1111, @400, false)), 0); + assert!(worker_config::is_worker_admin(@0x1111, @100), 0); + assert!(!worker_config::is_worker_admin(@0x1111, @400), 1); + assert!(!worker_config::is_worker_admin(@0x1111, @500), 1); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config_store::EADMIN_ALREADY_EXISTS)] + fun test_set_worker_admin_internal_fails_if_admin_already_exists() { + let admins = vector[@100, @200, @300]; + worker_config::initialize_for_worker_test_only(@0x1111, 1, @0x1111, @0x501ead, admins, vector[], @0xfee11b); + + // Add new admin + worker_config::set_worker_admin(&make_call_ref_for_test(@0x1111), @400, true); + assert!(worker_config::is_worker_admin(@0x1111, @400), 0); + + // Attempt to add the same admin again (expected failure) + worker_config::set_worker_admin(&make_call_ref_for_test(@0x1111), @400, true); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config_store::EADMIN_NOT_FOUND)] + fun test_set_worker_admin_internal_fails_to_remove_an_admin_if_admin_not_found() { + let admins = vector[@100, @200, @300]; + worker_config::initialize_for_worker_test_only(@0x1111, 1, @0x1111, @0x501ead, admins, vector[], @0xfee11b); + + // Attempt to remove non-existent admin (expected failure) + worker_config::set_worker_admin(&make_call_ref_for_test(@0x1111), @400, false); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config_store::EATTEMPING_TO_REMOVE_ONLY_ADMIN)] + fun test_set_worker_admin_internal_fails_to_remove_last_admin() { + let admins = vector[@100]; + worker_config::initialize_for_worker_test_only(@0x1111, 1, @0x1111, @0x501ead, admins, vector[], @0xfee11b); + + // Attempt to remove last admin (expected failure) + worker_config::set_worker_admin(&make_call_ref_for_test(@0x1111), @100, false); + } + + #[test] + fun test_set_worker_admin() { + let admins = vector[@100, @200, @300]; + worker_config::initialize_for_worker_test_only(@0x1111, 1, @0x1111, @0x501ead, admins, vector[], @0xfee11b); + assert!(worker_config::is_worker_admin(@0x1111, @100), 0); + assert!(!worker_config::is_worker_admin(@0x1111, @400), 0); + + // Add new admin + worker_config::set_worker_admin(&make_call_ref_for_test(@0x1111), @400, true); + assert!(worker_config::is_worker_admin(@0x1111, @100), 0); + assert!(worker_config::is_worker_admin(@0x1111, @400), 0); + assert!(!worker_config::is_worker_admin(@0x1111, @500), 1); + + // Remove admins + worker_config::set_worker_admin(&make_call_ref_for_test(@0x1111), @400, false); + worker_config::set_worker_admin(&make_call_ref_for_test(@0x1111), @100, false); + assert!(!worker_config::is_worker_admin(@0x1111, @100), 0); + assert!(!worker_config::is_worker_admin(@0x1111, @400), 1); + assert!(!worker_config::is_worker_admin(@0x1111, @500), 1); + + // Re-add admin + worker_config::set_worker_admin(&make_call_ref_for_test(@0x1111), @400, true); + assert!(!worker_config::is_worker_admin(@0x1111, @100), 0); + assert!(worker_config::is_worker_admin(@0x1111, @400), 0); + assert!(!worker_config::is_worker_admin(@0x1111, @500), 1); + } + + #[test] + fun test_set_worker_admin_with_call_ref() { + let admins = vector[@100, @200, @300]; + worker_config::initialize_for_worker_test_only(@0x1111, 1, @0x1111, @0x501ead, admins, vector[], @0xfee11b); + assert!(worker_config::is_worker_admin(@0x1111, @100), 0); + assert!(!worker_config::is_worker_admin(@0x1111, @400), 0); + + // Add new admin + worker_config::set_worker_admin(&make_call_ref_for_test(@0x1111), @400, true); + assert!(worker_config::is_worker_admin(@0x1111, @100), 0); + assert!(worker_config::is_worker_admin(@0x1111, @400), 0); + assert!(!worker_config::is_worker_admin(@0x1111, @500), 1); + + // Remove admins + worker_config::set_worker_admin(&make_call_ref_for_test(@0x1111), @400, false); + worker_config::set_worker_admin(&make_call_ref_for_test(@0x1111), @100, false); + assert!(!worker_config::is_worker_admin(@0x1111, @100), 0); + assert!(!worker_config::is_worker_admin(@0x1111, @400), 1); + assert!(!worker_config::is_worker_admin(@0x1111, @500), 1); + + // Readd admin + worker_config::set_worker_admin(&make_call_ref_for_test(@0x1111), @400, true); + assert!(!worker_config::is_worker_admin(@0x1111, @100), 0); + assert!(worker_config::is_worker_admin(@0x1111, @400), 0); + assert!(!worker_config::is_worker_admin(@0x1111, @500), 1); + } + + #[test] + fun test_set_worker_role_admin() { + let admins = vector[@100, @200, @300]; + worker_config::initialize_for_worker_test_only(@0x1111, 1, @0x1111, @0x501ead, admins, vector[], @0xfee11b); + assert!(!worker_config::is_worker_role_admin(@0x1111, @100), 0); + assert!(worker_config::is_worker_role_admin(@0x1111, @0x501ead), 0); + + // Add new admin + worker_config::set_worker_role_admin(&make_call_ref_for_test(@0x1111), @400, true); + assert!(worker_config::is_worker_role_admin(@0x1111, @400), 0); + assert!(worker_config::is_worker_role_admin(@0x1111, @0x501ead), 0); + + // Remove admins + worker_config::set_worker_role_admin(&make_call_ref_for_test(@0x1111), @400, false); + worker_config::set_worker_role_admin(&make_call_ref_for_test(@0x1111), @0x501ead, false); + assert!(!worker_config::is_worker_role_admin(@0x1111, @400), 0); + assert!(!worker_config::is_worker_role_admin(@0x1111, @0x501ead), 0); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config_store::EROLE_ADMIN_ALREADY_EXISTS)] + fun test_set_worker_role_admin_fails_if_admin_already_exists() { + let admins = vector[@100, @200, @300]; + worker_config::initialize_for_worker_test_only(@0x1111, 1, @0x1111, @0x501ead, admins, vector[], @0xfee11b); + + // Add new admin + worker_config::set_worker_role_admin(&make_call_ref_for_test(@0x1111), @0x501ead, true); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config_store::EROLE_ADMIN_NOT_FOUND)] + fun test_set_worker_role_admin_fails_to_remove_an_admin_if_admin_not_found() { + let admins = vector[@100, @200, @300]; + worker_config::initialize_for_worker_test_only(@0x1111, 1, @0x1111, @0x501ead, admins, vector[], @0xfee11b); + + // Attempt to remove non-existent admin (expected failure) + worker_config::set_worker_role_admin(&make_call_ref_for_test(@0x1111), @400, false); + } + + #[test] + fun test_set_allowlist() { + let worker_address = @3001; + worker_common::worker_config::initialize_for_worker_test_only( + worker_address, + WORKER_ID_EXECUTOR(), + worker_address, + @0x501ead, + vector[@1234, @2345], + vector[], + @0xfee11b1, + ); + let alice = @1122; + let bob = @3344; + let carol = @5566; + + // add alice and bob to the allow list + set_allowlist(&make_call_ref_for_test(worker_address), alice, true); + set_allowlist(&make_call_ref_for_test(worker_address), bob, true); + assert!(allowlist_contains(worker_address, alice), 0); + assert!(allowlist_contains(worker_address, bob), 0); + assert!(!allowlist_contains(worker_address, carol), 0); + + // remove alice from the allow list + set_allowlist(&make_call_ref_for_test(worker_address), alice, false); + assert!(!allowlist_contains(worker_address, alice), 0); + assert!(allowlist_contains(worker_address, bob), 0); + assert!(!allowlist_contains(worker_address, carol), 0); + } + + #[test] + fun test_set_denylist() { + let worker_address = @3001; + worker_common::worker_config::initialize_for_worker_test_only( + worker_address, + WORKER_ID_EXECUTOR(), + worker_address, + @0x501ead, + vector[@1234, @2345], + vector[], + @0xfee11b1, + ); + let alice = @1122; + let bob = @3344; + let carol = @5566; + + // add alice and bob to the deny list + set_denylist(&make_call_ref_for_test(worker_address), alice, true); + set_denylist(&make_call_ref_for_test(worker_address), bob, true); + assert!(denylist_contains(worker_address, alice), 0); + assert!(denylist_contains(worker_address, bob), 0); + assert!(!denylist_contains(worker_address, carol), 0); + + // remove alice from the deny list + set_denylist(&make_call_ref_for_test(worker_address), alice, false); + assert!(!denylist_contains(worker_address, alice), 0); + assert!(denylist_contains(worker_address, bob), 0); + assert!(!denylist_contains(worker_address, carol), 0); + } + + #[test] + fun test_is_allowed() { + let worker_address = @3001; + worker_common::worker_config::initialize_for_worker_test_only( + worker_address, + WORKER_ID_EXECUTOR(), + worker_address, + @0x501ead, + vector[@1234, @2345], + vector[], + @0xfee11b1, + ); + let alice = @1122; + let bob = @3344; + let carol = @5566; + + + // add carol to the deny list, then assert that alice and bob are allowed + set_denylist(&make_call_ref_for_test(worker_address), carol, true); + assert_allowed(worker_address, alice); + assert_allowed(worker_address, bob); + assert!(!is_allowed(worker_address, carol), 0); + + // add alice to the allow list, then assert that alice is allowed and bob is not + set_allowlist(&make_call_ref_for_test(worker_address), alice, true); + assert_allowed(worker_address, alice); + assert!(!is_allowed(worker_address, bob), 0); + + // add bob to the allow list, then assert that alice and bob are allowed + set_allowlist(&make_call_ref_for_test(worker_address), bob, true); + assert_allowed(worker_address, alice); + assert_allowed(worker_address, bob); + + // add bob to the deny list, then assert that bob is not allowed even though he was on allow list + set_denylist(&make_call_ref_for_test(worker_address), bob, true); + assert_allowed(worker_address, alice); + assert!(!is_allowed(worker_address, bob), 0); + assert!(!is_allowed(worker_address, carol), 0); + + // remove all from lists, then assert that all are allowed + set_allowlist(&make_call_ref_for_test(worker_address), alice, false); + set_allowlist(&make_call_ref_for_test(worker_address), bob, false); + set_denylist(&make_call_ref_for_test(worker_address), bob, false); + set_denylist(&make_call_ref_for_test(worker_address), carol, false); + assert_allowed(worker_address, alice); + assert_allowed(worker_address, bob); + assert_allowed(worker_address, carol); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::ESENDER_DENIED)] + fun test_assert_allowed_fails_if_denied() { + let worker_address = @3001; + worker_common::worker_config::initialize_for_worker_test_only( + worker_address, + WORKER_ID_EXECUTOR(), + worker_address, + @0x501ead, + vector[@1234, @2345], + vector[], + @0xfee11b1, + ); + let carol = @5566; + + // add carol to the deny list, then assert that alice and bob are allowed + set_denylist(&make_call_ref_for_test(worker_address), carol, true); + assert_allowed(worker_address, carol); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::ESENDER_DENIED)] + fun test_assert_allowed_fails_if_not_in_an_existing_allowlist() { + let worker_address = @3001; + worker_common::worker_config::initialize_for_worker_test_only( + worker_address, + WORKER_ID_EXECUTOR(), + worker_address, + @0x501ead, + vector[@1234, @2345], + vector[], + @0xfee11b1, + ); + let alice = @1122; + let bob = @3344; + let carol = @5566; + set_denylist(&make_call_ref_for_test(worker_address), bob, true); + set_allowlist(&make_call_ref_for_test(worker_address), carol, true); + + // Since an allowlist exists, Alice must be on the allowlist to be authorized + assert_allowed(worker_address, alice); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config_store::EWORKER_ALREADY_ON_ALLOWLIST)] + fun test_set_allowlist_fails_if_already_on_allowlist() { + let worker_address = @3001; + worker_common::worker_config::initialize_for_worker_test_only( + worker_address, + WORKER_ID_EXECUTOR(), + worker_address, + @0x501ead, + vector[@1234, @2345], + vector[], + @0xfee11b1, + ); + let alice = @1122; + set_allowlist(&make_call_ref_for_test(worker_address), alice, true); + set_allowlist(&make_call_ref_for_test(worker_address), alice, true); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config_store::EWORKER_ALREADY_ON_DENYLIST)] + fun test_set_denylist_fails_if_already_on_denylist() { + let worker_address = @3001; + worker_common::worker_config::initialize_for_worker_test_only( + worker_address, + WORKER_ID_EXECUTOR(), + worker_address, + @0x501ead, + vector[@1234, @2345], + vector[], + @0xfee11b1, + ); + let alice = @1122; + set_denylist(&make_call_ref_for_test(worker_address), alice, true); + set_denylist(&make_call_ref_for_test(worker_address), alice, true); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config_store::EWORKER_NOT_ON_ALLOWLIST)] + fun test_set_allowlist_fails_if_not_on_allowlist() { + let worker_address = @3001; + worker_common::worker_config::initialize_for_worker_test_only( + worker_address, + WORKER_ID_EXECUTOR(), + worker_address, + @0x501ead, + vector[@1234, @2345], + vector[], + @0xfee11b1, + ); + let alice = @1122; + set_allowlist(&make_call_ref_for_test(worker_address), alice, false); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config_store::EWORKER_NOT_ON_DENYLIST)] + fun test_set_denylist_fails_if_not_on_denylist() { + let worker_address = @3001; + worker_common::worker_config::initialize_for_worker_test_only( + worker_address, + WORKER_ID_EXECUTOR(), + worker_address, + @0x501ead, + vector[@1234, @2345], + vector[], + @0xfee11b1, + ); + let alice = @1122; + set_denylist(&make_call_ref_for_test(worker_address), alice, false); + } + + #[test] + fun test_set_worker_fee_lib() { + let worker_address = @3001; + let fee_lib = @1234; + worker_common::worker_config::initialize_for_worker_test_only( + worker_address, + WORKER_ID_EXECUTOR(), + worker_address, + @0x501ead, + vector[@1234, @2345], + vector[], + @0xfee11b1, + ); + + worker_config::set_worker_fee_lib(&make_call_ref_for_test(worker_address), fee_lib); + assert!(was_event_emitted(&worker_config::worker_fee_lib_updated_event(worker_address, fee_lib)), 0); + let fee_lib_result = worker_config::get_worker_fee_lib(worker_address); + assert!(fee_lib == fee_lib_result, 0); + } +} diff --git a/packages/layerzero-v2/initia/contracts/workers/dvn/Move.toml b/packages/layerzero-v2/initia/contracts/workers/dvn/Move.toml new file mode 100644 index 00000000..92a4e003 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/workers/dvn/Move.toml @@ -0,0 +1,62 @@ +[package] +name = "dvn" +version = "1.0.0" +authors = [] + +[addresses] +dvn = "_" +router_node_0 = "_" +zro = "_" +simple_msglib = "_" +blocked_msglib = "_" +uln_302 = "_" +router_node_1 = "_" +endpoint_v2_common = "_" +endpoint_v2 = "_" +layerzero_admin = "_" +layerzero_treasury_admin = "_" +treasury = "_" +msglib_types = "_" +worker_common = "_" +price_feed_router_0 = "_" +price_feed_router_1 = "_" +price_feed_module_0 = "_" +executor_fee_lib_router_0 = "_" +executor_fee_lib_router_1 = "_" +dvn_fee_lib_router_0 = "_" +dvn_fee_lib_router_1 = "_" +executor_fee_lib_0 = "_" +dvn_fee_lib_0 = "_" + +[dev-addresses] +dvn = "0x3001" +router_node_0 = "0x9001" +zro = "0x1112" +simple_msglib = "0x9002" +blocked_msglib = "0x9003" +uln_302 = "0x9005" +router_node_1 = "0x9007" +endpoint_v2_common = "0x9098" +endpoint_v2 = "0x12345678" +layerzero_admin = "0x18943124" +layerzero_treasury_admin = "0x1894312499" +treasury = "0x123432432" +msglib_types = "0x97324123" +worker_common = "0x3999" +price_feed_router_0 = "0x65DD71A" +price_feed_router_1 = "0x65DD71AB" +price_feed_module_0 = "0x65DD71" +executor_fee_lib_router_0 = "0x30001" +executor_fee_lib_router_1 = "0x30002" +dvn_fee_lib_router_0 = "0x30001a" +dvn_fee_lib_router_1 = "0x30002a" +executor_fee_lib_0 = "0x3000" +dvn_fee_lib_0 = "0x3000a" + +[dependencies] +endpoint_v2_common = { local = "../../endpoint_v2_common" } +worker_common = { local = "../../worker_peripherals/worker_common" } +router_node_0 = { local = "../../msglib/routers/router_node_0" } + +[dev-dependencies] + diff --git a/packages/layerzero-v2/initia/contracts/workers/dvn/sources/dvn.move b/packages/layerzero-v2/initia/contracts/workers/dvn/sources/dvn.move new file mode 100644 index 00000000..6388f686 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/workers/dvn/sources/dvn.move @@ -0,0 +1,387 @@ +module dvn::dvn { + use std::signer::address_of; + use std::vector; + + use dvn::hashes::{ + create_quorum_change_admin_hash, + create_set_allowlist_hash, + create_set_denylist_hash, + create_set_dvn_signer_hash, + create_set_fee_lib_hash, + create_set_msglibs_hash, + create_set_pause_hash, + create_set_quorum_hash, + create_verify_hash, + }; + use endpoint_v2_common::bytes32; + use endpoint_v2_common::contract_identity::{Self, CallRef, ContractSigner, DynamicCallRef, make_call_ref}; + use endpoint_v2_common::packet_raw; + use endpoint_v2_common::universal_config; + use msglib_types::worker_options::DVN_WORKER_ID; + use router_node_0::router_node; + use worker_common::multisig::{Self, assert_all_and_add_to_history}; + use worker_common::worker_config; + + #[test_only] + friend dvn::dvn_tests; + + struct DvnStore has key { + contract_signer: ContractSigner, + } + + /// Initialize the DVN Store + fun init_module(account: &signer) { + move_to(account, DvnStore { contract_signer: contract_identity::create_contract_signer(account) }); + } + + #[test_only] + /// Initialize the DVN Store for testing + public fun init_module_for_test() { + init_module(&std::account::create_signer_for_test(@dvn)); + } + + /// Worker-only function to register and configure the DVN. This can only be called once, and should be called with + /// the contract (@dvn) as the signer. + public entry fun initialize( + account: &signer, + deposit_address: address, + admins: vector
, + dvn_signers: vector>, + quorum: u64, + supported_msglibs: vector
, + fee_lib: address, + ) { + assert!(address_of(account) == @dvn, EUNAUTHORIZED); + assert!(vector::length(&supported_msglibs) > 0, EDVN_MSGLIB_LESS_THAN_ONE); + + worker_config::initialize_for_worker( + account, + DVN_WORKER_ID(), + deposit_address, + // Instead of a role admin, this DVN allows regular admins or signers (quorum_change_admin()) to add or + // remove admins + @0x0, + admins, + supported_msglibs, + fee_lib, + ); + multisig::initialize_for_worker(move account, quorum, dvn_signers); + } + + // ================================== Protocol: DVN Verify (Admin /w Signatures) ================================== + + /// DVNs call dvn_verify() in uln_302/router_calls.move to verify a packet + /// Only admins can call this function and it requires a quorum of dvn_signer signatures to succeed + public entry fun verify( + account: &signer, + packet_header: vector, + payload_hash: vector, + confirmations: u64, + msglib: address, + expiration: u64, + signatures: vector, + ) acquires DvnStore { + assert_admin(address_of(move account)); + let packet_header_raw = packet_raw::bytes_to_raw_packet(packet_header); + let hash = create_verify_hash( + packet_header, + payload_hash, + confirmations, + msglib, + get_vid(), + expiration, + ); + assert_all_and_add_to_history(call_ref(), &signatures, expiration, hash); + let dvn_verify_params = msglib_types::dvn_verify_params::pack_dvn_verify_params( + packet_header_raw, + bytes32::to_bytes32(payload_hash), + confirmations, + ); + router_node::dvn_verify(msglib, dynamic_call_ref(msglib, b"dvn_verify"), dvn_verify_params); + } + + // ================================================== Admin Only ================================================== + + /// Add or remove an admin. `active` is true to add, false to remove. + /// Admins are required to call the majority of DVN non-view actions, including verifying messages. + public entry fun set_admin(account: &signer, admin: address, active: bool) acquires DvnStore { + assert_admin(address_of(move account)); + worker_config::set_worker_admin(call_ref(), admin, active); + } + + + /// Set the deposit address for the DVN (must be real account) + /// The message library is instructed to send DVN fees to this address + public entry fun set_deposit_address(account: &signer, deposit_address: address) acquires DvnStore { + assert_admin(address_of(move account)); + worker_config::set_deposit_address(call_ref(), deposit_address); + } + + /// Set the configuration for a specific destination EID + public entry fun set_dst_config( + account: &signer, + remote_eid: u32, + gas: u64, + multiplier_bps: u16, + floor_margin_usd: u128, + ) acquires DvnStore { + assert_admin(address_of(move account)); + worker_config::set_dvn_dst_config(call_ref(), remote_eid, gas, multiplier_bps, floor_margin_usd); + } + + /// Sets the price feed module address and the feed address for the dvn + public entry fun set_price_feed( + account: &signer, + price_feed: address, + feed_address: address, + ) acquires DvnStore { + assert_admin(address_of(move account)); + worker_config::set_price_feed(call_ref(), price_feed, feed_address); + } + + // =========================================== Admin /w Signatures Only =========================================== + + /// Add or remove a dvn signer (public key) + /// This will abort if it results in fewer signers than the quorum + public entry fun set_dvn_signer( + account: &signer, + dvn_signer: vector, + active: bool, + expiration: u64, + signatures: vector, + ) acquires DvnStore { + assert_admin(address_of(move account)); + let hash = create_set_dvn_signer_hash(dvn_signer, active, get_vid(), expiration); + assert_all_and_add_to_history(call_ref(), &signatures, expiration, hash); + multisig::set_signer(call_ref(), dvn_signer, active); + } + + /// Update the quorum threshold + /// This will abort if the new quorum is greater than the number of registered dvn signers + public entry fun set_quorum( + account: &signer, + quorum: u64, + expiration: u64, + signatures: vector, + ) acquires DvnStore { + assert_admin(address_of(move account)); + let hash = create_set_quorum_hash(quorum, get_vid(), expiration); + assert_all_and_add_to_history(call_ref(), &signatures, expiration, hash); + multisig::set_quorum(call_ref(), quorum); + } + + /// Add or remove a sender address from the allowlist + /// When the allowlist has as least one entry, only senders on the allowlist can send messages to the DVN + /// When the allowlist is empty, only denylist senders will be rejected + /// The allowlist and the denylist are enforced upon get fee + public entry fun set_allowlist( + account: &signer, + oapp: address, + allowed: bool, + expiration: u64, + signatures: vector, + ) acquires DvnStore { + assert_admin(address_of(move account)); + let hash = create_set_allowlist_hash(oapp, allowed, get_vid(), expiration); + assert_all_and_add_to_history(call_ref(), &signatures, expiration, hash); + worker_config::set_allowlist(call_ref(), oapp, allowed); + } + + /// Add or remove an oapp sender address from the denylist + /// A denylisted sender will be rejected by the DVN regardless of the allowlist status + /// The allowlist and the denylist are enforced upon get fee + public entry fun set_denylist( + account: &signer, + oapp: address, + denied: bool, + expiration: u64, + signatures: vector, + ) acquires DvnStore { + assert_admin(address_of(move account)); + let hash = create_set_denylist_hash(oapp, denied, get_vid(), expiration); + assert_all_and_add_to_history(call_ref(), &signatures, expiration, hash); + worker_config::set_denylist(call_ref(), oapp, denied); + } + + /// Set the supported message libraries for the DVN + /// The list provided will completely replace the existing list + public entry fun set_supported_msglibs( + account: &signer, + msglibs: vector
, + expiration: u64, + signatures: vector, + ) acquires DvnStore { + assert_admin(address_of(move account)); + let hash = create_set_msglibs_hash(msglibs, get_vid(), expiration); + assert_all_and_add_to_history(call_ref(), &signatures, expiration, hash); + worker_config::set_supported_msglibs(call_ref(), msglibs); + } + + /// Set the fee lib for the DVN + /// The fee lib will be used by the Message Library to route the call to the correct DVN Fee Lib + public entry fun set_fee_lib( + account: &signer, + fee_lib: address, + expiration: u64, + signatures: vector, + ) acquires DvnStore { + assert_admin(address_of(move account)); + let hash = create_set_fee_lib_hash(fee_lib, get_vid(), expiration); + assert_all_and_add_to_history(call_ref(), &signatures, expiration, hash); + worker_config::set_worker_fee_lib(call_ref(), fee_lib); + } + + // Pause or unpause the DVN + public entry fun set_pause( + account: &signer, + pause: bool, + expiration: u64, + signatures: vector, + ) acquires DvnStore { + assert_admin(address_of(move account)); + let hash = create_set_pause_hash(pause, get_vid(), expiration); + assert_all_and_add_to_history(call_ref(), &signatures, expiration, hash); + worker_config::set_worker_pause(call_ref(), pause); + } + + // ================================================= Signers Only ================================================= + + /// Add or remove an admin using a quorum of dvn signers + public entry fun quorum_change_admin( + admin: address, + active: bool, + expiration: u64, + signatures: vector, + ) acquires DvnStore { + let hash = create_quorum_change_admin_hash(admin, active, get_vid(), expiration); + assert_all_and_add_to_history(call_ref(), &signatures, expiration, hash); + worker_config::set_worker_admin(call_ref(), admin, active); + } + + // ================================================ View Functions ================================================ + + #[view] + /// Returns the admins of the DVN + public fun get_admins(): vector
{ + worker_config::get_worker_admins(@dvn) + } + + #[view] + /// Returns whether the account is an admin of the DVN + public fun is_admin(account: address): bool { worker_config::is_worker_admin(@dvn, account) } + + #[view] + /// Returns whether the worker is paused + public fun is_paused(): bool { worker_config::is_worker_paused(@dvn) } + + #[view] + /// Returns the deposit address for the DVN. The message library will send fees to this address + public fun get_deposit_address(): address { + worker_config::get_deposit_address(@dvn) + } + + #[view] + /// Returns whether a particular signer (public key) is one of the DVN signers + public fun is_dvn_signer(signer: vector): bool { multisig::is_signer(@dvn, signer) } + + #[view] + /// Returns the fee library selected for this DVN + public fun get_fee_lib(): address { + worker_config::get_worker_fee_lib(@dvn) + } + + #[view] + /// Returns the quorum count required by this DVN + public fun get_quorum(): u64 { multisig::get_quorum(@dvn) } + + #[view] + /// Returns the list of supported message libraries for the DVN + public fun get_supported_msglibs(): vector
{ worker_config::get_supported_msglibs(@dvn) } + + #[view] + /// Returns the fee lib for the DVN + public fun get_worker_fee_lib(): address { + let fee_lib = worker_config::get_worker_fee_lib(@dvn); + fee_lib + } + + #[view] + /// Returns the default multiplier bps for the premium calculation + public fun get_default_multiplier_bps(): u16 { + worker_config::get_default_multiplier_bps(@dvn) + } + + #[view] + /// Returns the supported option types for the DVN + public fun get_supported_option_types(): vector { + worker_config::get_supported_option_types(@dvn) + } + + #[view] + /// Returns whether a particular sender is on the allowlist + public fun allowlist_contains(sender: address): bool { worker_config::allowlist_contains(@dvn, sender) } + + #[view] + /// Returns whether a particular sender is on the denylist + public fun denylist_contains(sender: address): bool { worker_config::denylist_contains(@dvn, sender) } + + #[view] + /// Returns whether the sender is allowed to send messages to the DVN based on allowlist and denylist + public fun is_allowed(sender: address): bool { worker_config::is_allowed(@dvn, sender) } + + #[view] + /// Returns the DVN config values for the destination EID + /// @returns (gas, multiplier_bps, floor_margin_usd) + public fun get_dst_config(dst_eid: u32): (u64, u16, u128) { + worker_config::get_dvn_dst_config_values(@dvn, dst_eid) + } + + #[view] + /// Returns the VID for the DVN + public fun get_vid(): u32 { + universal_config::eid() % 30_000 + } + + #[view] + /// Get the number of other workers that are currently delegating to this dvn's price feed configuration + public fun get_count_price_feed_delegate_dependents(): u64 { + worker_config::get_count_price_feed_delegate_dependents(@dvn) + } + + // ==================================================== Helpers =================================================== + + /// Derive a call ref for the DVN worker for a given target contract + inline fun dynamic_call_ref(target_contract: address, authorization: vector): &DynamicCallRef { + &contract_identity::make_dynamic_call_ref(&store().contract_signer, target_contract, authorization) + } + + /// Get a Call Ref directed at the worker common contract + inline fun call_ref(): &CallRef { + &make_call_ref(&store().contract_signer) + } + + /// Borrow the DVN Store + inline fun store(): &DvnStore { borrow_global(@dvn) } + + /// Borrow a mutable DVN store + inline fun store_mut(): &mut DvnStore { borrow_global_mut(@dvn) } + + // ==================================================== Internal =================================================== + + /// Assert that the caller is an admin + inline fun assert_admin(admin: address) { + worker_config::assert_worker_admin(@dvn, admin); + } + + /// Assert that the VID for the DVN is the expected VID for this chain + /// VID is a endpoint v1-v2 compatible eid e.g. 30101 -> 101 + inline fun assert_vid(vid: u32) { + assert!(get_vid() == vid, EDVN_INVALID_VID); + } + + // ================================================== Error Codes ================================================= + + const EDVN_INVALID_VID: u64 = 2; + const EDVN_MSGLIB_LESS_THAN_ONE: u64 = 3; + const EUNAUTHORIZED: u64 = 4; +} diff --git a/packages/layerzero-v2/initia/contracts/workers/dvn/sources/hashes.move b/packages/layerzero-v2/initia/contracts/workers/dvn/sources/hashes.move new file mode 100644 index 00000000..ac0e96be --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/workers/dvn/sources/hashes.move @@ -0,0 +1,214 @@ +/// These functions are used to generate hashes against which signatures are created and verified +module dvn::hashes { + use std::aptos_hash; + use std::vector; + + use endpoint_v2_common::bytes32::{Bytes32, keccak256}; + use endpoint_v2_common::serde; + + // ================================================ Hash Generation =============================================== + + // These hashes are used by the DVN Multisig as an input to signature generation + + #[view] + /// Get a 4-byte hash that represents a given function name + public fun get_function_signature(function_name: vector): vector { + vector::slice(&aptos_hash::keccak256(std::bcs::to_bytes(&function_name)), 0, 4) + } + + #[view] + /// Create a hash for a verify function call + public fun create_verify_hash( + packet_header: vector, + payload_hash: vector, + confirmations: u64, + target: address, + vid: u32, + expiration: u64, + ): Bytes32 { + keccak256(build_verify_payload(packet_header, payload_hash, confirmations, target, vid, expiration)) + } + + #[view] + /// Create a hash for a set_quorum function call + public fun create_set_quorum_hash(quorum: u64, vid: u32, expiration: u64): Bytes32 { + keccak256(build_set_quorum_payload(quorum, vid, expiration)) + } + + #[view] + /// Create a hash for a set_dvn_signer function call + public fun create_set_dvn_signer_hash(dvn_signer: vector, active: bool, vid: u32, expiration: u64): Bytes32 { + keccak256(build_set_dvn_signer_payload(dvn_signer, active, vid, expiration)) + } + + #[view] + /// Create a hash for a set_allowlist function call + public fun create_set_allowlist_hash(sender: address, allowed: bool, vid: u32, expiration: u64): Bytes32 { + keccak256(build_set_allowlist_payload(sender, allowed, vid, expiration)) + } + + #[view] + /// Create a hash for a set_denylist function call + public fun create_set_denylist_hash(sender: address, denied: bool, vid: u32, expiration: u64): Bytes32 { + keccak256(build_set_denylist_payload(sender, denied, vid, expiration)) + } + + #[view] + /// Create a hash for a quorum_change_admin function call + public fun create_quorum_change_admin_hash( + admin: address, + active: bool, + vid: u32, + expiration: u64, + ): Bytes32 { + keccak256(build_quorum_change_admin_payload(admin, active, vid, expiration)) + } + + #[view] + /// Create a hash for a set_msglibs function call + public fun create_set_msglibs_hash(msglibs: vector
, vid: u32, expiration: u64): Bytes32 { + keccak256(build_set_msglibs_payload(msglibs, vid, expiration)) + } + + #[view] + public fun create_set_fee_lib_hash(fee_lib: address, vid: u32, expiration: u64): Bytes32 { + keccak256(build_set_fee_lib_payload(fee_lib, vid, expiration)) + } + + #[view] + public fun create_set_pause_hash(pause: bool, vid: u32, expiration: u64): Bytes32 { + keccak256(build_set_pause_payload(pause, vid, expiration)) + } + + // ============================================== Payload Generation ============================================== + + // Payloads are serialized data that are hashed to create a hash that can be signed by a worker + + #[view] + /// Build the serialized payload for a verify function call (for procuring a hash) + public fun build_verify_payload( + packet_header: vector, + payload_hash: vector, + confirmations: u64, + target: address, + vid: u32, + expiration: u64, + ): vector { + let payload = vector[]; + serde::append_bytes(&mut payload, get_function_signature(b"verify")); + serde::append_bytes(&mut payload, packet_header); + serde::append_bytes(&mut payload, payload_hash); + serde::append_u64(&mut payload, confirmations); + serde::append_bytes(&mut payload, std::bcs::to_bytes(&target)); + serde::append_u32(&mut payload, vid); + serde::append_u64(&mut payload, expiration); + payload + } + + #[view] + /// Build the serialized payload for a set_quorum function call (for procuring a hash) + public fun build_set_quorum_payload(quorum: u64, vid: u32, expiration: u64): vector { + let payload = vector[]; + serde::append_bytes(&mut payload, get_function_signature(b"set_quorum")); + serde::append_u64(&mut payload, quorum); + serde::append_u32(&mut payload, vid); + serde::append_u64(&mut payload, expiration); + payload + } + + #[view] + /// Build the serialized payload for a set_dvn_signer function call (for procuring a hash) + public fun build_set_dvn_signer_payload( + dvn_signer: vector, + active: bool, + vid: u32, + expiration: u64, + ): vector { + let active_value: u8 = if (active) 1 else 0; + let payload = vector[]; + serde::append_bytes(&mut payload, get_function_signature(b"set_dvn_signer")); + serde::append_bytes(&mut payload, dvn_signer); + serde::append_u8(&mut payload, active_value); + serde::append_u32(&mut payload, vid); + serde::append_u64(&mut payload, expiration); + payload + } + + #[view] + /// Build the serialized payload for a set_allowlist function call (for procuring a hash) + public fun build_set_allowlist_payload(sender: address, allowed: bool, vid: u32, expiration: u64): vector { + let allowed_value: u8 = if (allowed) 1 else 0; + let payload = vector[]; + serde::append_bytes(&mut payload, get_function_signature(b"set_allowlist")); + serde::append_bytes(&mut payload, std::bcs::to_bytes(&sender)); + serde::append_u8(&mut payload, allowed_value); + serde::append_u32(&mut payload, vid); + serde::append_u64(&mut payload, expiration); + payload + } + + #[view] + /// Build the serialized payload for a set_denylist function call (for procuring a hash) + public fun build_set_denylist_payload(sender: address, denied: bool, vid: u32, expiration: u64): vector { + let denied_value: u8 = if (denied) 1 else 0; + let payload = vector[]; + serde::append_bytes(&mut payload, get_function_signature(b"set_denylist")); + serde::append_bytes(&mut payload, std::bcs::to_bytes(&sender)); + serde::append_u8(&mut payload, denied_value); + serde::append_u32(&mut payload, vid); + serde::append_u64(&mut payload, expiration); + payload + } + + #[view] + /// Build the serialized payload for a quorum_change_admin function call (for procuring a hash) + public fun build_quorum_change_admin_payload( + admin: address, + active: bool, + vid: u32, + expiration: u64, + ): vector { + let active_value: u8 = if (active) 1 else 0; + let payload = vector[]; + serde::append_bytes(&mut payload, get_function_signature(b"quorum_change_admin")); + serde::append_bytes(&mut payload, std::bcs::to_bytes(&admin)); + serde::append_u8(&mut payload, active_value); + serde::append_u32(&mut payload, vid); + serde::append_u64(&mut payload, expiration); + payload + } + + #[view] + /// Build the serialized payload for a set_msglibs function call (for procuring a hash) + public fun build_set_msglibs_payload(msglibs: vector
, vid: u32, expiration: u64): vector { + let payload = vector[]; + serde::append_bytes(&mut payload, get_function_signature(b"set_msglibs")); + for (i in 0..vector::length(&msglibs)) { + let msglib = *vector::borrow(&msglibs, i); + serde::append_address(&mut payload, msglib); + }; + serde::append_u32(&mut payload, vid); + serde::append_u64(&mut payload, expiration); + payload + } + + #[view] + public fun build_set_fee_lib_payload(fee_lib: address, vid: u32, expiration: u64): vector { + let payload = vector[]; + serde::append_bytes(&mut payload, get_function_signature(b"set_fee_lib")); + serde::append_address(&mut payload, fee_lib); + serde::append_u32(&mut payload, vid); + serde::append_u64(&mut payload, expiration); + payload + } + + #[view] + public fun build_set_pause_payload(pause: bool, vid: u32, expiration: u64): vector { + let payload = vector[]; + serde::append_bytes(&mut payload, get_function_signature(b"set_pause")); + serde::append_u8(&mut payload, if (pause) 1 else 0); + serde::append_u32(&mut payload, vid); + serde::append_u64(&mut payload, expiration); + payload + } +} diff --git a/packages/layerzero-v2/initia/contracts/workers/dvn/tests/dvn_tests.move b/packages/layerzero-v2/initia/contracts/workers/dvn/tests/dvn_tests.move new file mode 100644 index 00000000..da430455 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/workers/dvn/tests/dvn_tests.move @@ -0,0 +1,555 @@ +#[test_only] +module dvn::dvn_tests { + use std::account::{create_account_for_test, create_signer_for_test}; + use std::event::was_event_emitted; + + use dvn::dvn::{ + get_fee_lib, get_quorum, init_module_for_test, initialize, is_admin, is_dvn_signer, is_paused, + quorum_change_admin, set_admin, set_allowlist, set_denylist, set_deposit_address, set_dst_config, + set_dvn_signer, set_fee_lib, set_pause, set_quorum, set_supported_msglibs, verify, + }; + use dvn::hashes::create_verify_hash; + use endpoint_v2_common::bytes32::{Self, from_bytes32}; + use endpoint_v2_common::contract_identity::make_call_ref_for_test; + use endpoint_v2_common::guid::compute_guid; + use endpoint_v2_common::packet_raw::get_packet_bytes; + use endpoint_v2_common::packet_v1_codec; + use endpoint_v2_common::serde::flatten; + use endpoint_v2_common::universal_config; + use worker_common::worker_config; + + const VID: u32 = 1; + const EXPIRATION: u64 = 2000; + + #[test] + #[expected_failure(abort_code = worker_common::worker_config_store::EWORKER_ALREADY_INITIALIZED)] + fun test_register_and_configure_dvn_cannot_initialize_twice() { + let pub_key_1: vector = x"3bd5f17b6bc7a9022402246dd8e1530f0acd1d6439089b4f3bd8868250c1656c08a9fc2e4bff170ed023fbf77e6645020a77eba9c7c03390ed1b316af1ab6f0c"; + universal_config::init_module_for_test(VID); + init_module_for_test(); + + let dvn = &create_account_for_test(@dvn); + initialize( + dvn, + @dvn, + vector[@1234], + vector[pub_key_1], + 1, + vector[@0xaaaa], + @dvn_fee_lib_router_0, + ); + initialize( + dvn, + @dvn, + vector[@1234], + vector[pub_key_1], + 1, + vector[@0xaaaa], + @dvn_fee_lib_router_0, + ) + } + + #[test] + fun test_initialization() { + let pub_key_1: vector = x"3bd5f17b6bc7a9022402246dd8e1530f0acd1d6439089b4f3bd8868250c1656c08a9fc2e4bff170ed023fbf77e6645020a77eba9c7c03390ed1b316af1ab6f0c"; + let pub_key_2: vector = x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d"; + let pub_key_3: vector = x"37bdab42a45e9d6cc56f7d0cc7897e871a0357bce3f0f4c99c93c54291b259a29a92111167a25ae188ef49b2f3df880d8aae8522e29cb6c299745258a200cfff"; + universal_config::init_module_for_test(VID); + init_module_for_test(); + let dvn = &create_account_for_test(@dvn); + initialize( + dvn, + @dvn, + vector[@1234, @2234, @3234], + vector[pub_key_1, pub_key_2], + 1, + vector[@0xaaaa], + @dvn_fee_lib_router_0, + ); + assert!(get_quorum() == 1, 0); + assert!(is_dvn_signer(pub_key_1), 1); + assert!(is_dvn_signer(pub_key_2), 2); + assert!(!is_dvn_signer(pub_key_3), 3); + assert!(is_admin(@1234), 4); + assert!(is_admin(@2234), 5); + assert!(is_admin(@3234), 6); + assert!(!is_admin(@9876), 7); + } + + #[test] + fun test_set_dvn_signer() { + let pub_key_1: vector = x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a"; + let pub_key_2: vector = x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d"; + universal_config::init_module_for_test(VID); + init_module_for_test(); + let dvn = &create_account_for_test(@dvn); + initialize( + dvn, + @dvn, + vector[@1234, @2234, @3234], + vector[pub_key_1], + 1, + vector[@0xaaaa], + @dvn_fee_lib_router_0, + ); + + let signature_1 = x"a13c94e82fc009f71f152f137bed7fb799fa7d75a91a0e3a4ed2000fd408ba052743f3b91ee00cf6a5e98cd4d12b3b2e4984213c0c1c5a251b4e98eeec54f7a800"; + + let native_framework = &create_signer_for_test(@std); + std::timestamp::set_time_has_started_for_testing(native_framework); + std::timestamp::update_global_time_for_test_secs(1000); + + let admin_1 = &create_signer_for_test(@1234); + set_dvn_signer( + admin_1, + pub_key_2, + true, + EXPIRATION, + signature_1, + ); + + assert!(is_dvn_signer(pub_key_2), 0); + } + + #[test] + fun test_set_dst_config() { + let pub_key_1: vector = x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a"; + universal_config::init_module_for_test(VID); + init_module_for_test(); + let dvn = &create_account_for_test(@dvn); + let dvn_address = std::signer::address_of(dvn); + initialize( + dvn, + @dvn, + vector[dvn_address], + vector[pub_key_1], + 1, + vector[@0xaaaa], + @dvn_fee_lib_router_0, + ); + + set_dst_config(dvn, 101, 77000, 12000, 1); + + let (gas, multiplier_bps, floor_margin_usd) = worker_config::get_dvn_dst_config_values( + std::signer::address_of(dvn), + 101, + ); + assert!(gas == 77000, 0); + assert!(multiplier_bps == 12000, 1); + assert!(floor_margin_usd == 1, 2); + } + + #[test] + fun test_set_quorum() { + let pub_key_1: vector = x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a"; + let pub_key_2: vector = x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d"; + universal_config::init_module_for_test(VID); + init_module_for_test(); + let dvn = &create_account_for_test(@dvn); + initialize( + dvn, + @dvn, + vector[@1234, @2234, @3234], + vector[pub_key_1, pub_key_2], + 1, + vector[@0xaaaa], + @dvn_fee_lib_router_0, + ); + + let signature_1 = x"456e6b632d0958e6dc3d2fff9998e9c4be8023884e4a7f05d63bfd55f0178c743902b838114150a715597c808832c6bc61215ddc5133beac665861d9c2d0e26800"; + + let native_framework = &create_signer_for_test(@std); + std::timestamp::set_time_has_started_for_testing(native_framework); + std::timestamp::update_global_time_for_test_secs(1000); + + let admin_1 = &create_signer_for_test(@1234); + set_quorum( + admin_1, + 2, + EXPIRATION, + signature_1, + ); + + assert!(get_quorum() == 2, 0); + } + + #[test] + fun test_set_allowlist() { + let pub_key_1: vector = x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a"; + let pub_key_2: vector = x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d"; + universal_config::init_module_for_test(VID); + init_module_for_test(); + + let dvn = &create_account_for_test(@dvn); + initialize( + dvn, + @dvn, + vector[@1234, @2234, @3234], + vector[pub_key_1, pub_key_2], + 1, + vector[@simple_msglib], + @dvn_fee_lib_router_0, + ); + + let signature_1_1 = x"d9a9aa95f0e21102aa8d05f2ada4261bc887b16f85d669df700c01ed985439187a278239f70567bcb879b1b5109d2f6fd4fa551535421188450e6cb1c74df7f200"; + + let native_framework = &create_signer_for_test(@std); + std::timestamp::set_time_has_started_for_testing(native_framework); + std::timestamp::update_global_time_for_test_secs(1000); + + let admin_1 = &create_signer_for_test(@1234); + + set_allowlist(admin_1, @9988, true, EXPIRATION, signature_1_1); // pubkey 1 adds oapp to allowlist + assert!(worker_config::allowlist_contains(@dvn, @9988), 0); + assert!(worker_config::is_allowed(@dvn, @9988), 1); + + let signature_1_2 = x"504be072fd7ca14b3ef724d4dcfe2eb24ed121651b9f293b56c1df3a0e3e5e17437308c3d0aff27f7a9608003447d11d1f8f2f5c521ba77abb751d6fa225693001"; + set_allowlist(admin_1, @9988, false, EXPIRATION, signature_1_2); // pubkey 1 removes oapp to allowlist + assert!(!worker_config::allowlist_contains(@dvn, @9988), 2); + } + + #[test] + fun test_set_denylist() { + let pub_key_1: vector = x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a"; + let pub_key_2: vector = x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d"; + universal_config::init_module_for_test(VID); + init_module_for_test(); + + let dvn = &create_account_for_test(@dvn); + initialize( + dvn, + @dvn, + vector[@1234, @2234, @3234], + vector[pub_key_1, pub_key_2], + 1, + vector[@simple_msglib], + @dvn_fee_lib_router_0, + ); + + let signature_1_1 = x"afbb4ac1ed62b3c63ee2ae9b9b5272f5fb7296990da96a29029e75bb6b97b3fc6471ad3ac62e897ab7681bf7dc4dd10f8b33325e391155d8d02d7c3ad455eaf001"; + + let native_framework = &create_signer_for_test(@std); + std::timestamp::set_time_has_started_for_testing(native_framework); + std::timestamp::update_global_time_for_test_secs(1000); + + let admin_1 = &create_signer_for_test(@1234); + + set_denylist(admin_1, @9988, true, EXPIRATION, signature_1_1); // pubkey 1 adds oapp to denylist + assert!(worker_config::denylist_contains(@dvn, @9988), 0); + assert!(!worker_config::is_allowed(@dvn, @9988), 1); + + let signature_1_2 = x"64526a94655175cb1553d36615b2cbae8c7df8465719e1508ea1905d47b833fe44aea8d6aebf48a1cb70f6d93ba92215b70a311b18dd468d290d02404c15b63e01"; + set_denylist(admin_1, @9988, false, EXPIRATION, signature_1_2); // pubkey 1 removes oapp to denylist + assert!(!worker_config::denylist_contains(@dvn, @9988), 2); + assert!(worker_config::is_allowed(@dvn, @9988), 3); + } + + #[test] + fun test_set_fee_lib() { + let pub_key_1: vector = x"1656867692ee1158567ecf944ea0755eff7d804b72fb3bdd7dda07758296cf14df3a10d6632e17023a4ed2aa47f6adf83b7aa6b0be4100efbcb7654cc40bcede"; + universal_config::init_module_for_test(VID); + init_module_for_test(); + + let dvn = &create_account_for_test(@dvn); + initialize( + dvn, + @dvn, + vector[@1234, @2234, @3234], + vector[pub_key_1], + 1, + vector[@simple_msglib], + @0xfee11b001, + ); + + let fee_lib_from_worker_config = worker_config::get_worker_fee_lib(@dvn); + assert!(fee_lib_from_worker_config == @0xfee11b001, 0); + let fee_lib_from_dvn = get_fee_lib(); + assert!(fee_lib_from_dvn == @0xfee11b001, 1); + + let signature_1 = x"8675d9d20931230c405c6a756d7d9e9f6c2bc9770e2e7d52a1d31b5a7a5fedd80f5e60a0af97ef2ee5a7063ebbf285f34c2de07630a5bc07bac24da80d0a055e00"; + + let native_framework = &create_signer_for_test(@std); + std::timestamp::set_time_has_started_for_testing(native_framework); + std::timestamp::update_global_time_for_test_secs(1000); + + let admin_1 = &create_signer_for_test(@1234); + set_fee_lib( + admin_1, + @0xfee11b002, + EXPIRATION, + signature_1, + ); + assert!(was_event_emitted(&worker_config::worker_fee_lib_updated_event(@dvn, @0xfee11b002)), 2); + + let fee_lib_from_worker_config = worker_config::get_worker_fee_lib(@dvn); + assert!(fee_lib_from_worker_config == @0xfee11b002, 0); + let fee_lib_from_dvn = get_fee_lib(); + assert!(fee_lib_from_dvn == @0xfee11b002, 1); + } + + #[test] + fun test_set_pause() { + let pub_key_1: vector = x"2f68cff6060b082c04370615bbd5097d2f55f6d4ec9e3ed6156db64095b43efe0894d94399cc394394cdfb1075515877049959398ef042fca64e03443f9e8a41"; + universal_config::init_module_for_test(VID); + init_module_for_test(); + + let dvn = &create_account_for_test(@dvn); + initialize( + dvn, + @dvn, + vector[@1234, @2234, @3234], + vector[pub_key_1], + 1, + vector[@simple_msglib], + @0xfee11b001, + ); + + let signature_1 = x"8675d9d20931230c405c6a756d7d9e9f6c2bc9770e2e7d52a1d31b5a7a5fedd80f5e60a0af97ef2ee5a7063ebbf285f34c2de07630a5bc07bac24da80d0a055e00"; + + let native_framework = &create_signer_for_test(@std); + std::timestamp::set_time_has_started_for_testing(native_framework); + std::timestamp::update_global_time_for_test_secs(1000); + + assert!(!is_paused(), 0); + let admin_1 = &create_signer_for_test(@1234); + set_pause(admin_1, true, EXPIRATION, signature_1); + assert!(was_event_emitted(&worker_config::paused_event(@dvn)), 2); + assert!(is_paused(), 0); + } + + #[test] + fun test_set_unpause() { + let pub_key_1: vector = x"7dd96c8221160d75f6ea7b11382755a907e80a90d03275f360f1febd21d8454819abbefaed3080505f6e3c2d5b33cf04e019cb8cfd90440c7715663ee4fe5483"; + universal_config::init_module_for_test(VID); + init_module_for_test(); + + let dvn = &create_account_for_test(@dvn); + initialize( + dvn, + @dvn, + vector[@1234, @2234, @3234], + vector[pub_key_1], + 1, + vector[@simple_msglib], + @0xfee11b001, + ); + + let signature_1 = x"8675d9d20931230c405c6a756d7d9e9f6c2bc9770e2e7d52a1d31b5a7a5fedd80f5e60a0af97ef2ee5a7063ebbf285f34c2de07630a5bc07bac24da80d0a055e00"; + + let native_framework = &create_signer_for_test(@std); + std::timestamp::set_time_has_started_for_testing(native_framework); + std::timestamp::update_global_time_for_test_secs(1000); + assert!(!is_paused(), 0); + worker_config::set_worker_pause(&make_call_ref_for_test(@dvn), true); + assert!(is_paused(), 0); + + let admin_1 = &create_signer_for_test(@1234); + set_pause(admin_1, false, EXPIRATION, signature_1); + assert!(was_event_emitted(&worker_config::unpaused_event(@dvn)), 2); + assert!(!is_paused(), 0); + } + + #[test] + fun test_quorum_change_admin() { + let pub_key_1: vector = x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a"; + let pub_key_2: vector = x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d"; + universal_config::init_module_for_test(VID); + init_module_for_test(); + + let dvn = &create_account_for_test(@dvn); + initialize( + dvn, + @dvn, + vector[@1234, @2234, @3234], + vector[pub_key_1, pub_key_2], + 1, + vector[@simple_msglib], + @dvn_fee_lib_router_0, + ); + + let signature_1 = x"8675d9d20931230c405c6a756d7d9e9f6c2bc9770e2e7d52a1d31b5a7a5fedd80f5e60a0af97ef2ee5a7063ebbf285f34c2de07630a5bc07bac24da80d0a055e00"; + let signature_2 = x"6525f9282e67057649022911b539663b92b9ba06b3b1a797c052499b61cf4e404f1aba7379fb1f75a7a891ce890ffa866c38704b0f978f8c76721ab2dcff437d01"; + + let native_framework = &create_signer_for_test(@std); + std::timestamp::set_time_has_started_for_testing(native_framework); + std::timestamp::update_global_time_for_test_secs(1000); + + assert!(!is_admin(@9988), 0); + quorum_change_admin( + @9988, + true, + EXPIRATION, + flatten(vector[signature_1, signature_2]), + ); + assert!(is_admin(@9988), 1); + } + + #[test] + fun test_set_supported_msglibs() { + let pub_key_1: vector = x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a"; + universal_config::init_module_for_test(VID); + init_module_for_test(); + + let dvn = &create_account_for_test(@dvn); + initialize( + dvn, + @dvn, + vector[@1234, @2234, @3234], + vector[pub_key_1], + 1, + vector[@simple_msglib], + @dvn_fee_lib_router_0, + ); + + let signature_1 = x"e7b2bfe8c1f079ea3aa1923ba76e3f15ae30fab716941352514b34656c6cd9b96c5a0ee0a5e4f579dcff9a8a339dcd44c53b7f56068fb7c97c7c589d2e4518a601"; + + let native_framework = &create_signer_for_test(@std); + std::timestamp::set_time_has_started_for_testing(native_framework); + std::timestamp::update_global_time_for_test_secs(1000); + + let admin_1 = &create_signer_for_test(@1234); + + assert!(!std::vector::contains(&worker_config::get_supported_msglibs(@dvn), &@2345), 1); + set_supported_msglibs( + admin_1, + vector[@1234, @2345], + EXPIRATION, + flatten(vector[signature_1]), + ); + assert!(std::vector::contains(&worker_config::get_supported_msglibs(@dvn), &@2345), 1); + } + + #[test] + fun test_verify() { + let pub_key_1: vector = x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a"; + let pub_key_2: vector = x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d"; + + universal_config::init_module_for_test(VID); + uln_302::msglib::initialize_for_test(); + init_module_for_test(); + + let dvn = &create_account_for_test(@dvn); + initialize( + dvn, + @dvn, + vector[@1234, @2234, @3234], + vector[pub_key_1, pub_key_2], + 1, + vector[@simple_msglib, @uln_302], + @dvn_fee_lib_router_0, + ); + + let signature_1 = x"0fdeda25570d3cb243b39764a167558e4445786c54b180afe8ed36ab87b95a9f04c26107fb149be923d0f82d54d064a2dfc18edaa9c3f00f572c20240a51064700"; + let signature_2 = x"344aa33801eab51e48fa0b1112f1cb3227316a6640a0d05dc9e1f9cf1c62494a69fe521a2fca7cdfa70271bf3126b7d4fa10696ad648a17c1c32e3e3c678eb0d00"; + + let native_framework = &create_signer_for_test(@std); + std::timestamp::set_time_has_started_for_testing(native_framework); + std::timestamp::update_global_time_for_test_secs(1000); + + // params to use for test + let src_eid = 1; + let sender = bytes32::from_address(@9999); + let dst_eid = 1; + let receiver = bytes32::from_address(@9999); + let nonce = 1; + let message = vector[1, 2, 3, 4]; + let guid = compute_guid(nonce, src_eid, sender, dst_eid, receiver); + let packet = endpoint_v2_common::packet_v1_codec::new_packet_v1( + src_eid, + sender, + dst_eid, + receiver, + nonce, + guid, + message, + ); + let packet_header = packet_v1_codec::extract_header(&packet); + let packet_header_bytes = get_packet_bytes(packet_header); + let payload_hash = endpoint_v2_common::packet_v1_codec::get_payload_hash(&packet); + + let admin = &create_signer_for_test(@1234); + + verify( + admin, + packet_header_bytes, + bytes32::from_bytes32(payload_hash), + 10, + @uln_302, + EXPIRATION, + flatten(vector[signature_1, signature_2]), + ); + + let expected_used_hash = create_verify_hash( + packet_header_bytes, + bytes32::from_bytes32(payload_hash), + 10, + @uln_302, + dst_eid, + EXPIRATION, + ); + assert!(worker_common::multisig::was_hash_used(@dvn, from_bytes32(expected_used_hash)), 0) + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::EUNAUTHORIZED)] + fun test_set_admin_fails_if_not_admin() { + let pub_key_1: vector = x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a"; + universal_config::init_module_for_test(VID); + init_module_for_test(); + + let dvn = &create_account_for_test(@dvn); + initialize( + dvn, + @dvn, + vector[@1234, @2234, @3234], + vector[pub_key_1], + 1, + vector[@simple_msglib], + @dvn_fee_lib_router_0, + ); + let admin = &create_signer_for_test(@3333); + set_admin(admin, @8888, true); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::EUNAUTHORIZED)] + fun test_set_deposit_address_fails_if_not_admin() { + let pub_key_1: vector = x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a"; + universal_config::init_module_for_test(VID); + init_module_for_test(); + + let dvn = &create_account_for_test(@dvn); + initialize( + dvn, + @dvn, + vector[@1234, @2234, @3234], + vector[pub_key_1], + 1, + vector[@simple_msglib], + @dvn_fee_lib_router_0, + ); + let admin = &create_signer_for_test(@1111); + set_deposit_address(admin, @8888); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::EUNAUTHORIZED)] + fun test_set_dst_config_fails_if_not_admin() { + let pub_key_1: vector = x"e1b271a7296266189d300d37814581a695ec1da2e8ffbbeb9b89d754ac88d7bbecbff48968853fb6bf19251a0265df162fd436b8308a5ca6db97ee3e8f6e541a"; + universal_config::init_module_for_test(VID); + init_module_for_test(); + + let dvn = &create_account_for_test(@dvn); + initialize( + dvn, + @dvn, + vector[@1234, @2234, @3234], + vector[pub_key_1], + 1, + vector[@simple_msglib], + @dvn_fee_lib_router_0, + ); + let admin = &create_signer_for_test(@1111); + set_dst_config(admin, 101, 77000, 12000, 1); + } +} diff --git a/packages/layerzero-v2/initia/contracts/workers/dvn/tests/hashes_test.move b/packages/layerzero-v2/initia/contracts/workers/dvn/tests/hashes_test.move new file mode 100644 index 00000000..5fbc9b3d --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/workers/dvn/tests/hashes_test.move @@ -0,0 +1,125 @@ +#[test_only] +module dvn::hashes_test { + use dvn::hashes; + use endpoint_v2_common::bytes32::to_bytes32; + + const VID: u32 = 1; + const EXPIRATION: u64 = 2000; + + #[test] + fun test_get_function_signature() { + assert!(hashes::get_function_signature(b"verify") == x"7c40a351", 0); + assert!(hashes::get_function_signature(b"set_dvn_signer") == x"1372c8d1", 1); + assert!(hashes::get_function_signature(b"set_quorum") == x"17b7ccf9", 2); + assert!(hashes::get_function_signature(b"set_allowlist") == x"934ff7eb", 3); + assert!(hashes::get_function_signature(b"set_denylist") == x"8442b40b", 4); + assert!(hashes::get_function_signature(b"quorum_change_admin") == x"73028773", 5); + } + + #[test] + fun test_create_verify_hash() { + // Test params + let packet_header = x"010000000000000001000000010000000000000000000000000000000000000000000000000000000000009099000000010000000000000000000000000000000000000000000000000000000000009099"; + let payload_hash = x"cc35f70cc84269e2bfe02824b3d69e4120e6a58302a8129c4e11d9d9777a38c0"; + let confirmations = 10; + let target = @0x0000000000000000000000000000000000000000000000000000000000009005; + // Expected results + let expected_payload = x"7c40a351010000000000000001000000010000000000000000000000000000000000000000000000000000000000009099000000010000000000000000000000000000000000000000000000000000000000009099cc35f70cc84269e2bfe02824b3d69e4120e6a58302a8129c4e11d9d9777a38c0000000000000000a00000000000000000000000000000000000000000000000000000000000090050000000100000000000007d0"; + let expected_hash = to_bytes32(x"e3e8219995b9d75e7748415b6d54235f1d1cbec86f12d6602f423b7b20353799"); + assert!( + hashes::build_verify_payload( + packet_header, + payload_hash, + confirmations, + target, + VID, + EXPIRATION, + ) == expected_payload, + 0, + ); + assert!( + hashes::create_verify_hash( + packet_header, + payload_hash, + confirmations, + target, + VID, + EXPIRATION, + ) == expected_hash, + 1, + ); + } + + #[test] + fun test_create_set_quorum_hash() { + // Test params + let quorum = 2; + // Expected results + let expected_payload = x"17b7ccf900000000000000020000000100000000000007d0"; + let expected_hash = to_bytes32(x"3064e840a7183166bb439dfb1de0f6befc2e1731efcbb90cc6178b6b42cf3584"); + assert!(hashes::build_set_quorum_payload(quorum, VID, EXPIRATION) == expected_payload, 0); + assert!(hashes::create_set_quorum_hash(quorum, VID, EXPIRATION) == expected_hash, 1); + } + + #[test] + fun test_create_set_dvn_signer_hash() { + // Test params + let dvn_signer = x"505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d"; + let active = true; + // Expected results + let expected_payload = x"1372c8d1505d1d231bb110780d1190b0a2ce9f2770350b295cbe970f127c4bc399cc406bb8c85d26b5afdbdc7316a065e4d4a3e4f27182310bf0d7c16da4b65ae787435d010000000100000000000007d0"; + let expected_hash = to_bytes32(x"ad2262753ab5dab4d29c2437dd09d5bc6bdb4632e781d424f031d5cd5970728b"); + assert!(hashes::build_set_dvn_signer_payload(dvn_signer, active, VID, EXPIRATION) == expected_payload, 0); + assert!(hashes::create_set_dvn_signer_hash(dvn_signer, active, VID, EXPIRATION) == expected_hash, 1); + } + + #[test] + fun test_create_set_allowlist_hash() { + // Test params + let sender = @9988; + let allowed = true; + // Expected results + let expected_payload = x"934ff7eb0000000000000000000000000000000000000000000000000000000000002704010000000100000000000007d0"; + let expected_hash = to_bytes32(x"d418cb1c18cfd5d3fc1fbdac34a9d71fb4b56a8f2d074e36d497c8e0489c7a15"); + assert!(hashes::build_set_allowlist_payload(sender, allowed, VID, EXPIRATION) == expected_payload, 0); + assert!(hashes::create_set_allowlist_hash(sender, allowed, VID, EXPIRATION) == expected_hash, 1); + } + + #[test] + fun test_create_set_denylist_hash() { + // Test params + let sender = @9988; + let denied = true; + // Expected results + let expected_payload = x"8442b40b0000000000000000000000000000000000000000000000000000000000002704010000000100000000000007d0"; + let expected_hash = to_bytes32(x"67f702d40c8e4bd7c2d59cc2d772b0a8c2398a08336176f38118fd0f33704817"); + assert!(hashes::build_set_denylist_payload(sender, denied, VID, EXPIRATION) == expected_payload, 0); + assert!(hashes::create_set_denylist_hash(sender, denied, VID, EXPIRATION) == expected_hash, 1); + } + + #[test] + fun test_create_quorum_change_admin_hash() { + // Test params + let admin = @0x0000000000000000000000000000000000000000000000000000000000002704; + let active = true; + // Expected results + let expected_payload = x"730287730000000000000000000000000000000000000000000000000000000000002704010000000100000000000007d0"; + let expected_hash = to_bytes32(x"c8ee741967867d5a99e739baa2a57c8b480a438855a4fc0d7b5ea2e28a8deaa5"); + assert!( + hashes::build_quorum_change_admin_payload(admin, active, VID, EXPIRATION) == expected_payload, + 0, + ); + assert!(hashes::create_quorum_change_admin_hash(admin, active, VID, EXPIRATION) == expected_hash, 1); + } + + #[test] + fun test_create_set_msglibs_hash() { + // Test params + let msglibs = vector
[@1234, @2345]; + // Expected results + let expected_payload = x"6456530e00000000000000000000000000000000000000000000000000000000000004d200000000000000000000000000000000000000000000000000000000000009290000000100000000000007d0"; + let expected_hash = to_bytes32(x"8796cf58bf29e0d42e08c3e9b3544b451c1881223d2d3e76ad6e39ff1ba8ec8b"); + assert!(hashes::build_set_msglibs_payload(msglibs, VID, EXPIRATION) == expected_payload, 0); + assert!(hashes::create_set_msglibs_hash(msglibs, VID, EXPIRATION) == expected_hash, 1); + } +} \ No newline at end of file diff --git a/packages/layerzero-v2/initia/contracts/workers/executor/Move.toml b/packages/layerzero-v2/initia/contracts/workers/executor/Move.toml new file mode 100644 index 00000000..6a04517f --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/workers/executor/Move.toml @@ -0,0 +1,25 @@ +[package] +name = "executor" +version = "1.0.0" +authors = [] + +[addresses] +executor = "_" +endpoint_v2 = "_" +endpoint_v2_common = "_" +layerzero_admin = "_" +layerzero_treasury_admin = "_" +worker_common = "_" +native_token_metadata_address = "0x8e4733bdabcf7d4afc3d14f0dd46c9bf52fb0fce9e4b996c939e195b8bc891d9" + +[dev-addresses] +executor = "0x6001" +endpoint_v2 = "0x13241234" +endpoint_v2_common = "0x9098" +layerzero_admin = "0x18943124" +layerzero_treasury_admin = "0x1894312499" +worker_common = "0x3999" + +[dependencies] +endpoint_v2_common = { local = "../../endpoint_v2_common" } +worker_common = { local = "../../worker_peripherals/worker_common" } diff --git a/packages/layerzero-v2/initia/contracts/workers/executor/sources/executor.move b/packages/layerzero-v2/initia/contracts/workers/executor/sources/executor.move new file mode 100644 index 00000000..9de9c330 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/workers/executor/sources/executor.move @@ -0,0 +1,442 @@ +module executor::executor { + use std::event::emit; + use std::fungible_asset::{Self, Metadata}; + use std::object::address_to_object; + use std::primary_fungible_store; + use std::signer::address_of; + use std::vector; + + use endpoint_v2_common::contract_identity::{ + CallRef, + ContractSigner, + create_contract_signer, + make_call_ref, + }; + use endpoint_v2_common::native_token; + use executor::native_drop_params::{ + calculate_total_amount, deserialize_native_drop_params, NativeDropParams, unpack_native_drop_params, + }; + use worker_common::worker_config::{Self, WORKER_ID_EXECUTOR}; + + struct ExecutorConfig has key { + contract_signer: ContractSigner, + } + + fun init_module(account: &signer) { + move_to(account, ExecutorConfig { contract_signer: create_contract_signer(account) }); + } + + #[test_only] + public fun init_module_for_test() { + let account = &std::account::create_signer_for_test(@executor); + init_module(account); + } + + /// Initializes the executor + /// This function must be called before using the executor + /// + /// @param account the signer of the transaction + /// @param role_admin the address of the default admin + /// @param admins the list of admins + /// @param suppored_msglibs the list of supported msglibs + /// @param fee_lib the fee lib to use + /// @param price_feed the price feed to use + /// @param feed_address the address of the feed within the module + /// @return the total fee and the deposit address + /// @dev this function must be called before using the executor + public entry fun initialize( + account: &signer, + deposit_address: address, + role_admin: address, + admins: vector
, + supported_msglibs: vector
, + fee_lib: address, + ) { + assert!(address_of(account) == @executor, EUNAUTHORIZED); + worker_config::initialize_for_worker( + account, + WORKER_ID_EXECUTOR(), + deposit_address, + role_admin, + admins, + supported_msglibs, + fee_lib, + ); + } + + // ================================================ Role Admin Only =============================================== + + /// Sets the role admin for the executor + /// A role admin can only be set by another role admin + public entry fun set_role_admin( + account: &signer, + role_admin: address, + active: bool, + ) acquires ExecutorConfig { + assert_role_admin(address_of(move account)); + worker_config::set_worker_role_admin(call_ref(), role_admin, active); + } + + /// Sets the fee lib for the executor + public entry fun set_admin( + account: &signer, + admin: address, + active: bool, + ) acquires ExecutorConfig { + assert_role_admin(address_of(move account)); + worker_config::set_worker_admin(call_ref(), admin, active); + } + + /// Pauses or unpauses the executor + public entry fun set_pause(account: &signer, pause: bool) acquires ExecutorConfig { + assert_role_admin(address_of(move account)); + worker_config::set_worker_pause(call_ref(), pause); + } + + // ================================================== Admin Only ================================================== + + /// Applies a native drop + /// This is intended to be called by the executor in concert with lz_receive (on the OApp), either separately from + /// the offchain or in a ad-hoc script. + /// This call will withdraw the total amount from the executor's primary fungible store and distribute it to the + /// receivers specified in the params. + public entry fun native_drop( + account: &signer, + src_eid: u32, + sender: vector, + nonce: u64, + dst_eid: u32, + oapp: address, + serialized_params: vector, + ) { + assert_admin(address_of(account)); + + let params = deserialize_native_drop_params(&serialized_params); + let total_amount = calculate_total_amount(params); + let metadata = address_to_object(@native_token_metadata_address); + + // Last signer use + let fa_total = native_token::withdraw(account, total_amount); + + for (i in 0..vector::length(¶ms)) { + let (receiver, amount) = unpack_native_drop_params(*vector::borrow(¶ms, i)); + let receiver_store = primary_fungible_store::ensure_primary_store_exists(receiver, metadata); + let fa = fungible_asset::extract(&mut fa_total, amount); + fungible_asset::deposit(receiver_store, fa); + }; + fungible_asset::destroy_zero(fa_total); + + emit(NativeDropApplied { + src_eid, + sender, + nonce, + dst_eid, + oapp, + params, + }); + } + + public fun emit_lz_receive_value_provided( + admin: &signer, + receiver: address, + src_eid: u32, + sender: vector, + nonce: u64, + guid: vector, + lz_receive_value: u64, + ) { + assert_admin(address_of(move admin)); + emit(LzReceiveValueProvided { receiver, src_eid, sender, nonce, guid, lz_receive_value }); + } + + public fun emit_lz_compose_value_provided( + admin: &signer, + from: address, + to: address, + index: u16, + guid: vector, + lz_compose_value: u64, + ) { + assert_admin(address_of(move admin)); + emit(LzComposeValueProvided { from, to, index, guid, lz_compose_value }); + } + + /// Sets the deposit address to which the fee lib will send payment for the executor + public entry fun set_deposit_address(account: &signer, deposit_address: address) acquires ExecutorConfig { + assert_admin(address_of(move account)); + worker_config::set_deposit_address(call_ref(), deposit_address); + } + + /// Sets the multiplier premium for the executor + public entry fun set_default_multiplier_bps(account: &signer, default_multiplier_bps: u16) acquires ExecutorConfig { + assert_admin(address_of(move account)); + worker_config::set_default_multiplier_bps(call_ref(), default_multiplier_bps); + } + + /// Sets the supported option types for the executor + public entry fun set_supported_option_types(account: &signer, option_types: vector) acquires ExecutorConfig { + assert_admin(address_of(move account)); + worker_config::set_supported_option_types(call_ref(), option_types); + } + + /// Sets the destination config for an eid on the executor + /// + /// @param account the signer of the transaction (must be admin) + /// @param remote_eid the destination eid + /// @param lz_receive_base_gas the base gas for lz receive + /// @param multiplier_bps the multiplier in basis points + /// @param floor_margin_usd the floor margin in USD + /// @param native_cap the native cap + /// @param lz_compose_base_gas the base gas for lz compose + public entry fun set_dst_config( + account: &signer, + remote_eid: u32, + lz_receive_base_gas: u64, + multiplier_bps: u16, + floor_margin_usd: u128, + native_cap: u128, + lz_compose_base_gas: u64, + ) acquires ExecutorConfig { + assert_admin(address_of(move account)); + worker_config::set_executor_dst_config( + call_ref(), + remote_eid, + lz_receive_base_gas, + multiplier_bps, + floor_margin_usd, + native_cap, + lz_compose_base_gas, + ); + } + + + #[view] + /// Checks whether the executor is paused + public fun is_paused(): bool { + worker_config::is_worker_paused(@executor) + } + + /// Sets the price feed module address and the feed address for the executor + public entry fun set_price_feed( + account: &signer, + price_feed: address, + feed_address: address, + ) acquires ExecutorConfig { + assert_admin(address_of(move account)); + worker_config::set_price_feed(call_ref(), price_feed, feed_address); + } + + /// Sets a price feed delegate for the executor + /// This is used to allow the executor delegate to the configuration defined in another module + /// When there is a delegation, the worker_config get_effective_price_feed will return the price feed of the + /// delegate + public entry fun set_price_feed_delegate(account: &signer, price_feed_delegate: address) acquires ExecutorConfig { + assert_admin(address_of(move account)); + worker_config::set_price_feed_delegate(call_ref(), price_feed_delegate); + } + + /// Add (allowed = true) or remove (allowed = false) a sender to/from the allowlist + /// A non-empty allowlist restrict senders to only those on the list (minus any on the denylist) + public entry fun set_allowlist( + account: &signer, + oapp: address, + allowed: bool, + ) acquires ExecutorConfig { + assert_admin(address_of(move account)); + worker_config::set_allowlist(call_ref(), oapp, allowed); + } + + /// Add (denied = true) or remove (denied = false) a sender from the denylist + /// Denylist members will be disallowed from interacting with the executor regardless of allowlist status + public entry fun set_denylist( + account: &signer, + oapp: address, + denied: bool, + ) acquires ExecutorConfig { + assert_admin(address_of(move account)); + worker_config::set_denylist(call_ref(), oapp, denied); + } + + /// Sets the supported message libraries for the executor + /// The provided list will entirely replace the previously configured list + public entry fun set_supported_msglibs( + account: &signer, + msglibs: vector
, + ) acquires ExecutorConfig { + assert_admin(address_of(move account)); + worker_config::set_supported_msglibs(call_ref(), msglibs); + } + + /// Sets the fee lib for the executor + public entry fun set_fee_lib( + account: &signer, + fee_lib: address, + ) acquires ExecutorConfig { + assert_admin(address_of(move account)); + worker_config::set_worker_fee_lib(call_ref(), fee_lib); + } + + // ================================================ View Functions ================================================ + + #[view] + /// Gets the list of role admins for the executor + public fun get_role_admins(): vector
{ + worker_config::get_worker_role_admins(@executor) + } + + #[view] + /// Gets the list of admins for the executor + public fun get_admins(): vector
{ + worker_config::get_worker_admins(@executor) + } + + #[view] + /// Gets the deposit address for the executor + public fun get_deposit_address(): address { + worker_config::get_deposit_address(@executor) + } + + #[view] + /// Gets the list of supported message libraries for the executor + public fun get_supported_msglibs(): vector
{ worker_config::get_supported_msglibs(@executor) } + + #[view] + /// Gets the fee lib address selected by the executor + public fun get_fee_lib(): address { + worker_config::get_worker_fee_lib(@executor) + } + + #[view] + /// Gets multiplier premium for the executor + public fun get_default_multiplier_bps(): u16 { + worker_config::get_default_multiplier_bps(@executor) + } + + #[view] + /// Gets the supported option types for the executor + public fun get_supported_option_types(): vector { + worker_config::get_supported_option_types(@executor) + } + + #[view] + /// Returns whether a sender is on the executor's allowlist + public fun allowlist_contains(sender: address): bool { worker_config::allowlist_contains(@executor, sender) } + + #[view] + /// Returns whether a sender is on the executor's denylist + public fun denylist_contains(sender: address): bool { worker_config::denylist_contains(@executor, sender) } + + #[view] + /// Returns whether a sender is allowed to interact with the executor based on the allowlist and denylist + public fun is_allowed(sender: address): bool { worker_config::is_allowed(@executor, sender) } + + #[view] + /// Gets the destination config for an eid on the executor + /// @return the lz_receive_base_gas, multiplier_bps, floor_margin_usd, native_cap, lz_compose_base_gas + public fun get_dst_config(dst_eid: u32): (u64, u16, u128, u128, u64) { + worker_config::get_executor_dst_config_values(@executor, dst_eid) + } + + #[view] + /// Get the number of other workers that are currently delegating to this executor's price feed configuration + public fun get_count_price_feed_delegate_dependents(): u64 { + worker_config::get_count_price_feed_delegate_dependents(@executor) + } + + // ================================================ Helper Functions ================================================ + + /// Asserts that an an address is a role admin + inline fun assert_role_admin(role_admin: address) { + worker_config::assert_worker_role_admin(@executor, role_admin); + } + + /// Asserts that an address is an admin + inline fun assert_admin(admin: address) { + worker_config::assert_worker_admin(@executor, admin); + } + + /// Derives the call ref targetting the worker_common module + inline fun call_ref(): &CallRef { + let contract_signer = &borrow_global(@executor).contract_signer; + &make_call_ref(contract_signer) + } + + // ==================================================== Events ==================================================== + + #[event] + /// Emits when a Native Drop is Applied + struct NativeDropApplied has store, drop { + src_eid: u32, + sender: vector, + nonce: u64, + dst_eid: u32, + oapp: address, + params: vector, + } + + #[event] + struct LzReceiveValueProvided has store, drop { + receiver: address, + src_eid: u32, + sender: vector, + nonce: u64, + guid: vector, + lz_receive_value: u64, + } + + #[event] + struct LzComposeValueProvided has store, drop { + from: address, + to: address, + index: u16, + guid: vector, + lz_compose_value: u64, + } + + #[test_only] + /// Creates a NativeDropApplied event for testing + public fun native_drop_applied_event( + src_eid: u32, + sender: vector, + nonce: u64, + dst_eid: u32, + oapp: address, + params: vector, + ): NativeDropApplied { + NativeDropApplied { + src_eid, + sender, + nonce, + dst_eid, + oapp, + params, + } + } + + #[test_only] + public fun lz_receive_value_provided_event( + receiver: address, + src_eid: u32, + sender: vector, + nonce: u64, + guid: vector, + lz_receive_value: u64, + ): LzReceiveValueProvided { + LzReceiveValueProvided { receiver, src_eid, sender, nonce, guid, lz_receive_value } + } + + #[test_only] + public fun lz_compose_value_provided_event( + from: address, + to: address, + index: u16, + guid: vector, + lz_compose_value: u64, + ): LzComposeValueProvided { + LzComposeValueProvided { from, to, index, guid, lz_compose_value } + } + + // ================================================== Error Codes ================================================= + + const EUNAUTHORIZED: u64 = 1; +} diff --git a/packages/layerzero-v2/initia/contracts/workers/executor/sources/types/native_drop_params.move b/packages/layerzero-v2/initia/contracts/workers/executor/sources/types/native_drop_params.move new file mode 100644 index 00000000..19df1c94 --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/workers/executor/sources/types/native_drop_params.move @@ -0,0 +1,52 @@ +module executor::native_drop_params { + use std::vector; + + use endpoint_v2_common::serde; + + struct NativeDropParams has copy, store, drop { + receiver: address, + amount: u64, + } + + public fun new_native_drop_params(receiver: address, amount: u64): NativeDropParams { + NativeDropParams { receiver, amount } + } + + public fun unpack_native_drop_params(params: NativeDropParams): (address, u64) { + let NativeDropParams { receiver, amount } = params; + (receiver, amount) + } + + public fun deserialize_native_drop_params(params_serialized: &vector): vector { + let params = vector[]; + let pos = 0; + let len = vector::length(params_serialized); + while (pos < len) { + let receiver = serde::extract_address(params_serialized, &mut pos); + let amount = serde::extract_u64(params_serialized, &mut pos); + vector::push_back(&mut params, new_native_drop_params(receiver, amount)); + }; + params + } + + public fun serialize_native_drop_params(params: vector): vector { + let params_serialized = vector[]; + for (i in 0..vector::length(¶ms)) { + let item = vector::borrow(¶ms, i); + let receiver = item.receiver; + let amount = item.amount; + serde::append_address(&mut params_serialized, receiver); + serde::append_u64(&mut params_serialized, amount); + }; + params_serialized + } + + public fun calculate_total_amount(params: vector): u64 { + let total_amount = 0; + for (i in 0..vector::length(¶ms)) { + let item = vector::borrow(¶ms, i); + total_amount = total_amount + item.amount; + }; + total_amount + } +} diff --git a/packages/layerzero-v2/initia/contracts/workers/executor/tests/executor_tests.move b/packages/layerzero-v2/initia/contracts/workers/executor/tests/executor_tests.move new file mode 100644 index 00000000..cfc8b02e --- /dev/null +++ b/packages/layerzero-v2/initia/contracts/workers/executor/tests/executor_tests.move @@ -0,0 +1,283 @@ +#[test_only] +module executor::executor_tests { + use std::account::{create_account_for_test, create_signer_for_test}; + use std::event::was_event_emitted; + use std::fungible_asset; + use std::fungible_asset::Metadata; + use std::object::address_to_object; + use std::primary_fungible_store::ensure_primary_store_exists; + + use endpoint_v2_common::bytes32::{Self, from_bytes32}; + use endpoint_v2_common::native_token_test_helpers::mint_native_token_for_test; + use executor::executor; + use executor::executor::{get_fee_lib, native_drop, native_drop_applied_event}; + use executor::native_drop_params; + use executor::native_drop_params::new_native_drop_params; + use worker_common::worker_config; + + const EXECUTOR_FEE_LIB: address = @100000009; + const PRICE_FEED_MODULE: address = @100000010; + + #[test] + fun test_native_drop() { + // initialize worker + executor::init_module_for_test(); + executor::initialize( + &create_account_for_test(@executor), + @executor, + @111111111, + vector[@9999], + vector[], + EXECUTOR_FEE_LIB, + ); + + let params = vector[ + new_native_drop_params(@1111, 10), + new_native_drop_params(@2222, 20), + new_native_drop_params(@3333, 30), + ]; + let fa = mint_native_token_for_test(1000); + let metadata = address_to_object(@native_token_metadata_address); + let admin_store = ensure_primary_store_exists(@9999, metadata); + fungible_asset::deposit(admin_store, fa); + + let admin = &create_signer_for_test(@9999); + native_drop( + admin, + 10101, + bytes32::from_bytes32(bytes32::from_address(@12345)), + 10, + 10202, + @50000, + native_drop_params::serialize_native_drop_params(params), + ); + + assert!(fungible_asset::balance(admin_store) == 940, 0); + assert!(was_event_emitted(&native_drop_applied_event( + 10101, from_bytes32(bytes32::from_address(@12345)), 10, 10202, @50000, params)), 0); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::EUNAUTHORIZED)] + fun test_native_drop_fails_if_not_admin() { + // initialize worker + executor::init_module_for_test(); + executor::initialize( + &create_account_for_test(@executor), + @executor, + @111111111, + vector[@9999], + vector[], + EXECUTOR_FEE_LIB, + ); + + let params = vector[ + new_native_drop_params(@1111, 10), + new_native_drop_params(@2222, 20), + new_native_drop_params(@3333, 30), + ]; + let fa = mint_native_token_for_test(1000); + let metadata = address_to_object(@native_token_metadata_address); + let admin_store = ensure_primary_store_exists(@9999, metadata); + fungible_asset::deposit(admin_store, fa); + + let admin = &create_signer_for_test(@9900); + native_drop( + admin, + 10101, + bytes32::from_bytes32(bytes32::from_address(@12345)), + 10, + 10202, + @50000, + native_drop_params::serialize_native_drop_params(params), + ); + } + + #[test] + #[expected_failure(abort_code = 0x10004, location = std::fungible_asset)] + fun test_native_drop_fails_if_insufficient_balance() { + // initialize worker + executor::init_module_for_test(); + executor::initialize( + &create_account_for_test(@executor), + @executor, + @111111111, + vector[@9999], + vector[], + EXECUTOR_FEE_LIB, + ); + + let params = vector[ + new_native_drop_params(@1111, 10), + new_native_drop_params(@2222, 20), + new_native_drop_params(@3333, 30), + ]; + let fa = mint_native_token_for_test(1); + let metadata = address_to_object(@native_token_metadata_address); + let admin_store = ensure_primary_store_exists(@9999, metadata); + fungible_asset::deposit(admin_store, fa); + + let admin = &create_signer_for_test(@9999); + native_drop( + admin, + 10101, + bytes32::from_bytes32(bytes32::from_address(@12345)), + 10, + 10202, + @50000, + native_drop_params::serialize_native_drop_params(params), + ); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::EUNAUTHORIZED)] + fun test_set_price_feed_should_fail_if_not_admin() { + // initialize worker + executor::init_module_for_test(); + executor::initialize( + &create_account_for_test(@executor), + @executor, + @111111111, + vector[@9999], + vector[], + EXECUTOR_FEE_LIB, + ); + + let admin = &create_signer_for_test(@8888); + executor::set_price_feed(admin, PRICE_FEED_MODULE, @1111); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::EUNAUTHORIZED)] + fun test_set_price_feed_delegate_should_fail_if_not_admin() { + // initialize worker + executor::init_module_for_test(); + executor::initialize( + &create_account_for_test(@executor), + @executor, + @111111111, + vector[@9999], + vector[], + EXECUTOR_FEE_LIB, + ); + + let admin = &create_signer_for_test(@8888); + executor::set_price_feed_delegate(admin, PRICE_FEED_MODULE); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::EUNAUTHORIZED)] + fun test_set_allowlist_should_fail_if_not_admin() { + // initialize worker + executor::init_module_for_test(); + executor::initialize( + &create_account_for_test(@executor), + @executor, + @111111111, + vector[@9999], + vector[], + EXECUTOR_FEE_LIB, + ); + + let admin = &create_signer_for_test(@8888); + executor::set_allowlist(admin, @1234, true); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::EUNAUTHORIZED)] + fun test_set_denylist_should_fail_is_not_admin() { + // initialize worker + executor::init_module_for_test(); + executor::initialize( + &create_account_for_test(@executor), + @executor, + @111111111, + vector[@9999], + vector[], + EXECUTOR_FEE_LIB, + ); + + let admin = &create_signer_for_test(@8888); + executor::set_denylist(admin, @1234, true); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::EUNAUTHORIZED)] + fun test_set_supported_msglibs_should_fail_if_not_admin() { + // initialize worker + executor::init_module_for_test(); + executor::initialize( + &create_account_for_test(@executor), + @executor, + @111111111, + vector[@9999], + vector[], + EXECUTOR_FEE_LIB, + ); + + let admin = &create_signer_for_test(@8888); + executor::set_supported_msglibs(admin, vector[@1234]); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::EUNAUTHORIZED)] + fun set_admin_should_fail_if_not_role_admin() { + // initialize worker + executor::init_module_for_test(); + executor::initialize( + &create_account_for_test(@executor), + @executor, + @111111111, + vector[@9999], + vector[], + EXECUTOR_FEE_LIB, + ); + + let admin = &create_signer_for_test(@9999); + executor::set_admin(admin, @8888, true); + } + + #[test] + #[expected_failure(abort_code = worker_common::worker_config::EUNAUTHORIZED)] + fun set_role_admin_should_fail_if_not_role_admin() { + // initialize worker + executor::init_module_for_test(); + executor::initialize( + &create_account_for_test(@executor), + @executor, + @111111111, + vector[@9999], + vector[], + EXECUTOR_FEE_LIB, + ); + + let admin = &create_signer_for_test(@9999); + executor::set_role_admin(admin, @8888, true); + } + + #[test] + fun set_fee_lib() { + // initialize worker + executor::init_module_for_test(); + executor::initialize( + &create_account_for_test(@executor), + @executor, + @111111111, + vector[@9999], + vector[], + EXECUTOR_FEE_LIB, + ); + let fee_lib_from_worker_config = worker_config::get_worker_fee_lib(@executor); + assert!(fee_lib_from_worker_config == EXECUTOR_FEE_LIB, 0); + let fee_lib_from_executor = get_fee_lib(); + assert!(fee_lib_from_executor == EXECUTOR_FEE_LIB, 0); + + let admin = &create_signer_for_test(@9999); + executor::set_fee_lib(admin, @1111); + assert!(was_event_emitted(&worker_config::worker_fee_lib_updated_event(@executor, @1111)), 0); + let fee_lib_from_worker_config = worker_config::get_worker_fee_lib(@executor); + assert!(fee_lib_from_worker_config == @1111, 0); + let fee_lib_from_executor = get_fee_lib(); + assert!(fee_lib_from_executor == @1111, 0); + } +}