diff --git a/Cargo.lock b/Cargo.lock index 26c87d941..975be0835 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9189,6 +9189,7 @@ dependencies = [ "valence-program-registry-utils", "valence-reverse-splitter-library", "valence-splitter-library", + "valence-union-transfer", ] [[package]] @@ -9362,6 +9363,29 @@ dependencies = [ "valence-processor-utils", ] +[[package]] +name = "valence-union-transfer" +version = "0.2.0" +dependencies = [ + "alloy-primitives 0.7.7", + "alloy-sol-types 0.7.7", + "cosmwasm-schema 2.2.1", + "cosmwasm-std 2.2.1", + "cw-multi-test", + "cw-ownable", + "cw-storage-plus 2.0.0", + "cw-utils 2.0.0", + "cw20 2.0.0", + "schemars", + "serde", + "sha2 0.10.8", + "thiserror 1.0.69", + "valence-account-utils", + "valence-library-base", + "valence-library-utils", + "valence-macros", +] + [[package]] name = "valuable" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index a67922b0b..99d4ebc99 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,6 +88,7 @@ valence-drop-liquid-staker = { path = "contracts/libraries/drop-liquid valence-drop-liquid-unstaker = { path = "contracts/libraries/drop-liquid-unstaker", features = ["library"] } valence-ica-cctp-transfer = { path = "contracts/libraries/ica-cctp-transfer", features = ["library"] } valence-ica-ibc-transfer = { path = "contracts/libraries/ica-ibc-transfer", features = ["library"] } +valence-union-transfer = { path = "contracts/libraries/union-transfer", features = ["library"] } # middleware valence-middleware-osmosis = { path = "contracts/middleware/type-registries/osmosis/osmo-26-0-0", features = [ diff --git a/contracts/libraries/union-transfer/.cargo/config.toml b/contracts/libraries/union-transfer/.cargo/config.toml new file mode 100644 index 000000000..e03b96d17 --- /dev/null +++ b/contracts/libraries/union-transfer/.cargo/config.toml @@ -0,0 +1,3 @@ +[alias] +wasm = "build --release --lib --target wasm32-unknown-unknown" +schema = "run --bin schema" diff --git a/contracts/libraries/union-transfer/Cargo.toml b/contracts/libraries/union-transfer/Cargo.toml new file mode 100644 index 000000000..4310e91a5 --- /dev/null +++ b/contracts/libraries/union-transfer/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "valence-union-transfer" +authors = { workspace = true } +edition = { workspace = true } +license = { workspace = true } +version = { workspace = true } +repository = { workspace = true } + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +cw-ownable = { workspace = true } +cw-storage-plus = { workspace = true } +cw-utils = { workspace = true } +cw20 = { workspace = true } +schemars = { workspace = true } +serde = { workspace = true } +thiserror = { workspace = true } +valence-macros = { workspace = true } +valence-library-utils = { workspace = true } +valence-library-base = { workspace = true } +alloy-sol-types = "0.7.7" +alloy-primitives = "0.7.7" +sha2 = { workspace = true } + +[dev-dependencies] +cw-multi-test = { workspace = true } +valence-account-utils = { workspace = true } +valence-library-utils = { workspace = true, features = ["testing"] } diff --git a/contracts/libraries/union-transfer/README.md b/contracts/libraries/union-transfer/README.md new file mode 100644 index 000000000..7cb24255e --- /dev/null +++ b/contracts/libraries/union-transfer/README.md @@ -0,0 +1,81 @@ +# Valence Union Transfer library + +The **Valence Union Transfer** library allows to transfer funds over [Union](https://union.build/) from an **input account** on a source chain to an **output account** on a destination chain. It is typically used as part of a **Valence Program**. In that context, a **Processor** contract will be the main contract interacting with the Forwarder library. + + +## High-level flow + +```mermaid +--- +title: Union Transfer Library +--- +graph LR + IA((Input Account)) + ZG((zkGM)) + R((Recipient)) + P[Processor] + U[Union Transfer + Library] + UTM[Union Token + Protocol] + subgraph EVM[ EVM Chain ] + UTM -- 6/Send tokens --> R + end + subgraph CW[ CosmWasm Domain ] + P -- 1/Call + transfer(quote_amount) --> U + U -- 2/Query CW20/Native + token balance --> IA + U -- 3/Call approve (if applies) and send --> IA + IA -- 4/Approve CW20 (if applies) --> ZG + IA -- 5/Call send with instructions --> ZG + end + CW --- EVM +``` + +## Configuration + +The library is configured on instantiation via the `LibraryConfig` type. + +```rust +pub struct LibraryConfig { + pub input_addr: LibraryAccountType, + pub output_addr: LibraryAccountType, + pub denom: UncheckedUnionDenomConfig, + pub amount: TransferAmount, + // Information about the asset to be transferred. + pub input_asset_name: String, + pub input_asset_symbol: String, + pub input_asset_decimals: u8, + pub input_asset_token_path: Uint256, + // Information about the asset to be received. + pub quote_token: String, + pub quote_amount: Uint256, + // Information about the remote chain. + pub channel_id: u64, + pub transfer_timeout: Option, // If not provided, a default 3 days will be used (259200 seconds). + // Information about the protocol + pub zkgm_contract: String, // The address of the ZKGM contract that we will interact with + // They are using a batch operation with a transfer (FungibleAssetOrder) operation inside, so we need the version for both instructions. + // If not provided, we will use the versions currently used by the protocol, but this is meant to be used for future upgrades. + pub batch_instruction_version: Option, // The version of the batch instruction to be used. If not provided, the current default version will be used. + pub transfer_instruction_version: Option, // The version of the transfer instruction to be used. If not provided, the current default version will be used. +} + +pub enum UncheckedUnionDenomConfig { + /// A native (bank module) asset. + Native(String), + /// A cw20 asset along with the token minter address that needs to be approved for spending during transfers. + Cw20(UncheckedUnionCw20Config), +} + +pub struct UncheckedUnionCw20Config { + pub token: String, + pub minter: String, +} + +pub enum TransferAmount { + FullAmount, + FixedAmount(Uint128), +} +``` diff --git a/contracts/libraries/union-transfer/schema/valence-union-transfer.json b/contracts/libraries/union-transfer/schema/valence-union-transfer.json new file mode 100644 index 000000000..13b7ba65b --- /dev/null +++ b/contracts/libraries/union-transfer/schema/valence-union-transfer.json @@ -0,0 +1,1288 @@ +{ + "contract_name": "valence-union-transfer", + "contract_version": "0.2.0", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "config", + "owner", + "processor" + ], + "properties": { + "config": { + "$ref": "#/definitions/LibraryConfig" + }, + "owner": { + "type": "string" + }, + "processor": { + "type": "string" + } + }, + "additionalProperties": false, + "definitions": { + "LibraryAccountType": { + "description": "A helper type that is used to associate an account or library with an id When a program is not instantiated yet, ids will be used to reference accounts and libraries When a program is instantiated, the ids will be replaced by the instantiated addresses", + "oneOf": [ + { + "type": "object", + "required": [ + "|library_account_addr|" + ], + "properties": { + "|library_account_addr|": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "|account_id|" + ], + "properties": { + "|account_id|": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "|library_id|" + ], + "properties": { + "|library_id|": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + ] + }, + "LibraryConfig": { + "type": "object", + "required": [ + "amount", + "channel_id", + "denom", + "input_addr", + "input_asset_decimals", + "input_asset_name", + "input_asset_symbol", + "input_asset_token_path", + "output_addr", + "quote_amount", + "quote_token", + "zkgm_contract" + ], + "properties": { + "amount": { + "$ref": "#/definitions/TransferAmount" + }, + "batch_instruction_version": { + "type": [ + "integer", + "null" + ], + "format": "uint8", + "minimum": 0.0 + }, + "channel_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "denom": { + "$ref": "#/definitions/UncheckedUnionDenomConfig" + }, + "input_addr": { + "$ref": "#/definitions/LibraryAccountType" + }, + "input_asset_decimals": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, + "input_asset_name": { + "type": "string" + }, + "input_asset_symbol": { + "type": "string" + }, + "input_asset_token_path": { + "$ref": "#/definitions/Uint256" + }, + "output_addr": { + "$ref": "#/definitions/LibraryAccountType" + }, + "quote_amount": { + "$ref": "#/definitions/Uint256" + }, + "quote_token": { + "type": "string" + }, + "transfer_instruction_version": { + "type": [ + "integer", + "null" + ], + "format": "uint8", + "minimum": 0.0 + }, + "transfer_timeout": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "zkgm_contract": { + "type": "string" + } + }, + "additionalProperties": false + }, + "TransferAmount": { + "oneOf": [ + { + "type": "string", + "enum": [ + "full_amount" + ] + }, + { + "type": "object", + "required": [ + "fixed_amount" + ], + "properties": { + "fixed_amount": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + }, + "Uint256": { + "description": "An implementation of u256 that is using strings for JSON encoding/decoding, such that the full u256 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances out of primitive uint types or `new` to provide big endian bytes:\n\n``` # use cosmwasm_std::Uint256; let a = Uint256::from(258u128); let b = Uint256::new([ 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 1u8, 2u8, ]); assert_eq!(a, b); ```", + "type": "string" + }, + "UncheckedUnionCw20Config": { + "type": "object", + "required": [ + "minter", + "token" + ], + "properties": { + "minter": { + "type": "string" + }, + "token": { + "type": "string" + } + }, + "additionalProperties": false + }, + "UncheckedUnionDenomConfig": { + "oneOf": [ + { + "description": "A native (bank module) asset.", + "type": "object", + "required": [ + "native" + ], + "properties": { + "native": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "description": "A cw20 asset along with the token minter address that needs to be approved for spending during transfers.", + "type": "object", + "required": [ + "cw20" + ], + "properties": { + "cw20": { + "$ref": "#/definitions/UncheckedUnionCw20Config" + } + }, + "additionalProperties": false + } + ] + } + } + }, + "execute": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "process_function" + ], + "properties": { + "process_function": { + "$ref": "#/definitions/FunctionMsgs" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "update_config" + ], + "properties": { + "update_config": { + "type": "object", + "required": [ + "new_config" + ], + "properties": { + "new_config": { + "$ref": "#/definitions/LibraryConfigUpdate" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "update_processor" + ], + "properties": { + "update_processor": { + "type": "object", + "required": [ + "processor" + ], + "properties": { + "processor": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Update the contract's ownership. The `action` to be provided can be either to propose transferring ownership to an account, accept a pending ownership transfer, or renounce the ownership permanently.", + "type": "object", + "required": [ + "update_ownership" + ], + "properties": { + "update_ownership": { + "$ref": "#/definitions/Action" + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Action": { + "description": "Actions that can be taken to alter the contract's ownership", + "oneOf": [ + { + "description": "Propose to transfer the contract's ownership to another account, optionally with an expiry time.\n\nCan only be called by the contract's current owner.\n\nAny existing pending ownership transfer is overwritten.", + "type": "object", + "required": [ + "transfer_ownership" + ], + "properties": { + "transfer_ownership": { + "type": "object", + "required": [ + "new_owner" + ], + "properties": { + "expiry": { + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "new_owner": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Accept the pending ownership transfer.\n\nCan only be called by the pending owner.", + "type": "string", + "enum": [ + "accept_ownership" + ] + }, + { + "description": "Give up the contract's ownership and the possibility of appointing a new owner.\n\nCan only be invoked by the contract's current owner.\n\nAny existing pending ownership transfer is canceled.", + "type": "string", + "enum": [ + "renounce_ownership" + ] + } + ] + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "FunctionMsgs": { + "oneOf": [ + { + "description": "If quote amount is provided, it will override the quote amount in the config.", + "type": "object", + "required": [ + "transfer" + ], + "properties": { + "transfer": { + "type": "object", + "properties": { + "quote_amount": { + "anyOf": [ + { + "$ref": "#/definitions/Uint256" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "LibraryAccountType": { + "description": "A helper type that is used to associate an account or library with an id When a program is not instantiated yet, ids will be used to reference accounts and libraries When a program is instantiated, the ids will be replaced by the instantiated addresses", + "oneOf": [ + { + "type": "object", + "required": [ + "|library_account_addr|" + ], + "properties": { + "|library_account_addr|": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "|account_id|" + ], + "properties": { + "|account_id|": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "|library_id|" + ], + "properties": { + "|library_id|": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + ] + }, + "LibraryConfigUpdate": { + "type": "object", + "required": [ + "batch_instruction_version", + "transfer_instruction_version", + "transfer_timeout" + ], + "properties": { + "amount": { + "anyOf": [ + { + "$ref": "#/definitions/TransferAmount" + }, + { + "type": "null" + } + ] + }, + "batch_instruction_version": { + "$ref": "#/definitions/OptionUpdate_for_uint8" + }, + "channel_id": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "denom": { + "anyOf": [ + { + "$ref": "#/definitions/UncheckedUnionDenomConfig" + }, + { + "type": "null" + } + ] + }, + "input_addr": { + "anyOf": [ + { + "$ref": "#/definitions/LibraryAccountType" + }, + { + "type": "null" + } + ] + }, + "input_asset_decimals": { + "type": [ + "integer", + "null" + ], + "format": "uint8", + "minimum": 0.0 + }, + "input_asset_name": { + "type": [ + "string", + "null" + ] + }, + "input_asset_symbol": { + "type": [ + "string", + "null" + ] + }, + "input_asset_token_path": { + "anyOf": [ + { + "$ref": "#/definitions/Uint256" + }, + { + "type": "null" + } + ] + }, + "output_addr": { + "anyOf": [ + { + "$ref": "#/definitions/LibraryAccountType" + }, + { + "type": "null" + } + ] + }, + "quote_amount": { + "anyOf": [ + { + "$ref": "#/definitions/Uint256" + }, + { + "type": "null" + } + ] + }, + "quote_token": { + "type": [ + "string", + "null" + ] + }, + "transfer_instruction_version": { + "$ref": "#/definitions/OptionUpdate_for_uint8" + }, + "transfer_timeout": { + "$ref": "#/definitions/OptionUpdate_for_uint64" + }, + "zkgm_contract": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, + "OptionUpdate_for_uint64": { + "oneOf": [ + { + "type": "string", + "enum": [ + "none" + ] + }, + { + "type": "object", + "required": [ + "set" + ], + "properties": { + "set": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + ] + }, + "OptionUpdate_for_uint8": { + "oneOf": [ + { + "type": "string", + "enum": [ + "none" + ] + }, + { + "type": "object", + "required": [ + "set" + ], + "properties": { + "set": { + "type": [ + "integer", + "null" + ], + "format": "uint8", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "TransferAmount": { + "oneOf": [ + { + "type": "string", + "enum": [ + "full_amount" + ] + }, + { + "type": "object", + "required": [ + "fixed_amount" + ], + "properties": { + "fixed_amount": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + }, + "Uint256": { + "description": "An implementation of u256 that is using strings for JSON encoding/decoding, such that the full u256 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances out of primitive uint types or `new` to provide big endian bytes:\n\n``` # use cosmwasm_std::Uint256; let a = Uint256::from(258u128); let b = Uint256::new([ 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 1u8, 2u8, ]); assert_eq!(a, b); ```", + "type": "string" + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + }, + "UncheckedUnionCw20Config": { + "type": "object", + "required": [ + "minter", + "token" + ], + "properties": { + "minter": { + "type": "string" + }, + "token": { + "type": "string" + } + }, + "additionalProperties": false + }, + "UncheckedUnionDenomConfig": { + "oneOf": [ + { + "description": "A native (bank module) asset.", + "type": "object", + "required": [ + "native" + ], + "properties": { + "native": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "description": "A cw20 asset along with the token minter address that needs to be approved for spending during transfers.", + "type": "object", + "required": [ + "cw20" + ], + "properties": { + "cw20": { + "$ref": "#/definitions/UncheckedUnionCw20Config" + } + }, + "additionalProperties": false + } + ] + } + } + }, + "query": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "description": "Enum representing the different query messages that can be sent.", + "oneOf": [ + { + "description": "Query to get the processor address.", + "type": "object", + "required": [ + "get_processor" + ], + "properties": { + "get_processor": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Query to get the library configuration.", + "type": "object", + "required": [ + "get_library_config" + ], + "properties": { + "get_library_config": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "get_raw_library_config" + ], + "properties": { + "get_raw_library_config": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Query the contract's ownership information", + "type": "object", + "required": [ + "ownership" + ], + "properties": { + "ownership": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "migrate": null, + "sudo": null, + "responses": { + "get_library_config": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Config", + "type": "object", + "required": [ + "amount", + "channel_id", + "denom", + "input_addr", + "input_asset_decimals", + "input_asset_name", + "input_asset_symbol", + "input_asset_token_path", + "output_addr", + "quote_amount", + "quote_token", + "zkgm_contract" + ], + "properties": { + "amount": { + "$ref": "#/definitions/TransferAmount" + }, + "batch_instruction_version": { + "type": [ + "integer", + "null" + ], + "format": "uint8", + "minimum": 0.0 + }, + "channel_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "denom": { + "$ref": "#/definitions/CheckedUnionDenomConfig" + }, + "input_addr": { + "$ref": "#/definitions/Addr" + }, + "input_asset_decimals": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, + "input_asset_name": { + "type": "string" + }, + "input_asset_symbol": { + "type": "string" + }, + "input_asset_token_path": { + "$ref": "#/definitions/Uint256" + }, + "output_addr": { + "type": "string" + }, + "quote_amount": { + "$ref": "#/definitions/Uint256" + }, + "quote_token": { + "type": "string" + }, + "transfer_instruction_version": { + "type": [ + "integer", + "null" + ], + "format": "uint8", + "minimum": 0.0 + }, + "transfer_timeout": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "zkgm_contract": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "CheckedUnionCw20Config": { + "type": "object", + "required": [ + "minter", + "token" + ], + "properties": { + "minter": { + "$ref": "#/definitions/Addr" + }, + "token": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + }, + "CheckedUnionDenomConfig": { + "oneOf": [ + { + "description": "A native (bank module) asset.", + "type": "object", + "required": [ + "native" + ], + "properties": { + "native": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "description": "A cw20 asset along with the token minter address that needs to be approved for spending during transfers.", + "type": "object", + "required": [ + "cw20" + ], + "properties": { + "cw20": { + "$ref": "#/definitions/CheckedUnionCw20Config" + } + }, + "additionalProperties": false + } + ] + }, + "TransferAmount": { + "oneOf": [ + { + "type": "string", + "enum": [ + "full_amount" + ] + }, + { + "type": "object", + "required": [ + "fixed_amount" + ], + "properties": { + "fixed_amount": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + }, + "Uint256": { + "description": "An implementation of u256 that is using strings for JSON encoding/decoding, such that the full u256 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances out of primitive uint types or `new` to provide big endian bytes:\n\n``` # use cosmwasm_std::Uint256; let a = Uint256::from(258u128); let b = Uint256::new([ 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 1u8, 2u8, ]); assert_eq!(a, b); ```", + "type": "string" + } + } + }, + "get_processor": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Addr", + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "get_raw_library_config": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "LibraryConfig", + "type": "object", + "required": [ + "amount", + "channel_id", + "denom", + "input_addr", + "input_asset_decimals", + "input_asset_name", + "input_asset_symbol", + "input_asset_token_path", + "output_addr", + "quote_amount", + "quote_token", + "zkgm_contract" + ], + "properties": { + "amount": { + "$ref": "#/definitions/TransferAmount" + }, + "batch_instruction_version": { + "type": [ + "integer", + "null" + ], + "format": "uint8", + "minimum": 0.0 + }, + "channel_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "denom": { + "$ref": "#/definitions/UncheckedUnionDenomConfig" + }, + "input_addr": { + "$ref": "#/definitions/LibraryAccountType" + }, + "input_asset_decimals": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, + "input_asset_name": { + "type": "string" + }, + "input_asset_symbol": { + "type": "string" + }, + "input_asset_token_path": { + "$ref": "#/definitions/Uint256" + }, + "output_addr": { + "$ref": "#/definitions/LibraryAccountType" + }, + "quote_amount": { + "$ref": "#/definitions/Uint256" + }, + "quote_token": { + "type": "string" + }, + "transfer_instruction_version": { + "type": [ + "integer", + "null" + ], + "format": "uint8", + "minimum": 0.0 + }, + "transfer_timeout": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "zkgm_contract": { + "type": "string" + } + }, + "additionalProperties": false, + "definitions": { + "LibraryAccountType": { + "description": "A helper type that is used to associate an account or library with an id When a program is not instantiated yet, ids will be used to reference accounts and libraries When a program is instantiated, the ids will be replaced by the instantiated addresses", + "oneOf": [ + { + "type": "object", + "required": [ + "|library_account_addr|" + ], + "properties": { + "|library_account_addr|": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "|account_id|" + ], + "properties": { + "|account_id|": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "|library_id|" + ], + "properties": { + "|library_id|": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + ] + }, + "TransferAmount": { + "oneOf": [ + { + "type": "string", + "enum": [ + "full_amount" + ] + }, + { + "type": "object", + "required": [ + "fixed_amount" + ], + "properties": { + "fixed_amount": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + }, + "Uint256": { + "description": "An implementation of u256 that is using strings for JSON encoding/decoding, such that the full u256 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances out of primitive uint types or `new` to provide big endian bytes:\n\n``` # use cosmwasm_std::Uint256; let a = Uint256::from(258u128); let b = Uint256::new([ 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 1u8, 2u8, ]); assert_eq!(a, b); ```", + "type": "string" + }, + "UncheckedUnionCw20Config": { + "type": "object", + "required": [ + "minter", + "token" + ], + "properties": { + "minter": { + "type": "string" + }, + "token": { + "type": "string" + } + }, + "additionalProperties": false + }, + "UncheckedUnionDenomConfig": { + "oneOf": [ + { + "description": "A native (bank module) asset.", + "type": "object", + "required": [ + "native" + ], + "properties": { + "native": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "description": "A cw20 asset along with the token minter address that needs to be approved for spending during transfers.", + "type": "object", + "required": [ + "cw20" + ], + "properties": { + "cw20": { + "$ref": "#/definitions/UncheckedUnionCw20Config" + } + }, + "additionalProperties": false + } + ] + } + } + }, + "ownership": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Ownership_for_String", + "description": "The contract's ownership info", + "type": "object", + "properties": { + "owner": { + "description": "The contract's current owner. `None` if the ownership has been renounced.", + "type": [ + "string", + "null" + ] + }, + "pending_expiry": { + "description": "The deadline for the pending owner to accept the ownership. `None` if there isn't a pending ownership transfer, or if a transfer exists and it doesn't have a deadline.", + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "pending_owner": { + "description": "The account who has been proposed to take over the ownership. `None` if there isn't a pending ownership transfer.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "definitions": { + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } + } + } +} diff --git a/contracts/libraries/union-transfer/src/bin/schema.rs b/contracts/libraries/union-transfer/src/bin/schema.rs new file mode 100644 index 000000000..1d1744dd3 --- /dev/null +++ b/contracts/libraries/union-transfer/src/bin/schema.rs @@ -0,0 +1,12 @@ +use cosmwasm_schema::write_api; + +use valence_library_utils::msg::{ExecuteMsg, InstantiateMsg}; +use valence_union_transfer::msg::{FunctionMsgs, LibraryConfig, LibraryConfigUpdate, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + execute: ExecuteMsg, + query: QueryMsg, + } +} diff --git a/contracts/libraries/union-transfer/src/contract.rs b/contracts/libraries/union-transfer/src/contract.rs new file mode 100644 index 000000000..1bff48255 --- /dev/null +++ b/contracts/libraries/union-transfer/src/contract.rs @@ -0,0 +1,263 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; +use valence_library_utils::{ + error::LibraryError, + msg::{ExecuteMsg, InstantiateMsg}, +}; + +use crate::{ + msg::{Config, FunctionMsgs, LibraryConfig, LibraryConfigUpdate, QueryMsg}, + state::COUNTER, +}; + +// version info for migration info +const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +const DEFAULT_TIMEOUT_SECONDS: u64 = 259200; // 3 days +const DEFAULT_BATCH_INSTRUCTION_VERSION: u8 = 0x00; +const DEFAULT_TRANSFER_INSTRUCTION_VERSION: u8 = 0x01; +const BATCH_OP_CODE: u8 = 0x02; // OP_CODE for batch +const TRANSFER_OP_CODE: u8 = 0x03; // OP_CODE for transfer + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + // Initialize the counter + COUNTER.save(deps.storage, &0)?; + + valence_library_base::instantiate(deps, CONTRACT_NAME, CONTRACT_VERSION, msg) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + valence_library_base::execute( + deps, + env, + info, + msg, + functions::process_function, + execute::update_config, + ) +} + +mod functions { + use alloy_primitives::{ + hex::{self, FromHex}, + Bytes, U256, + }; + use alloy_sol_types::SolValue; + use cosmwasm_std::{ + coin, to_json_binary, CosmosMsg, DepsMut, Env, MessageInfo, Response, StdResult, Uint64, + WasmMsg, + }; + use cw20::Cw20ExecuteMsg; + use sha2::{Digest, Sha256}; + use valence_library_utils::{error::LibraryError, execute_on_behalf_of}; + + use crate::{ + msg::{CheckedUnionDenomConfig, Config, FunctionMsgs, TransferAmount}, + state::COUNTER, + union::{self, Batch, FungibleAssetOrder, Instruction}, + }; + + use super::{ + BATCH_OP_CODE, DEFAULT_BATCH_INSTRUCTION_VERSION, DEFAULT_TIMEOUT_SECONDS, + DEFAULT_TRANSFER_INSTRUCTION_VERSION, TRANSFER_OP_CODE, + }; + + pub fn process_function( + deps: DepsMut, + env: Env, + _info: MessageInfo, + msg: FunctionMsgs, + cfg: Config, + ) -> Result { + match msg { + FunctionMsgs::Transfer { quote_amount } => { + let balance = cfg.denom.query_balance(&deps.querier, &cfg.input_addr)?; + + let amount = match cfg.amount { + TransferAmount::FullAmount => balance, + TransferAmount::FixedAmount(amount) => { + if balance < amount { + return Err(LibraryError::ExecutionError(format!( + "Insufficient balance for denom '{}' in config (required: {}, available: {}).", + cfg.denom, amount, balance, + ))); + } + amount + } + }; + + // Messages to be used for the transfer + let mut msgs = vec![]; + // Funds to be attached to the transfer + let mut funds = vec![]; + + // If the token we are sending is Cw20, we first need to approve the token minter to spend the tokens + // This is how the union transfer works for Cw20 tokens + if let CheckedUnionDenomConfig::Cw20(ref checked_union_cw20_config) = cfg.denom { + let allowance_msg = Cw20ExecuteMsg::IncreaseAllowance { + spender: checked_union_cw20_config.minter.to_string(), + amount, + expires: None, + }; + + let cosmos_msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: checked_union_cw20_config.token.to_string(), + msg: to_json_binary(&allowance_msg)?, + funds: vec![], + }); + + msgs.push(cosmos_msg); + } else { + // If the token is not Cw20, we need to send the funds directly + // We need to convert the amount to u128 + let amount = amount.u128(); + let denom = cfg.denom.to_string(); + funds.push(coin(amount, denom)); + } + + // If we are passing the quote_amount in the message, we will use that one, otherwise the one in the config + let quote_amount = if let Some(quote_amount) = quote_amount { + U256::from_be_bytes(quote_amount.to_be_bytes()) + } else { + U256::from_be_bytes(cfg.quote_amount.to_be_bytes()) + }; + + // Let's create the transfer instruction that will go inside the batch + let fungible_asset_order = FungibleAssetOrder { + sender: Bytes::from(cfg.input_addr.to_string().into_bytes()), // The sender needs to be the bytes of the address + receiver: Bytes::from_hex(&cfg.output_addr).map_err(|_| { + LibraryError::ExecutionError( + "The receiver address is not a valid EVM address.".to_string(), + ) + })?, // The receiver is already in hex format + baseToken: Bytes::from(cfg.denom.to_string().into_bytes()), // The base token is the denom we are sending + baseAmount: U256::from(amount.u128()), // The base amount is the amount we are sending + baseTokenSymbol: cfg.input_asset_symbol, + baseTokenName: cfg.input_asset_name, + baseTokenDecimals: cfg.input_asset_decimals, + baseTokenPath: U256::from_be_bytes(cfg.input_asset_token_path.to_be_bytes()), + quoteToken: Bytes::from_hex(cfg.quote_token).map_err(|_| { + LibraryError::ExecutionError( + "The quote token is not a valid EVM address.".to_string(), + ) + })?, // The quote token is the output asset token path + quoteAmount: quote_amount, + }; + + let transfer_instruction = Instruction { + version: cfg + .transfer_instruction_version + .unwrap_or(DEFAULT_TRANSFER_INSTRUCTION_VERSION), + opcode: TRANSFER_OP_CODE, + operand: fungible_asset_order.abi_encode_params().into(), + }; + + // Now we create the batch instruction that will contain this one + let batch = Batch { + instructions: vec![transfer_instruction], + }; + let batch_instruction = Instruction { + version: cfg + .batch_instruction_version + .unwrap_or(DEFAULT_BATCH_INSTRUCTION_VERSION), + opcode: BATCH_OP_CODE, + operand: batch.abi_encode_params().into(), + }; + let bytes_instruction: Bytes = batch_instruction.abi_encode_params().into(); + + // Let's generate a unique salt for the transaction + let counter = COUNTER.update(deps.storage, |mut counter| -> StdResult<_> { + counter += 1; + Ok(counter) + })?; + let salt = Sha256::new() + .chain_update(cfg.input_addr.to_string().as_bytes()) + .chain_update(env.block.time.seconds().to_be_bytes()) + .chain_update(counter.to_be_bytes()) + .finalize(); + + // Create the send message + let send_msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: cfg.zkgm_contract.to_string(), + msg: to_json_binary(&union::ExecuteMsg::Send { + channel_id: cfg.channel_id, + timeout_height: Uint64::zero(), + timeout_timestamp: Uint64::from( + env.block + .time + .plus_seconds( + cfg.transfer_timeout.unwrap_or(DEFAULT_TIMEOUT_SECONDS), + ) + .nanos(), + ), + salt: Bytes::from_hex(hex::encode(salt)) + .map_err(|_| { + LibraryError::ExecutionError("Can't encode the salt.".to_string()) + })? + .to_string(), + instruction: bytes_instruction.to_string(), + })?, + funds, + }); + msgs.push(send_msg); + + let input_account_msgs = execute_on_behalf_of(msgs, &cfg.input_addr)?; + + Ok(Response::new() + .add_attribute("method", "union-transfer") + .add_message(input_account_msgs)) + } + } + } +} + +mod execute { + use cosmwasm_std::{DepsMut, Env, MessageInfo}; + use valence_library_utils::error::LibraryError; + + use crate::msg::LibraryConfigUpdate; + + pub fn update_config( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + new_config: LibraryConfigUpdate, + ) -> Result<(), LibraryError> { + new_config.update_config(deps) + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Ownership {} => { + to_json_binary(&valence_library_base::get_ownership(deps.storage)?) + } + QueryMsg::GetProcessor {} => { + to_json_binary(&valence_library_base::get_processor(deps.storage)?) + } + QueryMsg::GetLibraryConfig {} => { + let config: Config = valence_library_base::load_config(deps.storage)?; + to_json_binary(&config) + } + QueryMsg::GetRawLibraryConfig {} => { + let raw_config: LibraryConfig = + valence_library_utils::raw_config::query_raw_library_config(deps.storage)?; + to_json_binary(&raw_config) + } + } +} diff --git a/contracts/libraries/union-transfer/src/lib.rs b/contracts/libraries/union-transfer/src/lib.rs new file mode 100644 index 000000000..458d859c9 --- /dev/null +++ b/contracts/libraries/union-transfer/src/lib.rs @@ -0,0 +1,7 @@ +pub mod contract; +pub mod msg; +pub mod state; + +#[cfg(test)] +mod tests; +pub mod union; diff --git a/contracts/libraries/union-transfer/src/msg.rs b/contracts/libraries/union-transfer/src/msg.rs new file mode 100644 index 000000000..375c39f7f --- /dev/null +++ b/contracts/libraries/union-transfer/src/msg.rs @@ -0,0 +1,373 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, CustomQuery, Deps, DepsMut, QuerierWrapper, Uint128, Uint256}; +use cw_ownable::cw_ownable_query; +use valence_library_utils::{ + error::LibraryError, msg::LibraryConfigValidation, LibraryAccountType, +}; +use valence_macros::{valence_library_query, ValenceLibraryInterface}; + +#[cw_serde] +pub enum FunctionMsgs { + /// If quote amount is provided, it will override the quote amount in the config. + Transfer { quote_amount: Option }, +} + +#[valence_library_query] +#[cw_ownable_query] +#[cw_serde] +#[derive(QueryResponses)] +/// Enum representing the different query messages that can be sent. +pub enum QueryMsg {} + +#[cw_serde] +#[derive(ValenceLibraryInterface)] +pub struct LibraryConfig { + pub input_addr: LibraryAccountType, + pub output_addr: LibraryAccountType, + pub denom: UncheckedUnionDenomConfig, + pub amount: TransferAmount, + // Information about the asset to be transferred. + pub input_asset_name: String, + pub input_asset_symbol: String, + pub input_asset_decimals: u8, + pub input_asset_token_path: Uint256, + // Information about the asset to be received. + pub quote_token: String, + pub quote_amount: Uint256, + // Information about the remote chain. + pub channel_id: u64, + pub transfer_timeout: Option, // If not provided, a default 3 days will be used (259200 seconds). + // Information about the protocol + pub zkgm_contract: String, // The address of the ZKGM contract that we will interact with + // They are using a batch operation with a transfer (FungibleAssetOrder) operation inside, so we need the version for both instructions. + // If not provided, we will use the versions currently used by the protocol, but this is meant to be used for future upgrades. + pub batch_instruction_version: Option, // The version of the batch instruction to be used. If not provided, the current default version will be used. + pub transfer_instruction_version: Option, // The version of the transfer instruction to be used. If not provided, the current default version will be used. +} + +#[cw_serde] +pub enum UncheckedUnionDenomConfig { + /// A native (bank module) asset. + Native(String), + /// A cw20 asset along with the token minter address that needs to be approved for spending during transfers. + Cw20(UncheckedUnionCw20Config), +} + +#[cw_serde] +pub struct UncheckedUnionCw20Config { + pub token: String, + pub minter: String, +} + +impl UncheckedUnionDenomConfig { + pub fn into_checked(self, deps: Deps) -> StdResult { + match self { + Self::Native(denom) => Ok(CheckedUnionDenomConfig::Native(denom)), + Self::Cw20(unchecked_config) => { + let addr_token = deps.api.addr_validate(&unchecked_config.token)?; + let addr_minter = deps.api.addr_validate(&unchecked_config.minter)?; + let _info: cw20::TokenInfoResponse = deps + .querier + .query_wasm_smart(addr_token.clone(), &cw20::Cw20QueryMsg::TokenInfo {})?; + Ok(CheckedUnionDenomConfig::Cw20(CheckedUnionCw20Config { + token: addr_token, + minter: addr_minter, + })) + } + } + } +} + +#[cw_serde] +pub enum CheckedUnionDenomConfig { + /// A native (bank module) asset. + Native(String), + /// A cw20 asset along with the token minter address that needs to be approved for spending during transfers. + Cw20(CheckedUnionCw20Config), +} + +impl std::fmt::Display for CheckedUnionDenomConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Native(denom) => write!(f, "{}", denom), + Self::Cw20(config) => write!(f, "{}", config.token), + } + } +} + +impl CheckedUnionDenomConfig { + pub fn query_balance( + &self, + querier: &QuerierWrapper, + who: &Addr, + ) -> StdResult { + match self { + Self::Native(denom) => Ok(querier.query_balance(who, denom)?.amount), + Self::Cw20(config) => { + let balance: cw20::BalanceResponse = querier.query_wasm_smart( + config.token.clone(), + &cw20::Cw20QueryMsg::Balance { + address: who.to_string(), + }, + )?; + Ok(balance.balance) + } + } + } +} + +#[cw_serde] +pub struct CheckedUnionCw20Config { + pub token: Addr, + pub minter: Addr, +} + +#[cw_serde] +pub enum TransferAmount { + FullAmount, + FixedAmount(Uint128), +} + +impl LibraryConfig { + #[allow(clippy::too_many_arguments)] + pub fn new( + input_addr: LibraryAccountType, + output_addr: LibraryAccountType, + denom: UncheckedUnionDenomConfig, + amount: TransferAmount, + input_asset_name: String, + input_asset_symbol: String, + input_asset_decimals: u8, + input_asset_token_path: Uint256, + quote_token: String, + quote_amount: Uint256, + channel_id: u64, + transfer_timeout: Option, + zkgm_contract: String, + batch_instruction_version: Option, + transfer_instruction_version: Option, + ) -> Self { + Self { + input_addr, + output_addr, + denom, + amount, + input_asset_name, + input_asset_symbol, + input_asset_decimals, + input_asset_token_path, + quote_token, + quote_amount, + channel_id, + transfer_timeout, + zkgm_contract, + batch_instruction_version, + transfer_instruction_version, + } + } + + fn do_validate(&self, api: &dyn cosmwasm_std::Api) -> Result<(Addr, Addr), LibraryError> { + let input_addr = self.input_addr.to_addr(api)?; + let zkgm_addr = api.addr_validate(&self.zkgm_contract)?; + + match self.amount { + TransferAmount::FullAmount => {} + TransferAmount::FixedAmount(amount) => { + if amount.is_zero() { + return Err(LibraryError::ConfigurationError( + "Invalid Union transfer config: amount cannot be zero.".to_string(), + )); + } + } + } + + if let Some(timeout) = self.transfer_timeout { + if timeout == 0 { + return Err(LibraryError::ConfigurationError( + "Invalid Union transfer config: transfer_timeout cannot be zero.".to_string(), + )); + } + } + + Ok((input_addr, zkgm_addr)) + } +} + +impl LibraryConfigValidation for LibraryConfig { + #[cfg(not(target_arch = "wasm32"))] + fn pre_validate(&self, api: &dyn cosmwasm_std::Api) -> Result<(), LibraryError> { + self.do_validate(api)?; + Ok(()) + } + + fn validate(&self, deps: Deps) -> Result { + let (input_addr, zkgm_contract) = self.do_validate(deps.api)?; + Ok(Config { + input_addr, + // Can't validate output address as it's on another chain + output_addr: self.output_addr.to_string()?, + denom: self + .denom + .clone() + .into_checked(deps) + .map_err(|err| LibraryError::ConfigurationError(err.to_string()))?, + amount: self.amount.clone(), + input_asset_name: self.input_asset_name.clone(), + input_asset_symbol: self.input_asset_symbol.clone(), + input_asset_decimals: self.input_asset_decimals, + input_asset_token_path: self.input_asset_token_path, + quote_token: self.quote_token.clone(), + quote_amount: self.quote_amount, + channel_id: self.channel_id, + transfer_timeout: self.transfer_timeout, + zkgm_contract, + batch_instruction_version: self.batch_instruction_version, + transfer_instruction_version: self.transfer_instruction_version, + }) + } +} + +impl LibraryConfigUpdate { + pub fn update_config(self, deps: DepsMut) -> Result<(), LibraryError> + where + T: CustomQuery, + { + let mut config: Config = valence_library_base::load_config(deps.storage)?; + + if let Some(input_addr) = self.input_addr { + config.input_addr = input_addr.to_addr(deps.api)?; + } + + if let Some(output_addr) = self.output_addr { + config.output_addr = output_addr.to_string()?; + } + + if let Some(denom) = self.denom { + config.denom = denom + .clone() + .into_checked(deps.as_ref().into_empty()) + .map_err(|err| LibraryError::ConfigurationError(err.to_string()))?; + } + + if let Some(amount) = self.amount { + if let TransferAmount::FixedAmount(amount) = &amount { + if amount.is_zero() { + return Err(LibraryError::ConfigurationError( + "Invalid Union transfer config: amount cannot be zero.".to_string(), + )); + } + } + config.amount = amount; + } + + if let Some(input_asset_name) = self.input_asset_name { + config.input_asset_name = input_asset_name; + } + + if let Some(input_asset_symbol) = self.input_asset_symbol { + config.input_asset_symbol = input_asset_symbol; + } + + if let Some(input_asset_decimals) = self.input_asset_decimals { + config.input_asset_decimals = input_asset_decimals; + } + + if let Some(input_asset_token_path) = self.input_asset_token_path { + config.input_asset_token_path = input_asset_token_path; + } + + if let Some(quote_token) = self.quote_token { + config.quote_token = quote_token; + } + + if let Some(quote_amount) = self.quote_amount { + config.quote_amount = quote_amount; + } + + if let Some(channel_id) = self.channel_id { + config.channel_id = channel_id; + } + + if let OptionUpdate::Set(transfer_timeout) = self.transfer_timeout { + if let Some(timeout) = transfer_timeout { + if timeout == 0 { + return Err(LibraryError::ConfigurationError( + "Invalid Union transfer config: transfer_timeout cannot be zero." + .to_string(), + )); + } + } + config.transfer_timeout = transfer_timeout; + } + + if let OptionUpdate::Set(batch_instruction_version) = self.batch_instruction_version { + config.batch_instruction_version = batch_instruction_version; + } + + if let OptionUpdate::Set(transfer_instruction_version) = self.transfer_instruction_version { + config.transfer_instruction_version = transfer_instruction_version; + } + + valence_library_base::save_config(deps.storage, &config)?; + + Ok(()) + } +} + +#[cw_serde] +pub struct Config { + pub input_addr: Addr, + pub output_addr: String, + pub denom: CheckedUnionDenomConfig, + pub amount: TransferAmount, + pub input_asset_name: String, + pub input_asset_symbol: String, + pub input_asset_decimals: u8, + pub input_asset_token_path: Uint256, + pub quote_token: String, + pub quote_amount: Uint256, + pub channel_id: u64, + pub transfer_timeout: Option, + pub zkgm_contract: Addr, + pub batch_instruction_version: Option, + pub transfer_instruction_version: Option, +} + +impl Config { + #[allow(clippy::too_many_arguments)] + pub fn new( + input_addr: Addr, + output_addr: String, + denom: CheckedUnionDenomConfig, + amount: TransferAmount, + input_asset_name: String, + input_asset_symbol: String, + input_asset_decimals: u8, + input_asset_token_path: Uint256, + quote_token: String, + quote_amount: Uint256, + channel_id: u64, + transfer_timeout: Option, + zkgm_contract: Addr, + batch_instruction_version: Option, + transfer_instruction_version: Option, + ) -> Self { + Config { + input_addr, + output_addr, + denom, + amount, + input_asset_name, + input_asset_symbol, + input_asset_decimals, + input_asset_token_path, + quote_token, + quote_amount, + channel_id, + transfer_timeout, + zkgm_contract, + batch_instruction_version, + transfer_instruction_version, + } + } +} diff --git a/contracts/libraries/union-transfer/src/state.rs b/contracts/libraries/union-transfer/src/state.rs new file mode 100644 index 000000000..33ddc7c3c --- /dev/null +++ b/contracts/libraries/union-transfer/src/state.rs @@ -0,0 +1,3 @@ +use cw_storage_plus::Item; + +pub const COUNTER: Item = Item::new("counter"); diff --git a/contracts/libraries/union-transfer/src/tests.rs b/contracts/libraries/union-transfer/src/tests.rs new file mode 100644 index 000000000..280c21fb6 --- /dev/null +++ b/contracts/libraries/union-transfer/src/tests.rs @@ -0,0 +1,298 @@ +use crate::msg::{ + Config, FunctionMsgs, LibraryConfig, LibraryConfigUpdate, QueryMsg, TransferAmount, + UncheckedUnionDenomConfig, +}; +use cosmwasm_std::{coin, Addr, Empty, Uint128, Uint256}; +use cw_multi_test::{error::AnyResult, App, AppResponse, ContractWrapper, Executor}; +use cw_ownable::Ownership; +use valence_library_utils::{ + msg::{ExecuteMsg, InstantiateMsg, LibraryConfigValidation}, + testing::{LibraryTestSuite, LibraryTestSuiteBase}, + LibraryAccountType, +}; + +const NTRN: &str = "untrn"; +const ONE_MILLION: u128 = 1_000_000_000_000_u128; + +struct UnionTransferTestSuite { + inner: LibraryTestSuiteBase, + union_transfer_code_id: u64, + input_addr: Addr, + output_addr: String, + input_balance: Option<(u128, String)>, +} + +impl Default for UnionTransferTestSuite { + fn default() -> Self { + Self::new(None) + } +} + +#[allow(dead_code)] +impl UnionTransferTestSuite { + pub fn new(input_balance: Option<(u128, String)>) -> Self { + let mut inner = LibraryTestSuiteBase::new(); + + let input_addr = inner.get_contract_addr(inner.account_code_id(), "input_account"); + let output_addr = inner.api().addr_make("output_account").to_string(); + + // Template contract + let union_transfer_code = ContractWrapper::new( + crate::contract::execute, + crate::contract::instantiate, + crate::contract::query, + ); + + let union_transfer_code_id = inner.app_mut().store_code(Box::new(union_transfer_code)); + + Self { + inner, + union_transfer_code_id, + input_addr, + output_addr, + input_balance, + } + } + + pub fn union_transfer_init(&mut self, cfg: &LibraryConfig) -> Addr { + let init_msg = InstantiateMsg { + owner: self.owner().to_string(), + processor: self.processor().to_string(), + config: cfg.clone(), + }; + let addr = self.contract_init( + self.union_transfer_code_id, + "union_transfer_library", + &init_msg, + &[], + ); + + let input_addr = self.input_addr.clone(); + if self.app_mut().contract_data(&input_addr).is_err() { + let account_addr = self.account_init("input_account", vec![addr.to_string()]); + assert_eq!(account_addr, input_addr); + + if let Some((amount, denom)) = self.input_balance.as_ref().cloned() { + self.init_balance(&input_addr, vec![coin(amount, denom.to_string())]); + } + } + + addr + } + + fn union_transfer_config(&self, denom: String, amount: TransferAmount) -> LibraryConfig { + LibraryConfig::new( + valence_library_utils::LibraryAccountType::Addr(self.input_addr.to_string()), + valence_library_utils::LibraryAccountType::Addr(self.output_addr.to_string()), + UncheckedUnionDenomConfig::Native(denom), + amount, + NTRN.to_string(), + NTRN.to_string(), + 6, + Uint256::zero(), + "0xe53dcec07d16d88e386ae0710e86d9a400f83c31".to_string(), + Uint256::from_u128(100000000), + 1, + None, + self.input_addr.to_string(), + None, + None, + ) + } + + fn execute_union_transfer(&mut self, addr: Addr) -> AnyResult { + self.contract_execute( + addr, + &ExecuteMsg::<_, LibraryConfig>::ProcessFunction(FunctionMsgs::Transfer { + quote_amount: None, + }), + ) + } + + fn update_config(&mut self, addr: Addr, new_config: LibraryConfig) -> AnyResult { + let owner = self.owner().clone(); + let updated_config = LibraryConfigUpdate { + input_addr: Some(new_config.input_addr), + output_addr: Some(new_config.output_addr), + denom: Some(new_config.denom), + amount: Some(new_config.amount), + input_asset_name: Some(new_config.input_asset_name), + input_asset_symbol: Some(new_config.input_asset_symbol), + input_asset_decimals: Some(new_config.input_asset_decimals), + input_asset_token_path: Some(new_config.input_asset_token_path), + quote_token: Some(new_config.quote_token), + quote_amount: Some(new_config.quote_amount), + channel_id: Some(new_config.channel_id), + transfer_timeout: valence_library_utils::OptionUpdate::Set(new_config.transfer_timeout), + zkgm_contract: Some(new_config.zkgm_contract), + batch_instruction_version: valence_library_utils::OptionUpdate::Set( + new_config.batch_instruction_version, + ), + transfer_instruction_version: valence_library_utils::OptionUpdate::Set( + new_config.transfer_instruction_version, + ), + }; + self.app_mut().execute_contract( + owner, + addr, + &ExecuteMsg::::UpdateConfig { + new_config: updated_config, + }, + &[], + ) + } +} + +impl LibraryTestSuite for UnionTransferTestSuite { + fn app(&self) -> &App { + self.inner.app() + } + + fn app_mut(&mut self) -> &mut App { + self.inner.app_mut() + } + + fn owner(&self) -> &Addr { + self.inner.owner() + } + + fn processor(&self) -> &Addr { + self.inner.processor() + } + + fn account_code_id(&self) -> u64 { + self.inner.account_code_id() + } + + fn cw20_code_id(&self) -> u64 { + self.inner.cw20_code_id() + } +} + +#[test] +fn instantiate_with_valid_config() { + let mut suite = UnionTransferTestSuite::default(); + + let cfg = suite.union_transfer_config(NTRN.to_string(), TransferAmount::FullAmount); + + // Instantiate Union transfer contract + let lib = suite.union_transfer_init(&cfg); + + // Verify owner + let owner_res: Ownership = suite.query_wasm(&lib, &QueryMsg::Ownership {}); + assert_eq!(owner_res.owner, Some(suite.owner().clone())); + + // Verify processor + let processor_addr: Addr = suite.query_wasm(&lib, &QueryMsg::GetProcessor {}); + assert_eq!(processor_addr, suite.processor().clone()); + + // Verify library config + let lib_cfg: Config = suite.query_wasm(&lib, &QueryMsg::GetLibraryConfig {}); + assert_eq!(lib_cfg.input_addr.to_string(), suite.input_addr.to_string()); +} + +#[test] +fn pre_validate_config_works() { + let suite = UnionTransferTestSuite::default(); + + let cfg = suite.union_transfer_config(NTRN.to_string(), TransferAmount::FullAmount); + + // Pre-validate config + cfg.pre_validate(suite.api()).unwrap(); +} + +#[test] +#[should_panic(expected = "Invalid Union transfer config: amount cannot be zero.")] +fn instantiate_fails_for_zero_amount() { + let mut suite = UnionTransferTestSuite::default(); + + let cfg = suite.union_transfer_config( + NTRN.to_string(), + TransferAmount::FixedAmount(Uint128::zero()), + ); + + // Instantiate Union transfer contract + suite.union_transfer_init(&cfg); +} + +// Config update tests + +#[test] +#[should_panic(expected = "Invalid Union transfer config: amount cannot be zero.")] +fn update_config_validates_amount() { + let mut suite = UnionTransferTestSuite::default(); + + let mut cfg = suite.union_transfer_config(NTRN.to_string(), TransferAmount::FullAmount); + + // Instantiate Union transfer contract + let lib = suite.union_transfer_init(&cfg); + + // Update config and set amount to zero + cfg.amount = TransferAmount::FixedAmount(Uint128::zero()); + + // Execute update config action + suite.update_config(lib.clone(), cfg).unwrap(); +} + +#[test] +#[should_panic(expected = "Invalid Union transfer config: transfer_timeout cannot be zero.")] +fn update_config_validates_union_timeout() { + let mut suite = UnionTransferTestSuite::default(); + + let mut cfg = suite.union_transfer_config(NTRN.to_string(), TransferAmount::FullAmount); + + // Instantiate Union transfer contract + let lib = suite.union_transfer_init(&cfg); + + // Update config and set Union timeout to zero + cfg.transfer_timeout = Some(0); + + // Execute update config action + suite.update_config(lib.clone(), cfg).unwrap(); +} + +#[test] +fn update_config_with_valid_config() { + let mut suite = UnionTransferTestSuite::default(); + + let mut cfg = suite.union_transfer_config(NTRN.to_string(), TransferAmount::FullAmount); + + // Instantiate Union transfer contract + let lib = suite.union_transfer_init(&cfg); + + // Update config: swap input and output addresses + cfg.input_addr = LibraryAccountType::Addr(suite.output_addr.to_string()); + cfg.output_addr = LibraryAccountType::Addr(suite.input_addr.to_string()); + cfg.amount = TransferAmount::FixedAmount(ONE_MILLION.into()); + + // Execute update config action + suite.update_config(lib.clone(), cfg).unwrap(); + + // Verify library config + let lib_cfg: Config = suite.query_wasm(&lib, &QueryMsg::GetLibraryConfig {}); + assert_eq!( + lib_cfg.input_addr.to_string(), + suite.output_addr.to_string() + ); +} + +// Insufficient balance tests + +#[test] +#[should_panic( + expected = "Execution error: Insufficient balance for denom 'untrn' in config (required: 1000000000000, available: 0)." +)] +fn union_transfer_fails_for_insufficient_balance() { + let mut suite = UnionTransferTestSuite::default(); + + let cfg = suite.union_transfer_config( + NTRN.to_string(), + TransferAmount::FixedAmount(ONE_MILLION.into()), + ); + + // Instantiate contract + let lib = suite.union_transfer_init(&cfg); + + // Execute Union transfer + suite.execute_union_transfer(lib).unwrap(); +} diff --git a/contracts/libraries/union-transfer/src/union.rs b/contracts/libraries/union-transfer/src/union.rs new file mode 100644 index 000000000..aa5b2d933 --- /dev/null +++ b/contracts/libraries/union-transfer/src/union.rs @@ -0,0 +1,193 @@ +use alloy_sol_types::sol; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::Uint64; + +// We make a more Cosmwasm friendly version of the original msg +// Since the bytes with a Hex prefix are needed, we are not using HexBinary but instead a String that we will build correctly in the Union Transfer library +#[cw_serde] +pub enum ExecuteMsg { + Send { + channel_id: u64, + timeout_height: Uint64, + timeout_timestamp: Uint64, + salt: String, + instruction: String, + }, +} + +// Types that are used for the Instruction +sol! { + struct Instruction { + uint8 version; + uint8 opcode; + bytes operand; + } + + struct Batch { + Instruction[] instructions; + } + + struct FungibleAssetOrder { + bytes sender; + bytes receiver; + bytes baseToken; + uint256 baseAmount; + string baseTokenSymbol; + string baseTokenName; + uint8 baseTokenDecimals; + uint256 baseTokenPath; + bytes quoteToken; + uint256 quoteAmount; + } +} + +#[cfg(test)] +mod tests { + use alloy_primitives::{hex::FromHex, Bytes, U256}; + use alloy_sol_types::SolValue; + use cosmwasm_std::{to_json_string, Addr, Uint128, Uint256}; + + use crate::msg::{CheckedUnionDenomConfig, Config, TransferAmount}; + + use super::*; + + #[test] + fn test_serialize_execute_msg() { + let bytes_salt = Bytes::from(&[0xde, 0xad, 0xbe, 0xef]); + let bytes_instruction = Bytes::from(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]); + + // Create an ExecuteMsg::Send instance + let msg = ExecuteMsg::Send { + channel_id: 5, + timeout_height: Uint64::new(100), + timeout_timestamp: Uint64::new(1634567890), + salt: bytes_salt.to_string(), + instruction: bytes_instruction.to_string(), + }; + + // Serialize to JSON and check + let serialized = to_json_string(&msg).unwrap(); + assert_eq!( + serialized, + r#"{"send":{"channel_id":5,"timeout_height":"100","timeout_timestamp":"1634567890","salt":"0xdeadbeef","instruction":"0x0123456789abcdef"}}"# + ); + } + + #[test] + fn test_decode_real_instruction() { + // Real instruction example taken from a real transaction + let instruction_hex = "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000003c000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000002e0000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000002217153000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000002600000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002a00000000000000000000000000000000000000000000000000000000000ff8693000000000000000000000000000000000000000000000000000000000000002a62626e31657032756d6a366b6e3334673274746a616c73633572397738707437737634786a7537333472000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014e7c952d457121ba8f02df1b1d85b26de80a6f1ac00000000000000000000000000000000000000000000000000000000000000000000000000000000000000047562626e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000047562626e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000047562626e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014e53dCec07d16D88e386AE0710E86d9a400f83c31000000000000000000000000"; + + // Convert hex string to bytes - Bytes should handle the 0x prefix correctly + let instruction_bytes = + Bytes::from_hex(instruction_hex).expect("Failed to parse hex string"); + + // Decode the instruction + let instruction = Instruction::abi_decode_params(&instruction_bytes, true) + .expect("Failed to decode instruction"); + + // Verify instruction fields + assert_eq!(instruction.version, 0); + assert_eq!(instruction.opcode, 2); + + // Print decoded instruction details for debugging + println!("Instruction successfully decoded:"); + println!(" Version: {}", instruction.version); + println!(" Opcode: {}", instruction.opcode); + println!(" Operand length: {} bytes", instruction.operand.len()); + + // Decode the operand as a Batch + let batch = Batch::abi_decode_params(&instruction.operand, true) + .expect("Failed to decode Batch from operand"); + + // Verify Batch fields + assert_eq!(batch.instructions.len(), 1); + assert_eq!(batch.instructions[0].version, 1); + assert_eq!(batch.instructions[0].opcode, 3); + + // Decode the first instruction as a FungibleAssetOrder + let fungible_asset_order = + FungibleAssetOrder::abi_decode_params(&batch.instructions[0].operand, true) + .expect("Failed to decode FungibleAssetOrder from operand"); + + // Verify FungibleAssetOrder fields + assert_eq!(fungible_asset_order.baseTokenSymbol, "ubbn"); + assert_eq!(fungible_asset_order.baseTokenName, "ubbn"); + assert_eq!(fungible_asset_order.baseTokenDecimals, 6); + + // Extract and print important fields for verification + println!("\nFungibleAssetOrder details:"); + println!(" Sender: {}", fungible_asset_order.sender); + println!(" Receiver: {}", fungible_asset_order.receiver); + println!(" Base token: {}", fungible_asset_order.baseToken); + println!(" Base amount: {}", fungible_asset_order.baseAmount); + println!( + " Base token symbol: {}", + fungible_asset_order.baseTokenSymbol + ); + println!(" Base token name: {}", fungible_asset_order.baseTokenName); + println!( + " Base token decimals: {}", + fungible_asset_order.baseTokenDecimals + ); + println!(" Quote token: {}", fungible_asset_order.quoteToken); + println!(" Quote amount: {}", fungible_asset_order.quoteAmount); + println!(" Base token path: {}", fungible_asset_order.baseTokenPath); + } + + #[test] + fn correct_instruction_from_config() { + // Create an example Config object and verify that all fields are transformed correctly + let cfg = Config::new( + Addr::unchecked( + "bbn1rvp882qs76sawd6ejydst28272t4au3n3hl79cwt8xsc7t3kp8rs26uc7z".to_string(), + ), + "0xe7c952d457121ba8f02df1b1d85b26de80a6f1ac".to_string(), + CheckedUnionDenomConfig::Native("ubbn".to_string()), + TransferAmount::FixedAmount(Uint128::from(100000000u128)), + "ubbn".to_string(), + "ubbn".to_string(), + 6, + Uint256::zero(), + "0xe53dcec07d16d88e386ae0710e86d9a400f83c31".to_string(), + Uint256::from(100000000u128), + 3, + None, + Addr::unchecked( + "bbn1336jj8ertl8h7rdvnz4dh5rqahd09cy0x43guhsxx6xyrztx292q77945h".to_string(), + ), + None, + None, + ); + + let fungible_asset_order = FungibleAssetOrder { + sender: Bytes::from(cfg.input_addr.to_string().into_bytes()), + receiver: Bytes::from_hex(&cfg.output_addr).unwrap(), + baseToken: Bytes::from(cfg.denom.to_string().into_bytes()), + baseAmount: U256::from(100000000u128), + baseTokenSymbol: cfg.input_asset_symbol, + baseTokenName: cfg.input_asset_name, + baseTokenDecimals: cfg.input_asset_decimals, + baseTokenPath: U256::from_be_bytes(cfg.input_asset_token_path.to_be_bytes()), + quoteToken: Bytes::from_hex(cfg.quote_token).unwrap(), + quoteAmount: U256::from(100000000u128), + }; + + // Verify the fields + assert_eq!(fungible_asset_order.sender.to_string(), "0x62626e31727670383832717337367361776436656a79647374323832373274346175336e33686c3739637774387873633774336b7038727332367563377a".to_string()); + assert_eq!( + fungible_asset_order.receiver.to_string(), + "0xe7c952d457121ba8f02df1b1d85b26de80a6f1ac".to_string() + ); + assert_eq!( + fungible_asset_order.baseToken.to_string(), + "0x7562626e".to_string() // "ubbn" + ); + + assert_eq!(fungible_asset_order.baseTokenPath, U256::from(0)); + assert_eq!( + fungible_asset_order.quoteToken.to_string(), + "0xe53dcec07d16d88e386ae0710e86d9a400f83c31".to_string() + ); + } +} diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 3253cf020..86a7da043 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -43,6 +43,7 @@ - [Drop Liquid Unstaker](./libraries/cosmwasm/drop_liquid_unstaker.md) - [ICA CCTP Transfer](./libraries/cosmwasm/ica_cctp_transfer.md) - [ICA IBC Transfer](./libraries/cosmwasm/ica_ibc_transfer.md) + - [Union Transfer](./libraries/cosmwasm/union_transfer.md) - [EVM](./libraries/evm/_overview.md) - [Forwarder](./libraries/evm/forwarder.md) - [CCTP Transfer](./libraries/evm/cctp_transfer.md) diff --git a/docs/src/libraries/cosmwasm/union_transfer.md b/docs/src/libraries/cosmwasm/union_transfer.md new file mode 100644 index 000000000..63d0f0f06 --- /dev/null +++ b/docs/src/libraries/cosmwasm/union_transfer.md @@ -0,0 +1,86 @@ +# Valence Union Transfer library + +The **Valence Union Transfer** library allows to transfer funds over [Union](https://union.build/) from an **input account** on a source CosmWasm chain to an **output account** on a destination EVM chain using the [Union UCS03-ZKGM protocol](https://docs.union.build/ucs/03/), which allows arbitrary filling of orders by any party. It is typically used as part of a **Valence Program**. In that context, a **Processor** contract will be the main contract interacting with the Forwarder library. + +## High-level flow + +```mermaid +--- +title: Union Transfer Library +--- +graph LR + IA((Input Account)) + ZG((zkGM)) + R((Recipient)) + P[Processor] + U[Union Transfer + Library] + UTM[Union Token + Protocol] + subgraph EVM[ EVM Chain ] + UTM -- 6/Send tokens --> R + end + subgraph CW[ CosmWasm Domain ] + P -- 1/Call + transfer(quote_amount) --> U + U -- 2/Query CW20/Native + token balance --> IA + U -- 3/Call approve (if applies) and send --> IA + IA -- 4/Approve CW20 (if applies) --> ZG + IA -- 5/Call send with instructions --> ZG + end + CW --- EVM +``` + +## Functions + +| Function | Parameters | Description | +| ------------ | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Transfer** | quote_amount(optional) | Transfer funds from the configured **input account** to the **output account** on the **destination chain**. The quote_amount parameter can override the configured quote token amount. If not passed, the amount in the config is used. | + +## Configuration + +The library is configured on instantiation via the `LibraryConfig` type. A list of supported chains and their channels can be found [here](https://docs.union.build/protocol/chains/overview/). Additional information of parameters used in the configuration can be found [here](https://docs.union.build/ucs/03/). This library allows any party to fill orders, therefore the `quote_amount` value should take into consideration the amount of tokens that the filling party will receive. + +```rust +pub struct LibraryConfig { + pub input_addr: LibraryAccountType, + pub output_addr: LibraryAccountType, + pub denom: UncheckedUnionDenomConfig, + pub amount: TransferAmount, + // Information about the asset to be transferred. + pub input_asset_name: String, + pub input_asset_symbol: String, + pub input_asset_decimals: u8, + pub input_asset_token_path: Uint256, + // Information about the asset to be received. + pub quote_token: String, + pub quote_amount: Uint256, + // Information about the remote chain. + pub channel_id: u64, + pub transfer_timeout: Option, // If not provided, a default 3 days will be used (259200 seconds). + // Information about the protocol + pub zkgm_contract: String, // The address of the ZKGM contract that we will interact with + // They are using a batch operation with a transfer (FungibleAssetOrder) operation inside, so we need the version for both instructions. + // If not provided, we will use the versions currently used by the protocol, but this is meant to be used for future upgrades. + pub batch_instruction_version: Option, // The version of the batch instruction to be used. If not provided, the current default version will be used. + pub transfer_instruction_version: Option, // The version of the transfer instruction to be used. If not provided, the current default version will be used. +} + +pub enum UncheckedUnionDenomConfig { + /// A native (bank module) asset. + Native(String), + /// A cw20 asset along with the token minter address that needs to be approved for spending during transfers. + Cw20(UncheckedUnionCw20Config), +} + +pub struct UncheckedUnionCw20Config { + pub token: String, + pub minter: String, +} + +pub enum TransferAmount { + FullAmount, + FixedAmount(Uint128), +} +``` diff --git a/program-manager/Cargo.toml b/program-manager/Cargo.toml index 26974b32c..f9da74e5f 100644 --- a/program-manager/Cargo.toml +++ b/program-manager/Cargo.toml @@ -44,6 +44,7 @@ valence-generic-ibc-transfer-library = { workspace = true } valence-neutron-ibc-transfer-library = { workspace = true } valence-drop-liquid-staker = { workspace = true } valence-drop-liquid-unstaker = { workspace = true } +valence-union-transfer = { workspace = true } valence-mars-lending = { workspace = true } tokio = { workspace = true } diff --git a/program-manager/schema/valence-program-manager.json b/program-manager/schema/valence-program-manager.json index 885853ea2..e3beaf2b9 100644 --- a/program-manager/schema/valence-program-manager.json +++ b/program-manager/schema/valence-program-manager.json @@ -915,6 +915,18 @@ }, "additionalProperties": false }, + { + "type": "object", + "required": [ + "ValenceUnionTransfer" + ], + "properties": { + "ValenceUnionTransfer": { + "$ref": "#/definitions/LibraryConfig14" + } + }, + "additionalProperties": false + }, { "type": "object", "required": [ @@ -922,7 +934,7 @@ ], "properties": { "ValenceMarsLending": { - "$ref": "#/definitions/LibraryConfig14" + "$ref": "#/definitions/LibraryConfig15" } }, "additionalProperties": false @@ -1026,6 +1038,90 @@ "additionalProperties": false }, "LibraryConfig14": { + "type": "object", + "required": [ + "amount", + "channel_id", + "denom", + "input_addr", + "input_asset_decimals", + "input_asset_name", + "input_asset_symbol", + "input_asset_token_path", + "output_addr", + "quote_amount", + "quote_token", + "zkgm_contract" + ], + "properties": { + "amount": { + "$ref": "#/definitions/TransferAmount" + }, + "batch_instruction_version": { + "type": [ + "integer", + "null" + ], + "format": "uint8", + "minimum": 0.0 + }, + "channel_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "denom": { + "$ref": "#/definitions/UncheckedUnionDenomConfig" + }, + "input_addr": { + "$ref": "#/definitions/LibraryAccountType" + }, + "input_asset_decimals": { + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, + "input_asset_name": { + "type": "string" + }, + "input_asset_symbol": { + "type": "string" + }, + "input_asset_token_path": { + "$ref": "#/definitions/Uint256" + }, + "output_addr": { + "$ref": "#/definitions/LibraryAccountType" + }, + "quote_amount": { + "$ref": "#/definitions/Uint256" + }, + "quote_token": { + "type": "string" + }, + "transfer_instruction_version": { + "type": [ + "integer", + "null" + ], + "format": "uint8", + "minimum": 0.0 + }, + "transfer_timeout": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "zkgm_contract": { + "type": "string" + } + }, + "additionalProperties": false + }, + "LibraryConfig15": { "type": "object", "required": [ "credit_manager_addr", @@ -1439,6 +1535,18 @@ }, "additionalProperties": false }, + { + "type": "object", + "required": [ + "ValenceUnionTransfer" + ], + "properties": { + "ValenceUnionTransfer": { + "$ref": "#/definitions/LibraryConfigUpdate14" + } + }, + "additionalProperties": false + }, { "type": "object", "required": [ @@ -1446,7 +1554,7 @@ ], "properties": { "ValenceMarsLending": { - "$ref": "#/definitions/LibraryConfigUpdate14" + "$ref": "#/definitions/LibraryConfigUpdate15" } }, "additionalProperties": false @@ -1614,6 +1722,126 @@ "additionalProperties": false }, "LibraryConfigUpdate14": { + "type": "object", + "required": [ + "batch_instruction_version", + "transfer_instruction_version", + "transfer_timeout" + ], + "properties": { + "amount": { + "anyOf": [ + { + "$ref": "#/definitions/TransferAmount" + }, + { + "type": "null" + } + ] + }, + "batch_instruction_version": { + "$ref": "#/definitions/OptionUpdate_for_uint8" + }, + "channel_id": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "denom": { + "anyOf": [ + { + "$ref": "#/definitions/UncheckedUnionDenomConfig" + }, + { + "type": "null" + } + ] + }, + "input_addr": { + "anyOf": [ + { + "$ref": "#/definitions/LibraryAccountType" + }, + { + "type": "null" + } + ] + }, + "input_asset_decimals": { + "type": [ + "integer", + "null" + ], + "format": "uint8", + "minimum": 0.0 + }, + "input_asset_name": { + "type": [ + "string", + "null" + ] + }, + "input_asset_symbol": { + "type": [ + "string", + "null" + ] + }, + "input_asset_token_path": { + "anyOf": [ + { + "$ref": "#/definitions/Uint256" + }, + { + "type": "null" + } + ] + }, + "output_addr": { + "anyOf": [ + { + "$ref": "#/definitions/LibraryAccountType" + }, + { + "type": "null" + } + ] + }, + "quote_amount": { + "anyOf": [ + { + "$ref": "#/definitions/Uint256" + }, + { + "type": "null" + } + ] + }, + "quote_token": { + "type": [ + "string", + "null" + ] + }, + "transfer_instruction_version": { + "$ref": "#/definitions/OptionUpdate_for_uint8" + }, + "transfer_timeout": { + "$ref": "#/definitions/OptionUpdate_for_uint64" + }, + "zkgm_contract": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, + "LibraryConfigUpdate15": { "type": "object", "properties": { "credit_manager_addr": { @@ -2334,6 +2562,60 @@ } ] }, + "OptionUpdate_for_uint64": { + "oneOf": [ + { + "type": "string", + "enum": [ + "none" + ] + }, + { + "type": "object", + "required": [ + "set" + ], + "properties": { + "set": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + ] + }, + "OptionUpdate_for_uint8": { + "oneOf": [ + { + "type": "string", + "enum": [ + "none" + ] + }, + { + "type": "object", + "required": [ + "set" + ], + "properties": { + "set": { + "type": [ + "integer", + "null" + ], + "format": "uint8", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + ] + }, "PacketForwardMiddlewareConfig": { "type": "object", "required": [ @@ -2841,10 +3123,36 @@ } ] }, + "TransferAmount": { + "oneOf": [ + { + "type": "string", + "enum": [ + "full_amount" + ] + }, + { + "type": "object", + "required": [ + "fixed_amount" + ], + "properties": { + "fixed_amount": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + ] + }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" }, + "Uint256": { + "description": "An implementation of u256 that is using strings for JSON encoding/decoding, such that the full u256 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances out of primitive uint types or `new` to provide big endian bytes:\n\n``` # use cosmwasm_std::Uint256; let a = Uint256::from(258u128); let b = Uint256::new([ 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 1u8, 2u8, ]); assert_eq!(a, b); ```", + "type": "string" + }, "Uint64": { "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", "type": "string" @@ -3060,6 +3368,52 @@ } }, "additionalProperties": false + }, + "UncheckedUnionCw20Config": { + "type": "object", + "required": [ + "minter", + "token" + ], + "properties": { + "minter": { + "type": "string" + }, + "token": { + "type": "string" + } + }, + "additionalProperties": false + }, + "UncheckedUnionDenomConfig": { + "oneOf": [ + { + "description": "A native (bank module) asset.", + "type": "object", + "required": [ + "native" + ], + "properties": { + "native": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "description": "A cw20 asset along with the token minter address that needs to be approved for spending during transfers.", + "type": "object", + "required": [ + "cw20" + ], + "properties": { + "cw20": { + "$ref": "#/definitions/UncheckedUnionCw20Config" + } + }, + "additionalProperties": false + } + ] } } }, diff --git a/program-manager/src/library.rs b/program-manager/src/library.rs index f86ad8cbe..4744d68ae 100644 --- a/program-manager/src/library.rs +++ b/program-manager/src/library.rs @@ -97,6 +97,7 @@ pub enum LibraryConfig { ValenceOsmosisClWithdrawer(valence_osmosis_cl_withdrawer::msg::LibraryConfig), ValenceDropLiquidStaker(valence_drop_liquid_staker::msg::LibraryConfig), ValenceDropLiquidUnstaker(valence_drop_liquid_unstaker::msg::LibraryConfig), + ValenceUnionTransfer(valence_union_transfer::msg::LibraryConfig), ValenceMarsLending(valence_mars_lending::msg::LibraryConfig), }