diff --git a/Cargo.lock b/Cargo.lock index 15a36b447..0b47869ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -801,9 +801,11 @@ version = "2.0.3" dependencies = [ "cosmwasm-schema", "cosmwasm-std", + "cw-denom", "cw-multi-test", "cw-storage-plus 1.0.1 (git+https://github.com/DA0-DA0/cw-storage-plus.git)", "cw-utils 0.16.0", + "cw-vesting", "cw2 0.16.0", "cw20 0.16.0", "cw20-base 0.16.0", diff --git a/contracts/external/cw-token-swap/Cargo.toml b/contracts/external/cw-token-swap/Cargo.toml index 1e6903504..563dd4cc7 100644 --- a/contracts/external/cw-token-swap/Cargo.toml +++ b/contracts/external/cw-token-swap/Cargo.toml @@ -29,3 +29,5 @@ thiserror = { workspace = true } cosmwasm-schema = { workspace = true } cw-multi-test = { workspace = true } cw20-base = { workspace = true } +cw-vesting = { workspace = true } +cw-denom = { workspace = true } \ No newline at end of file diff --git a/contracts/external/cw-token-swap/schema/cw-token-swap.json b/contracts/external/cw-token-swap/schema/cw-token-swap.json index 80f134832..bfa4eecef 100644 --- a/contracts/external/cw-token-swap/schema/cw-token-swap.json +++ b/contracts/external/cw-token-swap/schema/cw-token-swap.json @@ -20,6 +20,25 @@ }, "additionalProperties": false, "definitions": { + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, "Counterparty": { "description": "Information about a counterparty in this escrow transaction and their promised funds.", "type": "object", @@ -36,14 +55,225 @@ "description": "The funds they have promised to provide.", "allOf": [ { - "$ref": "#/definitions/TokenInfo" + "$ref": "#/definitions/SwapInfo" } ] } }, "additionalProperties": false }, - "TokenInfo": { + "Cw20SendMsgs": { + "oneOf": [ + { + "type": "object", + "required": [ + "cw20_send" + ], + "properties": { + "cw20_send": { + "type": "object", + "required": [ + "amount", + "contract", + "msg" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "contract": { + "type": "string" + }, + "msg": { + "$ref": "#/definitions/Binary" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "cw20_burn" + ], + "properties": { + "cw20_burn": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "cw20_transfer" + ], + "properties": { + "cw20_transfer": { + "type": "object", + "required": [ + "amount", + "recipient" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "recipient": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "NativeSendMsgs": { + "oneOf": [ + { + "type": "object", + "required": [ + "bank_send" + ], + "properties": { + "bank_send": { + "type": "object", + "required": [ + "amount", + "to_address" + ], + "properties": { + "amount": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, + "to_address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "bank_burn" + ], + "properties": { + "bank_burn": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "wasm_execute" + ], + "properties": { + "wasm_execute": { + "type": "object", + "required": [ + "contract_addr", + "funds", + "msg" + ], + "properties": { + "contract_addr": { + "type": "string" + }, + "funds": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, + "msg": { + "$ref": "#/definitions/Binary" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "wasm_instantiate" + ], + "properties": { + "wasm_instantiate": { + "type": "object", + "required": [ + "code_id", + "funds", + "label", + "msg" + ], + "properties": { + "admin": { + "type": [ + "string", + "null" + ] + }, + "code_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "funds": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, + "label": { + "type": "string" + }, + "msg": { + "$ref": "#/definitions/Binary" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "SwapInfo": { "description": "Information about the token being used on one side of the escrow.", "oneOf": [ { @@ -57,7 +287,8 @@ "type": "object", "required": [ "amount", - "denom" + "denom", + "on_completion" ], "properties": { "amount": { @@ -65,6 +296,12 @@ }, "denom": { "type": "string" + }, + "on_completion": { + "type": "array", + "items": { + "$ref": "#/definitions/NativeSendMsgs" + } } }, "additionalProperties": false @@ -83,7 +320,8 @@ "type": "object", "required": [ "amount", - "contract_addr" + "contract_addr", + "on_completion" ], "properties": { "amount": { @@ -91,6 +329,12 @@ }, "contract_addr": { "type": "string" + }, + "on_completion": { + "type": "array", + "items": { + "$ref": "#/definitions/Cw20SendMsgs" + } } }, "additionalProperties": false @@ -233,6 +477,67 @@ "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" }, + "BankMsg": { + "description": "The message types of the bank module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/bank/v1beta1/tx.proto", + "oneOf": [ + { + "description": "Sends native tokens from the contract to the given address.\n\nThis is translated to a [MsgSend](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/bank/v1beta1/tx.proto#L19-L28). `from_address` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "send" + ], + "properties": { + "send": { + "type": "object", + "required": [ + "amount", + "to_address" + ], + "properties": { + "amount": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, + "to_address": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "This will burn the given coins from the contract's account. There is no Cosmos SDK message that performs this, but it can be done by calling the bank keeper. Important if a contract controls significant token supply that must be retired.", + "type": "object", + "required": [ + "burn" + ], + "properties": { + "burn": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + } + } + } + }, + "additionalProperties": false + } + ] + }, + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + }, "CheckedCounterparty": { "type": "object", "required": [ @@ -245,7 +550,7 @@ "$ref": "#/definitions/Addr" }, "promise": { - "$ref": "#/definitions/CheckedTokenInfo" + "$ref": "#/definitions/CheckedSwapInfo" }, "provided": { "type": "boolean" @@ -253,7 +558,7 @@ }, "additionalProperties": false }, - "CheckedTokenInfo": { + "CheckedSwapInfo": { "oneOf": [ { "type": "object", @@ -265,7 +570,8 @@ "type": "object", "required": [ "amount", - "denom" + "denom", + "on_completion" ], "properties": { "amount": { @@ -273,6 +579,12 @@ }, "denom": { "type": "string" + }, + "on_completion": { + "type": "array", + "items": { + "$ref": "#/definitions/CosmosMsg_for_Empty" + } } }, "additionalProperties": false @@ -290,7 +602,8 @@ "type": "object", "required": [ "amount", - "contract_addr" + "contract_addr", + "on_completion" ], "properties": { "amount": { @@ -298,6 +611,12 @@ }, "contract_addr": { "$ref": "#/definitions/Addr" + }, + "on_completion": { + "type": "array", + "items": { + "$ref": "#/definitions/CosmosMsg_for_Empty" + } } }, "additionalProperties": false @@ -307,9 +626,660 @@ } ] }, - "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" + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "CosmosMsg_for_Empty": { + "oneOf": [ + { + "type": "object", + "required": [ + "bank" + ], + "properties": { + "bank": { + "$ref": "#/definitions/BankMsg" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "custom" + ], + "properties": { + "custom": { + "$ref": "#/definitions/Empty" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "staking" + ], + "properties": { + "staking": { + "$ref": "#/definitions/StakingMsg" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "distribution" + ], + "properties": { + "distribution": { + "$ref": "#/definitions/DistributionMsg" + } + }, + "additionalProperties": false + }, + { + "description": "A Stargate message encoded the same way as a protobuf [Any](https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/any.proto). This is the same structure as messages in `TxBody` from [ADR-020](https://github.com/cosmos/cosmos-sdk/blob/master/docs/architecture/adr-020-protobuf-transaction-encoding.md)", + "type": "object", + "required": [ + "stargate" + ], + "properties": { + "stargate": { + "type": "object", + "required": [ + "type_url", + "value" + ], + "properties": { + "type_url": { + "type": "string" + }, + "value": { + "$ref": "#/definitions/Binary" + } + } + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "ibc" + ], + "properties": { + "ibc": { + "$ref": "#/definitions/IbcMsg" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "wasm" + ], + "properties": { + "wasm": { + "$ref": "#/definitions/WasmMsg" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "gov" + ], + "properties": { + "gov": { + "$ref": "#/definitions/GovMsg" + } + }, + "additionalProperties": false + } + ] + }, + "DistributionMsg": { + "description": "The message types of the distribution module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto", + "oneOf": [ + { + "description": "This is translated to a [MsgSetWithdrawAddress](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#L29-L37). `delegator_address` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "set_withdraw_address" + ], + "properties": { + "set_withdraw_address": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "description": "The `withdraw_address`", + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "This is translated to a [[MsgWithdrawDelegatorReward](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#L42-L50). `delegator_address` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "withdraw_delegator_reward" + ], + "properties": { + "withdraw_delegator_reward": { + "type": "object", + "required": [ + "validator" + ], + "properties": { + "validator": { + "description": "The `validator_address`", + "type": "string" + } + } + } + }, + "additionalProperties": false + } + ] + }, + "Empty": { + "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", + "type": "object" + }, + "GovMsg": { + "description": "This message type allows the contract interact with the [x/gov] module in order to cast votes.\n\n[x/gov]: https://github.com/cosmos/cosmos-sdk/tree/v0.45.12/x/gov\n\n## Examples\n\nCast a simple vote:\n\n``` # use cosmwasm_std::{ # HexBinary, # Storage, Api, Querier, DepsMut, Deps, entry_point, Env, StdError, MessageInfo, # Response, QueryResponse, # }; # type ExecuteMsg = (); use cosmwasm_std::{GovMsg, VoteOption};\n\n#[entry_point] pub fn execute( deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> Result { // ... Ok(Response::new().add_message(GovMsg::Vote { proposal_id: 4, vote: VoteOption::Yes, })) } ```\n\nCast a weighted vote:\n\n``` # use cosmwasm_std::{ # HexBinary, # Storage, Api, Querier, DepsMut, Deps, entry_point, Env, StdError, MessageInfo, # Response, QueryResponse, # }; # type ExecuteMsg = (); # #[cfg(feature = \"cosmwasm_1_2\")] use cosmwasm_std::{Decimal, GovMsg, VoteOption, WeightedVoteOption};\n\n# #[cfg(feature = \"cosmwasm_1_2\")] #[entry_point] pub fn execute( deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> Result { // ... Ok(Response::new().add_message(GovMsg::VoteWeighted { proposal_id: 4, options: vec![ WeightedVoteOption { option: VoteOption::Yes, weight: Decimal::percent(65), }, WeightedVoteOption { option: VoteOption::Abstain, weight: Decimal::percent(35), }, ], })) } ```", + "oneOf": [ + { + "description": "This maps directly to [MsgVote](https://github.com/cosmos/cosmos-sdk/blob/v0.42.5/proto/cosmos/gov/v1beta1/tx.proto#L46-L56) in the Cosmos SDK with voter set to the contract address.", + "type": "object", + "required": [ + "vote" + ], + "properties": { + "vote": { + "type": "object", + "required": [ + "proposal_id", + "vote" + ], + "properties": { + "proposal_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "vote": { + "description": "The vote option.\n\nThis should be called \"option\" for consistency with Cosmos SDK. Sorry for that. See .", + "allOf": [ + { + "$ref": "#/definitions/VoteOption" + } + ] + } + } + } + }, + "additionalProperties": false + } + ] + }, + "IbcMsg": { + "description": "These are messages in the IBC lifecycle. Only usable by IBC-enabled contracts (contracts that directly speak the IBC protocol via 6 entry points)", + "oneOf": [ + { + "description": "Sends bank tokens owned by the contract to the given address on another chain. The channel must already be established between the ibctransfer module on this chain and a matching module on the remote chain. We cannot select the port_id, this is whatever the local chain has bound the ibctransfer module to.", + "type": "object", + "required": [ + "transfer" + ], + "properties": { + "transfer": { + "type": "object", + "required": [ + "amount", + "channel_id", + "timeout", + "to_address" + ], + "properties": { + "amount": { + "description": "packet data only supports one coin https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/ibc/applications/transfer/v1/transfer.proto#L11-L20", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "channel_id": { + "description": "exisiting channel to send the tokens over", + "type": "string" + }, + "timeout": { + "description": "when packet times out, measured on remote chain", + "allOf": [ + { + "$ref": "#/definitions/IbcTimeout" + } + ] + }, + "to_address": { + "description": "address on the remote chain to receive these tokens", + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Sends an IBC packet with given data over the existing channel. Data should be encoded in a format defined by the channel version, and the module on the other side should know how to parse this.", + "type": "object", + "required": [ + "send_packet" + ], + "properties": { + "send_packet": { + "type": "object", + "required": [ + "channel_id", + "data", + "timeout" + ], + "properties": { + "channel_id": { + "type": "string" + }, + "data": { + "$ref": "#/definitions/Binary" + }, + "timeout": { + "description": "when packet times out, measured on remote chain", + "allOf": [ + { + "$ref": "#/definitions/IbcTimeout" + } + ] + } + } + } + }, + "additionalProperties": false + }, + { + "description": "This will close an existing channel that is owned by this contract. Port is auto-assigned to the contract's IBC port", + "type": "object", + "required": [ + "close_channel" + ], + "properties": { + "close_channel": { + "type": "object", + "required": [ + "channel_id" + ], + "properties": { + "channel_id": { + "type": "string" + } + } + } + }, + "additionalProperties": false + } + ] + }, + "IbcTimeout": { + "description": "In IBC each package must set at least one type of timeout: the timestamp or the block height. Using this rather complex enum instead of two timeout fields we ensure that at least one timeout is set.", + "type": "object", + "properties": { + "block": { + "anyOf": [ + { + "$ref": "#/definitions/IbcTimeoutBlock" + }, + { + "type": "null" + } + ] + }, + "timestamp": { + "anyOf": [ + { + "$ref": "#/definitions/Timestamp" + }, + { + "type": "null" + } + ] + } + } + }, + "IbcTimeoutBlock": { + "description": "IBCTimeoutHeight Height is a monotonically increasing data type that can be compared against another Height for the purposes of updating and freezing clients. Ordering is (revision_number, timeout_height)", + "type": "object", + "required": [ + "height", + "revision" + ], + "properties": { + "height": { + "description": "block height after which the packet times out. the height within the given revision", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "revision": { + "description": "the version that the client is currently on (eg. after reseting the chain this could increment 1 as height drops to 0)", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + }, + "StakingMsg": { + "description": "The message types of the staking module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto", + "oneOf": [ + { + "description": "This is translated to a [MsgDelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L81-L90). `delegator_address` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "delegate" + ], + "properties": { + "delegate": { + "type": "object", + "required": [ + "amount", + "validator" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Coin" + }, + "validator": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "This is translated to a [MsgUndelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L112-L121). `delegator_address` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "undelegate" + ], + "properties": { + "undelegate": { + "type": "object", + "required": [ + "amount", + "validator" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Coin" + }, + "validator": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "This is translated to a [MsgBeginRedelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L95-L105). `delegator_address` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "redelegate" + ], + "properties": { + "redelegate": { + "type": "object", + "required": [ + "amount", + "dst_validator", + "src_validator" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Coin" + }, + "dst_validator": { + "type": "string" + }, + "src_validator": { + "type": "string" + } + } + } + }, + "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" + } + ] + }, + "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" + }, + "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" + }, + "VoteOption": { + "type": "string", + "enum": [ + "yes", + "no", + "abstain", + "no_with_veto" + ] + }, + "WasmMsg": { + "description": "The message types of the wasm module.\n\nSee https://github.com/CosmWasm/wasmd/blob/v0.14.0/x/wasm/internal/types/tx.proto", + "oneOf": [ + { + "description": "Dispatches a call to another contract at a known address (with known ABI).\n\nThis is translated to a [MsgExecuteContract](https://github.com/CosmWasm/wasmd/blob/v0.14.0/x/wasm/internal/types/tx.proto#L68-L78). `sender` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "execute" + ], + "properties": { + "execute": { + "type": "object", + "required": [ + "contract_addr", + "funds", + "msg" + ], + "properties": { + "contract_addr": { + "type": "string" + }, + "funds": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, + "msg": { + "description": "msg is the json-encoded ExecuteMsg struct (as raw Binary)", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Instantiates a new contracts from previously uploaded Wasm code.\n\nThe contract address is non-predictable. But it is guaranteed that when emitting the same Instantiate message multiple times, multiple instances on different addresses will be generated. See also Instantiate2.\n\nThis is translated to a [MsgInstantiateContract](https://github.com/CosmWasm/wasmd/blob/v0.29.2/proto/cosmwasm/wasm/v1/tx.proto#L53-L71). `sender` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "instantiate" + ], + "properties": { + "instantiate": { + "type": "object", + "required": [ + "code_id", + "funds", + "label", + "msg" + ], + "properties": { + "admin": { + "type": [ + "string", + "null" + ] + }, + "code_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "funds": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, + "label": { + "description": "A human-readbale label for the contract", + "type": "string" + }, + "msg": { + "description": "msg is the JSON-encoded InstantiateMsg struct (as raw Binary)", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Migrates a given contracts to use new wasm code. Passes a MigrateMsg to allow us to customize behavior.\n\nOnly the contract admin (as defined in wasmd), if any, is able to make this call.\n\nThis is translated to a [MsgMigrateContract](https://github.com/CosmWasm/wasmd/blob/v0.14.0/x/wasm/internal/types/tx.proto#L86-L96). `sender` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "migrate" + ], + "properties": { + "migrate": { + "type": "object", + "required": [ + "contract_addr", + "msg", + "new_code_id" + ], + "properties": { + "contract_addr": { + "type": "string" + }, + "msg": { + "description": "msg is the json-encoded MigrateMsg struct that will be passed to the new code", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "new_code_id": { + "description": "the code_id of the new logic to place in the given contract", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Sets a new admin (for migrate) on the given contract. Fails if this contract is not currently admin of the target contract.", + "type": "object", + "required": [ + "update_admin" + ], + "properties": { + "update_admin": { + "type": "object", + "required": [ + "admin", + "contract_addr" + ], + "properties": { + "admin": { + "type": "string" + }, + "contract_addr": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "description": "Clears the admin on the given contract, so no more migration possible. Fails if this contract is not currently admin of the target contract.", + "type": "object", + "required": [ + "clear_admin" + ], + "properties": { + "clear_admin": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "type": "string" + } + } + } + }, + "additionalProperties": false + } + ] } } } diff --git a/contracts/external/cw-token-swap/src/contract.rs b/contracts/external/cw-token-swap/src/contract.rs index 283ea5ac0..55a4c880e 100644 --- a/contracts/external/cw-token-swap/src/contract.rs +++ b/contracts/external/cw-token-swap/src/contract.rs @@ -10,7 +10,8 @@ use cw_utils::must_pay; use crate::{ error::ContractError, msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, StatusResponse}, - state::{CheckedCounterparty, CheckedTokenInfo, COUNTERPARTY_ONE, COUNTERPARTY_TWO}, + state::{COUNTERPARTY_ONE, COUNTERPARTY_TWO}, + types::{CheckedCounterparty, CheckedSwapInfo}, }; pub(crate) const CONTRACT_NAME: &str = "crates.io:cw-token-swap"; @@ -112,14 +113,16 @@ fn do_fund( storage.save(deps.storage, &counterparty)?; let messages = if counterparty.provided && other_counterparty.provided { - vec![ - counterparty - .promise - .into_send_message(&other_counterparty.address)?, + let mut msgs = counterparty + .promise + .into_send_message(other_counterparty.address.to_string(), false)?; + + msgs.extend( other_counterparty .promise - .into_send_message(&counterparty.address)?, - ] + .into_send_message(counterparty.address.to_string(), false)?, + ); + msgs } else { vec![] }; @@ -143,9 +146,10 @@ pub fn execute_receive( storage, } = get_counterparty(deps.as_ref(), &sender)?; - let (expected_payment, paid) = if let CheckedTokenInfo::Cw20 { + let (expected_payment, paid) = if let CheckedSwapInfo::Cw20 { contract_addr, amount, + .. } = &counterparty.promise { if *contract_addr != token_contract { @@ -174,9 +178,8 @@ pub fn execute_fund(deps: DepsMut, info: MessageInfo) -> Result Result Result = Item::new("counterparty_one"); pub const COUNTERPARTY_TWO: Item = Item::new("counterparty_two"); - -impl Counterparty { - pub fn into_checked(self, deps: Deps) -> Result { - Ok(CheckedCounterparty { - address: deps.api.addr_validate(&self.address)?, - provided: false, - promise: self.promise.into_checked(deps)?, - }) - } -} - -impl TokenInfo { - pub fn into_checked(self, deps: Deps) -> Result { - match self { - TokenInfo::Native { denom, amount } => { - if amount.is_zero() { - Err(ContractError::ZeroTokens {}) - } else { - Ok(CheckedTokenInfo::Native { denom, amount }) - } - } - TokenInfo::Cw20 { - contract_addr, - amount, - } => { - if amount.is_zero() { - Err(ContractError::ZeroTokens {}) - } else { - let contract_addr = deps.api.addr_validate(&contract_addr)?; - // Make sure we are dealing with a cw20. - let _: cw20::TokenInfoResponse = deps.querier.query_wasm_smart( - contract_addr.clone(), - &cw20::Cw20QueryMsg::TokenInfo {}, - )?; - Ok(CheckedTokenInfo::Cw20 { - contract_addr, - amount, - }) - } - } - } - } -} - -impl CheckedTokenInfo { - pub fn into_send_message(self, recipient: &Addr) -> Result { - Ok(match self { - Self::Native { denom, amount } => BankMsg::Send { - to_address: recipient.to_string(), - amount: vec![Coin { denom, amount }], - } - .into(), - Self::Cw20 { - contract_addr, - amount, - } => WasmMsg::Execute { - contract_addr: contract_addr.into_string(), - msg: to_binary(&cw20::Cw20ExecuteMsg::Transfer { - recipient: recipient.to_string(), - amount, - })?, - funds: vec![], - } - .into(), - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_into_spend_message_native() { - let info = CheckedTokenInfo::Native { - amount: Uint128::new(100), - denom: "uekez".to_string(), - }; - let message = info.into_send_message(&Addr::unchecked("ekez")).unwrap(); - - assert_eq!( - message, - CosmosMsg::Bank(BankMsg::Send { - to_address: "ekez".to_string(), - amount: vec![Coin { - amount: Uint128::new(100), - denom: "uekez".to_string() - }] - }) - ); - } - - #[test] - fn test_into_spend_message_cw20() { - let info = CheckedTokenInfo::Cw20 { - amount: Uint128::new(100), - contract_addr: Addr::unchecked("ekez_token"), - }; - let message = info.into_send_message(&Addr::unchecked("ekez")).unwrap(); - - assert_eq!( - message, - CosmosMsg::Wasm(WasmMsg::Execute { - funds: vec![], - contract_addr: "ekez_token".to_string(), - msg: to_binary(&cw20::Cw20ExecuteMsg::Transfer { - recipient: "ekez".to_string(), - amount: Uint128::new(100) - }) - .unwrap() - }) - ); - } -} diff --git a/contracts/external/cw-token-swap/src/tests.rs b/contracts/external/cw-token-swap/src/tests.rs index 7e6073f20..c318c18d6 100644 --- a/contracts/external/cw-token-swap/src/tests.rs +++ b/contracts/external/cw-token-swap/src/tests.rs @@ -1,16 +1,17 @@ use cosmwasm_std::{ + coins, testing::{mock_dependencies, mock_env}, to_binary, Addr, Coin, Empty, Uint128, }; use cw20::Cw20Coin; -use cw_multi_test::{App, BankSudo, Contract, ContractWrapper, Executor, SudoMsg}; +use cw_multi_test::{next_block, App, BankSudo, Contract, ContractWrapper, Executor, SudoMsg}; use crate::{ contract::{migrate, CONTRACT_NAME, CONTRACT_VERSION}, - msg::{ - Counterparty, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, StatusResponse, TokenInfo, + msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, StatusResponse}, + types::{ + CheckedCounterparty, CheckedSwapInfo, Counterparty, Cw20SendMsgs, NativeSendMsgs, SwapInfo, }, - state::{CheckedCounterparty, CheckedTokenInfo}, ContractError, }; @@ -35,6 +36,15 @@ fn cw20_contract() -> Box> { Box::new(contract) } +fn cw_vesting() -> Box> { + let contract = ContractWrapper::new( + cw_vesting::contract::execute, + cw_vesting::contract::instantiate, + cw_vesting::contract::query, + ); + Box::new(contract) +} + #[test] fn test_simple_escrow() { let mut app = App::default(); @@ -70,16 +80,18 @@ fn test_simple_escrow() { &InstantiateMsg { counterparty_one: Counterparty { address: DAO1.to_string(), - promise: TokenInfo::Native { + promise: SwapInfo::Native { denom: "ujuno".to_string(), amount: Uint128::new(100), + on_completion: vec![], }, }, counterparty_two: Counterparty { address: DAO2.to_string(), - promise: TokenInfo::Cw20 { + promise: SwapInfo::Cw20 { contract_addr: cw20.to_string(), amount: Uint128::new(100), + on_completion: vec![], }, }, }, @@ -136,6 +148,493 @@ fn test_simple_escrow() { assert_eq!(dao2_balance.amount, Uint128::new(100)) } +#[test] +fn test_simple_with_send_messages() { + let mut app = App::default(); + + let cw20_code = app.store_code(cw20_contract()); + let escrow_code = app.store_code(escrow_contract()); + let vesting_code = app.store_code(cw_vesting()); + + let cw20 = app + .instantiate_contract( + cw20_code, + Addr::unchecked(DAO2), + &cw20_base::msg::InstantiateMsg { + name: "coin coin".to_string(), + symbol: "coin".to_string(), + decimals: 6, + initial_balances: vec![Cw20Coin { + address: DAO2.to_string(), + amount: Uint128::new(100), + }], + mint: None, + marketing: None, + }, + &[], + "coin", + None, + ) + .unwrap(); + + let vetsing_init_msg = cw_vesting::msg::InstantiateMsg { + owner: Some("owner".to_string()), + recipient: DAO2.to_string(), + title: "title".to_string(), + description: Some("description".to_string()), + total: Uint128::new(200), + denom: cw_denom::UncheckedDenom::Native("ujuno".to_string()), + schedule: cw_vesting::vesting::Schedule::SaturatingLinear, + start_time: None, + vesting_duration_seconds: 60 * 60 * 24 * 7, // one week + unbonding_duration_seconds: 60, + }; + + let escrow = app + .instantiate_contract( + escrow_code, + Addr::unchecked(DAO1), + &InstantiateMsg { + counterparty_one: Counterparty { + address: DAO1.to_string(), + promise: SwapInfo::Native { + denom: "ujuno".to_string(), + amount: Uint128::new(200), + on_completion: vec![NativeSendMsgs::WasmInstantiate { + admin: None, + code_id: vesting_code, + msg: to_binary(&vetsing_init_msg).unwrap(), + funds: coins(200, "ujuno"), + label: "some vesting".to_string(), + }], + }, + }, + counterparty_two: Counterparty { + address: DAO2.to_string(), + promise: SwapInfo::Cw20 { + contract_addr: cw20.to_string(), + amount: Uint128::new(100), + on_completion: vec![Cw20SendMsgs::Cw20Transfer { + recipient: "some_random".to_string(), + amount: Uint128::new(100), + }], + }, + }, + }, + &[], + "escrow", + None, + ) + .unwrap(); + + // In this case we are sending cw20 tokens, but expecting to get native token + // So we can send any set of messages we want here. + app.execute_contract( + Addr::unchecked(DAO2), + cw20.clone(), + &cw20::Cw20ExecuteMsg::Send { + contract: escrow.to_string(), + amount: Uint128::new(100), + msg: to_binary("").unwrap(), + }, + &[], + ) + .unwrap(); + + app.sudo(SudoMsg::Bank(BankSudo::Mint { + to_address: DAO1.to_string(), + amount: vec![Coin { + amount: Uint128::new(200), + denom: "ujuno".to_string(), + }], + })) + .unwrap(); + + // We recieve 100 cw20 token, just for fun, im trying to fund a different swap with this swap + // So once this swap is done, I can fund the other swap with the 50 cw20 tokens + app.execute_contract( + Addr::unchecked(DAO1), + escrow, + &ExecuteMsg::Fund {}, + &[Coin { + amount: Uint128::new(200), + denom: "ujuno".to_string(), + }], + ) + .unwrap(); + + // --- Cool everything passed, lets make sure everything is sent correctly --- + + // dao1 cw20 balance should be 0 because we sent it into the other escrow + let dao1_cw20_balance: cw20::BalanceResponse = app + .wrap() + .query_wasm_smart( + cw20.clone(), + &cw20::Cw20QueryMsg::Balance { + address: DAO2.to_string(), + }, + ) + .unwrap(); + assert_eq!(dao1_cw20_balance.balance, Uint128::new(0)); + + // Lets make sure the other escrow was funded correctly + // provided is true and the cw20 balance is 100 + let random_cw20_balance: cw20::BalanceResponse = app + .wrap() + .query_wasm_smart( + cw20, + &cw20::Cw20QueryMsg::Balance { + address: "some_random".to_string(), + }, + ) + .unwrap(); + assert_eq!(random_cw20_balance.balance, Uint128::new(100)); + + // Make sure that DAO1 native balance is 0 (sent to the vesting contract) + let dao1_balance = app.wrap().query_balance(DAO1.to_string(), "ujuno").unwrap(); + assert_eq!(dao1_balance.amount, Uint128::new(0)); +} + +#[test] +fn test_multiple_send_messages() { + let mut app = App::default(); + + let cw20_code = app.store_code(cw20_contract()); + let escrow_code = app.store_code(escrow_contract()); + + let cw20 = app + .instantiate_contract( + cw20_code, + Addr::unchecked(DAO2), + &cw20_base::msg::InstantiateMsg { + name: "coin coin".to_string(), + symbol: "coin".to_string(), + decimals: 6, + initial_balances: vec![Cw20Coin { + address: DAO2.to_string(), + amount: Uint128::new(200), + }], + mint: None, + marketing: None, + }, + &[], + "coin", + None, + ) + .unwrap(); + + let escrow = app + .instantiate_contract( + escrow_code, + Addr::unchecked(DAO1), + &InstantiateMsg { + counterparty_one: Counterparty { + address: DAO1.to_string(), + promise: SwapInfo::Native { + denom: "ujuno".to_string(), + amount: Uint128::new(200), + on_completion: vec![], + }, + }, + counterparty_two: Counterparty { + address: DAO2.to_string(), + promise: SwapInfo::Cw20 { + contract_addr: cw20.to_string(), + amount: Uint128::new(200), + on_completion: vec![ + Cw20SendMsgs::Cw20Transfer { + recipient: "some_random".to_string(), + amount: Uint128::new(100), + }, + Cw20SendMsgs::Cw20Burn { + amount: Uint128::new(100), + }, + ], + }, + }, + }, + &[], + "escrow", + None, + ) + .unwrap(); + + app.sudo(SudoMsg::Bank(BankSudo::Mint { + to_address: DAO1.to_string(), + amount: vec![Coin { + amount: Uint128::new(200), + denom: "ujuno".to_string(), + }], + })) + .unwrap(); + + app.execute_contract( + Addr::unchecked(DAO1), + escrow.clone(), + &ExecuteMsg::Fund {}, + &[Coin { + amount: Uint128::new(200), + denom: "ujuno".to_string(), + }], + ) + .unwrap(); + + app.execute_contract( + Addr::unchecked(DAO2), + cw20.clone(), + &cw20::Cw20ExecuteMsg::Send { + contract: escrow.to_string(), + amount: Uint128::new(200), + msg: to_binary("").unwrap(), + }, + &[], + ) + .unwrap(); + + let some_random_balance: cw20::BalanceResponse = app + .wrap() + .query_wasm_smart( + cw20, + &cw20::Cw20QueryMsg::Balance { + address: "some_random".to_string(), + }, + ) + .unwrap(); + assert_eq!(some_random_balance.balance, Uint128::new(100)); +} + +#[test] +fn test_withdraw_ignores_msgs() { + let mut app = App::default(); + + let cw20_code = app.store_code(cw20_contract()); + let escrow_code = app.store_code(escrow_contract()); + + let cw20 = app + .instantiate_contract( + cw20_code, + Addr::unchecked(DAO2), + &cw20_base::msg::InstantiateMsg { + name: "coin coin".to_string(), + symbol: "coin".to_string(), + decimals: 6, + initial_balances: vec![Cw20Coin { + address: DAO2.to_string(), + amount: Uint128::new(200), + }], + mint: None, + marketing: None, + }, + &[], + "coin", + None, + ) + .unwrap(); + + let escrow = app + .instantiate_contract( + escrow_code, + Addr::unchecked(DAO1), + &InstantiateMsg { + counterparty_one: Counterparty { + address: DAO1.to_string(), + promise: SwapInfo::Native { + denom: "ujuno".to_string(), + amount: Uint128::new(200), + on_completion: vec![], + }, + }, + counterparty_two: Counterparty { + address: DAO2.to_string(), + promise: SwapInfo::Cw20 { + contract_addr: cw20.to_string(), + amount: Uint128::new(200), + on_completion: vec![ + Cw20SendMsgs::Cw20Transfer { + recipient: "some_random".to_string(), + amount: Uint128::new(100), + }, + Cw20SendMsgs::Cw20Burn { + amount: Uint128::new(100), + }, + ], + }, + }, + }, + &[], + "escrow", + None, + ) + .unwrap(); + + app.execute_contract( + Addr::unchecked(DAO2), + cw20.clone(), + &cw20::Cw20ExecuteMsg::Send { + contract: escrow.to_string(), + amount: Uint128::new(200), + msg: to_binary("").unwrap(), + }, + &[], + ) + .unwrap(); + + app.update_block(next_block); + + // Make sure that we can withdraw, and it sends the funds to the correct address + app.execute_contract( + Addr::unchecked(DAO2), + escrow.clone(), + &ExecuteMsg::Withdraw {}, + &[], + ) + .unwrap(); + + let dao2_balance: cw20::BalanceResponse = app + .wrap() + .query_wasm_smart( + cw20.clone(), + &cw20::Cw20QueryMsg::Balance { + address: DAO2.to_string(), + }, + ) + .unwrap(); + assert_eq!(dao2_balance.balance, Uint128::new(200)); + + app.execute_contract( + Addr::unchecked(DAO2), + cw20, + &cw20::Cw20ExecuteMsg::Send { + contract: escrow.to_string(), + amount: Uint128::new(200), + msg: to_binary("").unwrap(), + }, + &[], + ) + .unwrap(); + + app.sudo(SudoMsg::Bank(BankSudo::Mint { + to_address: DAO1.to_string(), + amount: vec![Coin { + amount: Uint128::new(200), + denom: "ujuno".to_string(), + }], + })) + .unwrap(); + + app.execute_contract( + Addr::unchecked(DAO1), + escrow, + &ExecuteMsg::Fund {}, + &[Coin { + amount: Uint128::new(200), + denom: "ujuno".to_string(), + }], + ) + .unwrap(); +} + +#[test] +fn test_send_messages_incomplete_funds() { + let mut app = App::default(); + + let cw20_code = app.store_code(cw20_contract()); + let escrow_code = app.store_code(escrow_contract()); + + let cw20 = app + .instantiate_contract( + cw20_code, + Addr::unchecked(DAO2), + &cw20_base::msg::InstantiateMsg { + name: "coin coin".to_string(), + symbol: "coin".to_string(), + decimals: 6, + initial_balances: vec![Cw20Coin { + address: DAO2.to_string(), + amount: Uint128::new(100), + }], + mint: None, + marketing: None, + }, + &[], + "coin", + None, + ) + .unwrap(); + + let err = app + .instantiate_contract( + escrow_code, + Addr::unchecked(DAO1), + &InstantiateMsg { + counterparty_one: Counterparty { + address: DAO1.to_string(), + promise: SwapInfo::Native { + denom: "ujuno".to_string(), + amount: Uint128::new(200), + on_completion: vec![NativeSendMsgs::BankBurn { + amount: coins(100, "ujuno"), + }], + }, + }, + counterparty_two: Counterparty { + address: DAO2.to_string(), + promise: SwapInfo::Cw20 { + contract_addr: cw20.to_string(), + amount: Uint128::new(100), + on_completion: vec![Cw20SendMsgs::Cw20Burn { + amount: Uint128::new(100), + }], + }, + }, + }, + &[], + "escrow", + None, + ) + .unwrap_err() + .downcast::() + .unwrap(); + + assert_eq!(err, ContractError::WrongFundsCalculation {}); + + let err = app + .instantiate_contract( + escrow_code, + Addr::unchecked(DAO1), + &InstantiateMsg { + counterparty_one: Counterparty { + address: DAO1.to_string(), + promise: SwapInfo::Native { + denom: "ujuno".to_string(), + amount: Uint128::new(200), + on_completion: vec![NativeSendMsgs::BankBurn { + amount: coins(200, "ujuno"), + }], + }, + }, + counterparty_two: Counterparty { + address: DAO2.to_string(), + promise: SwapInfo::Cw20 { + contract_addr: cw20.to_string(), + amount: Uint128::new(100), + on_completion: vec![Cw20SendMsgs::Cw20Burn { + amount: Uint128::new(50), + }], + }, + }, + }, + &[], + "escrow", + None, + ) + .unwrap_err() + .downcast::() + .unwrap(); + + assert_eq!(err, ContractError::WrongFundsCalculation {}); +} + #[test] fn test_withdraw() { let mut app = App::default(); @@ -171,16 +670,18 @@ fn test_withdraw() { &InstantiateMsg { counterparty_one: Counterparty { address: DAO1.to_string(), - promise: TokenInfo::Native { + promise: SwapInfo::Native { denom: "ujuno".to_string(), amount: Uint128::new(100), + on_completion: vec![], }, }, counterparty_two: Counterparty { address: DAO2.to_string(), - promise: TokenInfo::Cw20 { + promise: SwapInfo::Cw20 { contract_addr: cw20.to_string(), amount: Uint128::new(100), + on_completion: vec![], }, }, }, @@ -264,17 +765,19 @@ fn test_withdraw() { StatusResponse { counterparty_one: CheckedCounterparty { address: Addr::unchecked(DAO1), - promise: CheckedTokenInfo::Native { + promise: CheckedSwapInfo::Native { denom: "ujuno".to_string(), - amount: Uint128::new(100) + amount: Uint128::new(100), + on_completion: vec![] }, provided: true, }, counterparty_two: CheckedCounterparty { address: Addr::unchecked(DAO2), - promise: CheckedTokenInfo::Cw20 { + promise: CheckedSwapInfo::Cw20 { contract_addr: cw20.clone(), - amount: Uint128::new(100) + amount: Uint128::new(100), + on_completion: vec![] }, provided: false, } @@ -302,17 +805,19 @@ fn test_withdraw() { StatusResponse { counterparty_one: CheckedCounterparty { address: Addr::unchecked(DAO1), - promise: CheckedTokenInfo::Native { + promise: CheckedSwapInfo::Native { denom: "ujuno".to_string(), - amount: Uint128::new(100) + amount: Uint128::new(100), + on_completion: vec![] }, provided: false, }, counterparty_two: CheckedCounterparty { address: Addr::unchecked(DAO2), - promise: CheckedTokenInfo::Cw20 { + promise: CheckedSwapInfo::Cw20 { contract_addr: cw20, - amount: Uint128::new(100) + amount: Uint128::new(100), + on_completion: vec![] }, provided: false, } @@ -355,16 +860,18 @@ fn test_withdraw_post_completion() { &InstantiateMsg { counterparty_one: Counterparty { address: DAO1.to_string(), - promise: TokenInfo::Native { + promise: SwapInfo::Native { denom: "ujuno".to_string(), amount: Uint128::new(100), + on_completion: vec![], }, }, counterparty_two: Counterparty { address: DAO2.to_string(), - promise: TokenInfo::Cw20 { + promise: SwapInfo::Cw20 { contract_addr: cw20.to_string(), amount: Uint128::new(100), + on_completion: vec![], }, }, }, @@ -464,16 +971,18 @@ fn test_invalid_instantiate() { &InstantiateMsg { counterparty_one: Counterparty { address: DAO1.to_string(), - promise: TokenInfo::Native { + promise: SwapInfo::Native { denom: "ujuno".to_string(), amount: Uint128::new(0), + on_completion: vec![], }, }, counterparty_two: Counterparty { address: DAO2.to_string(), - promise: TokenInfo::Cw20 { + promise: SwapInfo::Cw20 { contract_addr: cw20.to_string(), amount: Uint128::new(100), + on_completion: vec![], }, }, }, @@ -495,16 +1004,18 @@ fn test_invalid_instantiate() { &InstantiateMsg { counterparty_one: Counterparty { address: DAO1.to_string(), - promise: TokenInfo::Native { + promise: SwapInfo::Native { denom: "ujuno".to_string(), amount: Uint128::new(100), + on_completion: vec![], }, }, counterparty_two: Counterparty { address: DAO2.to_string(), - promise: TokenInfo::Cw20 { + promise: SwapInfo::Cw20 { contract_addr: cw20.to_string(), amount: Uint128::new(0), + on_completion: vec![], }, }, }, @@ -533,16 +1044,18 @@ fn test_non_distincy_counterparties() { &InstantiateMsg { counterparty_one: Counterparty { address: DAO1.to_string(), - promise: TokenInfo::Native { + promise: SwapInfo::Native { denom: "ujuno".to_string(), amount: Uint128::new(110), + on_completion: vec![], }, }, counterparty_two: Counterparty { address: DAO1.to_string(), - promise: TokenInfo::Native { + promise: SwapInfo::Native { denom: "ujuno".to_string(), amount: Uint128::new(10), + on_completion: vec![], }, }, }, @@ -592,16 +1105,18 @@ fn test_fund_non_counterparty() { &InstantiateMsg { counterparty_one: Counterparty { address: DAO1.to_string(), - promise: TokenInfo::Native { + promise: SwapInfo::Native { denom: "ujuno".to_string(), amount: Uint128::new(100), + on_completion: vec![], }, }, counterparty_two: Counterparty { address: DAO2.to_string(), - promise: TokenInfo::Cw20 { + promise: SwapInfo::Cw20 { contract_addr: cw20.to_string(), amount: Uint128::new(100), + on_completion: vec![], }, }, }, @@ -689,16 +1204,18 @@ fn test_fund_twice() { &InstantiateMsg { counterparty_one: Counterparty { address: DAO1.to_string(), - promise: TokenInfo::Native { + promise: SwapInfo::Native { denom: "ujuno".to_string(), amount: Uint128::new(100), + on_completion: vec![], }, }, counterparty_two: Counterparty { address: DAO2.to_string(), - promise: TokenInfo::Cw20 { + promise: SwapInfo::Cw20 { contract_addr: cw20.to_string(), amount: Uint128::new(100), + on_completion: vec![], }, }, }, @@ -809,16 +1326,18 @@ fn test_fund_invalid_amount() { &InstantiateMsg { counterparty_one: Counterparty { address: DAO1.to_string(), - promise: TokenInfo::Native { + promise: SwapInfo::Native { denom: "ujuno".to_string(), amount: Uint128::new(100), + on_completion: vec![], }, }, counterparty_two: Counterparty { address: DAO2.to_string(), - promise: TokenInfo::Cw20 { + promise: SwapInfo::Cw20 { contract_addr: cw20.to_string(), amount: Uint128::new(100), + on_completion: vec![], }, }, }, @@ -892,16 +1411,18 @@ fn test_fund_invalid_denom() { &InstantiateMsg { counterparty_one: Counterparty { address: DAO1.to_string(), - promise: TokenInfo::Native { + promise: SwapInfo::Native { denom: "ujuno".to_string(), amount: Uint128::new(100), + on_completion: vec![], }, }, counterparty_two: Counterparty { address: DAO2.to_string(), - promise: TokenInfo::Native { + promise: SwapInfo::Native { denom: "uekez".to_string(), amount: Uint128::new(100), + on_completion: vec![], }, }, }, @@ -995,16 +1516,18 @@ fn test_fund_invalid_cw20() { &InstantiateMsg { counterparty_one: Counterparty { address: DAO1.to_string(), - promise: TokenInfo::Native { + promise: SwapInfo::Native { denom: "ujuno".to_string(), amount: Uint128::new(100), + on_completion: vec![], }, }, counterparty_two: Counterparty { address: DAO2.to_string(), - promise: TokenInfo::Cw20 { + promise: SwapInfo::Cw20 { contract_addr: cw20.to_string(), amount: Uint128::new(100), + on_completion: vec![], }, }, }, diff --git a/contracts/external/cw-token-swap/src/types.rs b/contracts/external/cw-token-swap/src/types.rs new file mode 100644 index 000000000..366274410 --- /dev/null +++ b/contracts/external/cw-token-swap/src/types.rs @@ -0,0 +1,421 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{to_binary, Addr, BankMsg, Binary, Coin, CosmosMsg, Deps, Uint128, WasmMsg}; + +use crate::ContractError; + +#[cw_serde] +pub enum NativeSendMsgs { + BankSend { + to_address: String, + amount: Vec, + }, + BankBurn { + amount: Vec, + }, + WasmExecute { + contract_addr: String, + msg: Binary, + funds: Vec, + }, + WasmInstantiate { + admin: Option, + code_id: u64, + msg: Binary, + funds: Vec, + label: String, + }, +} + +impl NativeSendMsgs { + /// This is a helper function to convert the Cw20SendMsgs into a CosmosMsg + /// + /// Returns (amount_funds_to_send, CosmosMsg), we need `amount_funds_to_send` because later + /// we make sure total amount of funds sent is equal to the amount of funds promised + fn into_checked_cosmos_msg( + self, + deps: Deps, + denom: &str, // Promised denom. + ) -> Result<(Uint128, CosmosMsg), ContractError> { + // Function to verify the coins sent is valid + // it MUST be used in every match arm + let verify_coin = |coins: &Vec| { + if coins.len() != 1 { + return Err(ContractError::InvalidSendMsgFunds {}); + } + if coins[0].amount.is_zero() { + return Err(ContractError::InvalidSendMsgFunds {}); + } + if denom != coins[0].denom { + return Err(ContractError::InvalidSendMsgFunds {}); + } + Ok(coins[0].amount) + }; + + match self { + NativeSendMsgs::BankSend { to_address, amount } => Ok(( + verify_coin(&amount)?, + BankMsg::Send { + to_address: deps.api.addr_validate(&to_address)?.to_string(), + amount, + } + .into(), + )), + NativeSendMsgs::BankBurn { amount } => { + Ok((verify_coin(&amount)?, BankMsg::Burn { amount }.into())) + } + NativeSendMsgs::WasmExecute { + contract_addr, + msg, + funds, + } => Ok(( + verify_coin(&funds)?, + WasmMsg::Execute { + contract_addr: deps.api.addr_validate(&contract_addr)?.to_string(), + msg, + funds, + } + .into(), + )), + NativeSendMsgs::WasmInstantiate { + admin, + code_id, + msg, + funds, + label, + } => Ok(( + verify_coin(&funds)?, + WasmMsg::Instantiate { + admin: admin.map(|a| deps.api.addr_validate(&a).unwrap().to_string()), + code_id, + msg, + funds, + label, + } + .into(), + )), + } + } +} + +#[cw_serde] +pub enum Cw20SendMsgs { + Cw20Send { + contract: String, + amount: Uint128, + msg: Binary, + }, + Cw20Burn { + amount: Uint128, + }, + Cw20Transfer { + recipient: String, + amount: Uint128, + }, +} + +impl Cw20SendMsgs { + /// This is a helper function to convert the Cw20SendMsgs into a CosmosMsg + /// + /// Returns (amount_funds_to_send, CosmosMsg), we need `amount_funds_to_send` because later + /// we make sure total amount of funds sent is equal to the amount of funds promised + fn into_checked_cosmos_msg( + self, + deps: Deps, + cw20_addr: &str, + ) -> Result<(Uint128, CosmosMsg), ContractError> { + match self { + Cw20SendMsgs::Cw20Send { + contract, + amount, + msg, + } => Ok(( + amount, + WasmMsg::Execute { + contract_addr: cw20_addr.to_string(), + msg: to_binary(&cw20::Cw20ExecuteMsg::Send { + contract: deps.api.addr_validate(&contract)?.to_string(), + amount, + msg, + })?, + funds: vec![], + } + .into(), + )), + Cw20SendMsgs::Cw20Burn { amount } => Ok(( + amount, + WasmMsg::Execute { + contract_addr: cw20_addr.to_string(), + msg: to_binary(&cw20::Cw20ExecuteMsg::Burn { amount })?, + funds: vec![], + } + .into(), + )), + Cw20SendMsgs::Cw20Transfer { recipient, amount } => Ok(( + amount, + WasmMsg::Execute { + contract_addr: cw20_addr.to_string(), + msg: to_binary(&cw20::Cw20ExecuteMsg::Transfer { + recipient: deps.api.addr_validate(&recipient)?.to_string(), + amount, + })?, + funds: vec![], + } + .into(), + )), + } + } +} + +/// Information about a counterparty in this escrow transaction and +/// their promised funds. +#[cw_serde] +pub struct Counterparty { + /// The address of the counterparty. + pub address: String, + /// The funds they have promised to provide. + pub promise: SwapInfo, +} + +impl Counterparty { + pub fn into_checked(self, deps: Deps) -> Result { + Ok(CheckedCounterparty { + address: deps.api.addr_validate(&self.address)?, + provided: false, + promise: self.promise.into_checked(deps)?, + }) + } +} + +#[cw_serde] +pub struct CheckedCounterparty { + pub address: Addr, + pub promise: CheckedSwapInfo, + pub provided: bool, +} + +/// Information about the token being used on one side of the escrow. +#[cw_serde] +pub enum SwapInfo { + /// A native token. + Native { + denom: String, + amount: Uint128, + on_completion: Vec, + }, + /// A cw20 token. + Cw20 { + contract_addr: String, + amount: Uint128, + on_completion: Vec, + }, +} + +impl SwapInfo { + /// + pub fn into_checked(self, deps: Deps) -> Result { + match self { + SwapInfo::Native { + denom, + amount, + on_completion, + } => { + if amount.is_zero() { + Err(ContractError::ZeroTokens {}) + } else { + let on_completion = if on_completion.is_empty() { + vec![] + } else { + let (cosmos_msgs, tokens_sent) = on_completion.into_iter().try_fold( + (vec![], Uint128::zero()), + |(mut messages, total_sent), msg| -> Result<_, ContractError> { + let (sent, msg) = msg.into_checked_cosmos_msg(deps, &denom)?; + messages.push(msg); + Ok((messages, sent + total_sent)) + }, + )?; + + // Verify that total amount of funds matches funds sent in all messages + if tokens_sent != amount { + return Err(ContractError::WrongFundsCalculation {}); + } + cosmos_msgs + }; + + Ok(CheckedSwapInfo::Native { + denom, + amount, + on_completion, + }) + } + } + SwapInfo::Cw20 { + contract_addr, + amount, + on_completion, + } => { + if amount.is_zero() { + Err(ContractError::ZeroTokens {}) + } else { + let contract_addr = deps.api.addr_validate(&contract_addr)?; + // Make sure we are dealing with a cw20. + let _: cw20::TokenInfoResponse = deps.querier.query_wasm_smart( + contract_addr.clone(), + &cw20::Cw20QueryMsg::TokenInfo {}, + )?; + + let on_completion = if on_completion.is_empty() { + vec![] + } else { + let (cosmos_msgs, tokens_sent) = on_completion.into_iter().try_fold( + (vec![], Uint128::zero()), + |(mut messages, total_sent), msg| -> Result<_, ContractError> { + let (sent, msg) = + msg.into_checked_cosmos_msg(deps, contract_addr.as_str())?; + messages.push(msg); + Ok((messages, sent + total_sent)) + }, + )?; + + // Verify that total amount of funds matches funds sent in all messages + if tokens_sent != amount { + return Err(ContractError::WrongFundsCalculation {}); + } + cosmos_msgs + }; + + Ok(CheckedSwapInfo::Cw20 { + contract_addr, + amount, + on_completion, + }) + } + } + } + } +} + +#[cw_serde] +pub enum CheckedSwapInfo { + Native { + denom: String, + amount: Uint128, + on_completion: Vec, + }, + Cw20 { + contract_addr: Addr, + amount: Uint128, + on_completion: Vec, + }, +} + +impl CheckedSwapInfo { + /// Returns the msgs we need to send based on what we do/have + /// If we do withdraw, we ignore on completion msgs and set recipient as the withdrawer address + /// If swap successful, we either return on completion msgs if we have them or send funds to the other party + pub fn into_send_message( + self, + recipient: String, + is_withdraw: bool, + ) -> Result, ContractError> { + Ok(match self { + Self::Native { + denom, + amount, + on_completion, + } => { + // If completion msgs was specified we send them + if !is_withdraw && !on_completion.is_empty() { + return Ok(on_completion); + } + + // If completion msgs was not specified we send funds to the other party + vec![BankMsg::Send { + to_address: recipient, + amount: vec![Coin { denom, amount }], + } + .into()] + } + Self::Cw20 { + contract_addr, + amount, + on_completion, + } => { + // If completion msgs was specified we send them + if !is_withdraw && !on_completion.is_empty() { + return Ok(on_completion); + } + + // If completion msgs was not specified we transfer funds to the other party + vec![WasmMsg::Execute { + contract_addr: contract_addr.into_string(), + msg: to_binary(&cw20::Cw20ExecuteMsg::Transfer { recipient, amount })?, + funds: vec![], + } + .into()] + } + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_into_spend_message_native() { + let info = CheckedSwapInfo::Native { + amount: Uint128::new(100), + denom: "uekez".to_string(), + on_completion: vec![CosmosMsg::Bank(BankMsg::Send { + to_address: "ekez".to_string(), + amount: vec![Coin { + amount: Uint128::new(100), + denom: "uekez".to_string(), + }], + })], + }; + let message = info.into_send_message("ekez".to_string(), false).unwrap(); + + assert_eq!( + message[0], + CosmosMsg::Bank(BankMsg::Send { + to_address: "ekez".to_string(), + amount: vec![Coin { + amount: Uint128::new(100), + denom: "uekez".to_string() + }] + }) + ); + } + + #[test] + fn test_into_spend_message_cw20() { + let info = CheckedSwapInfo::Cw20 { + amount: Uint128::new(100), + contract_addr: Addr::unchecked("ekez_token"), + on_completion: vec![CosmosMsg::Wasm(WasmMsg::Execute { + funds: vec![], + contract_addr: "ekez_token".to_string(), + msg: to_binary(&cw20::Cw20ExecuteMsg::Transfer { + recipient: "ekez".to_string(), + amount: Uint128::new(100), + }) + .unwrap(), + })], + }; + let message = info.into_send_message("ekez".to_string(), false).unwrap(); + + assert_eq!( + message[0], + CosmosMsg::Wasm(WasmMsg::Execute { + funds: vec![], + contract_addr: "ekez_token".to_string(), + msg: to_binary(&cw20::Cw20ExecuteMsg::Transfer { + recipient: "ekez".to_string(), + amount: Uint128::new(100) + }) + .unwrap() + }) + ); + } +}