Skip to content

Commit

Permalink
Cosmwasmpool spec (#4810)
Browse files Browse the repository at this point in the history
* spec: creating cosmwasm pool

* spec: swap

* spec: deactivating

* spec: cosmwasm pool contract interface

* Update x/cosmwasmpool/README.md

Co-authored-by: Roman <[email protected]>

* decide on keeping swap on sudo and rephrase sudo's problem as spec

* remove get total shares

* update create pool flow per comment

* update explanation on ensuring token in

* update incentives & shares section

* update swap_fee explanation

* mention about no twap integration

---------

Co-authored-by: Roman <[email protected]>
  • Loading branch information
iboss-ptk and p0mvn authored May 29, 2023
1 parent b706442 commit 36c7d81
Showing 1 changed file with 345 additions and 9 deletions.
354 changes: 345 additions & 9 deletions x/cosmwasmpool/README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
# CosmWasm Pool Module
# CosmWasm Pool


## Overview
The CosmWasm Pool Module is an extension for the Osmosis pools, aiming to create a custom module that allows users to
create and manage liquidity pools backed by CosmWasm smart contracts. The feature enables developers to build and deploy
custom smart contracts that can be integrated with the rest of the pool types on the Osmosis chain.

The module is built on top of the CosmWasm smart contracting platform, which provides a secure and efficient way to develop
and execute WebAssembly (Wasm) smart contracts on the Cosmos SDK.
The CosmWasm Pool Module is an extension for the Osmosis pools, aiming to create a custom module that allows users to create and manage liquidity pools backed by CosmWasm smart contracts. The feature enables developers to build and deploy custom smart contracts that can be integrated with the rest of the pool types on the Osmosis chain.

The module is built on top of the CosmWasm smart contracting platform, which provides a secure and efficient way to develop and execute WebAssembly (Wasm) smart contracts on the Cosmos SDK.

Having pools in CosmWasm provides several benefits, one of which is avoiding the need for chain upgrades when introducing new functionalities or modifying existing ones related to liquidity pools. This advantage is particularly important in the context of speed of development and iteration.

Having pools in CosmWasm provides several benefits, one of which is avoiding the need for chain upgrades when introducing new functionalities
or modifying existing ones related to liquidity pools. This advantage is particularly important in the context of speed of development and
iteration.
An example of a CosmWasm pool type:
- [transmuter](https://github.com/osmosis-labs/transmuter)

## Key Components

Expand All @@ -37,3 +37,339 @@ other modules such as `x/concentrated-liquidity` and `x/gamm`.
the module's keeper has to implement the PoolModule interface from `x/poolmanager` Module. By implementing the PoolModule interface,
the CosmWasm Pool Keeper can register itself as an extension to the existing Pool Manager Module and handle the creation and management
of CosmWasm-backed liquidity pools as well as receive swaps propagated from the `x/poolmanager`.

## Creating new CosmWasm Pool

To create new CosmWasm Pool, there are 3 modules involved: `x/cosmwasmpool`, `x/wasm`, and `x/poolmanager`. Here is an overview of the process:

```mermaid
graph TD;
Sender((Sender))
Sender -- create poool --> x/cosmwasmpool
x/cosmwasmpool -- get next & set pool id --> x/poolmanager
x/cosmwasmpool -- instantiate contract --> x/wasm
```

The CosmWasm contract that is to be instanitiated needs to implement [CosmWasm Pool Contract Interface](#cosmwasm-pool-contract-interface) and store it on chain first. Then new pool can be created by sending `MsgCreateCosmWasmPool`.


`MsgCreateCosmWasmPool` contains `InstantiateMsg`, which is a message that will be passed to the CosmWasm contract when it is instantiated. The structure of the message is defined by the contract developer, and can contain any information that the contract needs to be instantiated. JSON format is used for `InstantiateMsg`.

```mermaid
sequenceDiagram
participant Sender
participant x/cosmwasmpool
participant x/poolmanager
participant x/wasm
Sender ->> x/cosmwasmpool: MsgCreateCosmWasmPool {CodeId, InstantiateMsg, Sender}
Note over x/wasm: Given there is a pool contract with CodeId
x/poolmanager ->> x/cosmwasmpool: Call GetNextPoolId()
x/cosmwasmpool ->> x/poolmanager: Call SetNextPoolId(poolId)
x/cosmwasmpool ->> x/wasm: Call InstantiateContract(CodeId, InstantiateMsg)
x/wasm -->> x/cosmwasmpool: ContractAddress
Note over x/cosmwasmpool: Store CodeId, ContractAddress, and PoolId
x/cosmwasmpool -->> Sender: MsgCreateCosmWasmPoolResponse {PoolId}
```


## Providing / Withdrawing Liquidity

Currently, all existing pool types have their own way of providing liquidity and shares calculation. CosmWasm pool aims to be flexible that regards and let the contract define the way of providing liquidity. So there is no restriction here, and the contract developer can define the way of providing liquidity as they wish, potentially with execute endpoint since `MsgExecuteContract` triggers state mutating endpoint and can also attach funds to it.

Common interface and later be defined for the contract to implement as spec and/or create a separated crate for that purpose.

It's important to note that the _**contract itselfs hold tokens that are provided by users**_.


## Swap

One of the main reason why CosmWasm pool is implemented as a module + contract rather than a contract only is that it allows us to use the existing pool manager module to handle swap, which means things like swap routing, cross chain swap, and other functionality that depends on existing pool interface works out of the box.

```mermaid
graph TD;
Sender((Sender))
Sender -- swap --> x/poolmanager
x/poolmanager -- route msg to --> x/cosmwasmpool
x/cosmwasmpool -- sudo execute contract --> x/wasm
x/wasm -- sudo --> wasm/pool
x/cosmwasmpool -- send token_in from sender to wasm/pool --> x/bank
wasm/pool -- send token_out to sender --> x/bank
```

Pool contract's sudo endpoint expect the following message variant:

```rs
/// SwapExactAmountIn swaps an exact amount of tokens in for as many tokens out as possible.
/// The amount of tokens out is determined by the current exchange rate and the swap fee.
/// The user specifies a minimum amount of tokens out, and the transaction will revert if that amount of tokens
/// is not received.
SwapExactAmountIn {
sender: String,
token_in: Coin,
token_out_denom: String,
token_out_min_amount: Uint128,
swap_fee: Decimal,
},
/// SwapExactAmountOut swaps as many tokens in as possible for an exact amount of tokens out.
/// The amount of tokens in is determined by the current exchange rate and the swap fee.
/// The user specifies a maximum amount of tokens in, and the transaction will revert if that amount of tokens
/// is exceeded.
SwapExactAmountOut {
sender: String,
token_in_denom: String,
token_in_max_amount: Uint128,
token_out: Coin,
swap_fee: Decimal,
},
```

The reason why this needs to be sudo endpoint, which can only be called by the chain itself, is that the chain can provide correct information about `swap_fee`, which can be deviated from contract defined `swap_fee` in multihop scenario.

`swap_fee` in this context is intended to be fee that is collected by liquidity providers. If the contract provider wants to collect fee for itself, it should implement its own fee collection mechanism.

And because sudo message can't attach funds like execute message, chain-side is required to perform sending token to the contract and ensure that `token_in` and `token_in_max_amount` is exactly the same amount of token that gets sent to the contract.


## Deactivating

On contract's sudo enpoint, `SetActive` can be called to deactivate the pool. This will prevent the pool from being used for swap, and also prevent users from providing liquidity to the pool. Contract needs to check if the pool is active before performing any state mutating operation except `SetActive`.

```rs
SetActive {
is_active: bool,
}
```

(TBD) On how to handle the deactivation operationally.

## CosmWasm Pool Contract Interface

The contract interface is defined so that `cosmwasmpool` can delegate `PoolI` and `PoolModuleI` calls to contract.

The following are the messages that the contract needs to implement. (If you have trouble interpreting this, please read [Rust de/serialization](#rust-deserialization))

### Query
```rs
#[cw_serde]
#[derive(QueryResponses)]
enum QueryMessage {
/// GetSwapFee returns the pool's swap fee, based on the current state.
/// Pools may choose to make their swap fees dependent upon state
/// (prior TWAPs, network downtime, other pool states, etc.)
/// This is intended to be fee that is collected by liquidity providers.
/// If the contract provider wants to collect fee for itself, it should implement its own fee collection mechanism.
#[returns(GetSwapFeeResponse)]
GetSwapFee {},

/// Returns whether the pool has swaps enabled at the moment
#[returns(IsActiveResponse)]
IsActive {},

/// GetTotalShares returns the total number of LP shares in the pool

/// GetTotalPoolLiquidity returns the coins in the pool owned by all LPs
#[returns(TotalPoolLiquidityResponse)]
GetTotalPoolLiquidity {},

/// Returns the spot price of the 'base asset' in terms of the 'quote asset' in the pool,
/// errors if either baseAssetDenom, or quoteAssetDenom does not exist.
/// For example, if this was a UniV2 50-50 pool, with 2 ETH, and 8000 UST
/// pool.SpotPrice(ctx, "eth", "ust") = 4000.00
#[returns(SpotPriceResponse)]
SpotPrice {
quote_asset_denom: String,
base_asset_denom: String,
},

/// CalcOutAmtGivenIn calculates the amount of tokenOut given tokenIn and the pool's current state.
/// Returns error if the given pool is not a CFMM pool. Returns error on internal calculations.
#[returns(CalcOutAmtGivenInResponse)]
CalcOutAmtGivenIn {
token_in: Coin,
token_out_denom: String,
swap_fee: Decimal,
},

/// CalcInAmtGivenOut calculates the amount of tokenIn given tokenOut and the pool's current state.
/// Returns error if the given pool is not a CFMM pool. Returns error on internal calculations.
#[returns(CalcInAmtGivenOutResponse)]
CalcInAmtGivenOut {
token_out: Coin,
token_in_denom: String,
swap_fee: Decimal,
},
}
#[cw_serde]
pub struct GetSwapFeeResponse {
pub swap_fee: Decimal,
}

#[cw_serde]
pub struct IsActiveResponse {
pub is_active: bool,
}

#[cw_serde]
pub struct TotalPoolLiquidityResponse {
pub total_pool_liquidity: Vec<Coin>,
}

#[cw_serde]
pub struct SpotPriceResponse {
pub spot_price: Decimal,
}

#[cw_serde]
pub struct CalcOutAmtGivenInResponse {
pub token_out: Coin,
}

#[cw_serde]
pub struct CalcInAmtGivenOutResponse {
pub token_in: Coin,
}
```

### Sudo

```rs
#[cw_serde]
pub enum SudoMessage {
/// SetActive sets the active status of the pool.
SetActive {
is_active: bool,
},
/// SwapExactAmountIn swaps an exact amount of tokens in for as many tokens out as possible.
/// The amount of tokens out is determined by the current exchange rate and the swap fee.
/// The user specifies a minimum amount of tokens out, and the transaction will revert if that amount of tokens
/// is not received.
SwapExactAmountIn {
sender: String,
token_in: Coin,
token_out_denom: String,
token_out_min_amount: Uint128,
swap_fee: Decimal,
},
/// SwapExactAmountOut swaps as many tokens in as possible for an exact amount of tokens out.
/// The amount of tokens in is determined by the current exchange rate and the swap fee.
/// The user specifies a maximum amount of tokens in, and the transaction will revert if that amount of tokens
/// is exceeded.
SwapExactAmountOut {
sender: String,
token_in_denom: String,
token_in_max_amount: Uint128,
token_out: Coin,
swap_fee: Decimal,
},
}
```

## Incentives and Shares

In order to allow CosmWasm pool to work with incentives module (or being composable in general), the contract needs to be able to create shares token and have ability to mint/burn them.

Each pool have share denom with this pattern: `cosmwasmpool/address/{contract_address}`.

(Using `contract_address` instead of `pool_id` giving the advantage of not needing to query `pool_id` in contract to reconstuct denom. It's mapping is 1:1 and already stored on chain, so on chain logic can work that out as well if it needs to.

Alternatively, we can use `cosmwasmpool/pool/{pool_id}` and have contract query for it to maintain consistency. (TBD))


```go
type MsgMintShares struct {
Sender sdk.AccAddress // the address of the pool contract
Shares sdk.Coin
}
```

```go
type MsgBurnShares struct {
Sender sdk.AccAddress // the address of the pool contract
Shares sdk.Coin
}
```

These msgs will get validated by the `cosmwasmpool` module:
- ensure that sender is the pool contract
- ensure that the denom has valid pattern, and `contract_address` is matched with sender

If they are valid, it will mint/burn the shares.

With shares denom, anyone can create guage with `incentives` module. But in existing pool, there is a way to set up guage automatically when the pool is created and wire things up in `pool-incentive` module.

This can be done by setting `after_pool_created` on `instantiate` response.

```rs
Ok(Response::new()
.add_attribute("method", "instantiate")
.add_attribute("contract_name", CONTRACT_NAME)
.add_attribute("contract_version", CONTRACT_VERSION)
// set `after_pool_created` information on response
// for `cosmwasmpool` module to process
.set_data(to_binary(&after_pool_created)?))
```

`after_pool_created` has type:

```rs
#[cw_serde]
pub struct AfterPoolCreated {
pub create_pool_guages: Option<CreatePoolGauges>,
}

#[cw_serde]
pub enum CreatePoolGauges {
// This works exactly like `gamm`'s.
DefaultLockableDurations {},
// Custom guages can be created.
Custom { msgs: Vec<MsgCreateGauge> },
}
```

---
## Appendix

### TWAP
`x/twap` is not implemented for cosmwasm pools but can be in the future if there is a need.

### Rust de/serialization

Contract read these msg as JSON format. Here are some examples of how it is being de/serialized:

```rs
// Notice that enum variant is turned into snake case and becomes the key of the JSON object.
enum QueryMessage {
// { "spot_price": { "quote_asset_denom": "denom1", "base_asset_denom": "denom2" } }
SpotPrice {
quote_asset_denom: String,
base_asset_denom: String,
},
}


// In case of struct, the struct name is not used as the key,
// since there is no need to distinguish between different structs.
struct SpotPriceResponse {
// { "spot_price": "0.001" }
pub spot_price: Decimal,
}
```

[Decimal](https://docs.rs/cosmwasm-std/1.2.3/cosmwasm_std/struct.Decimal.html) and [Uint128](https://docs.rs/cosmwasm-std/1.2.3/cosmwasm_std/struct.Uint128.html) are represented as string in JSON.

[Coin](https://docs.rs/cosmwasm-std/1.2.3/cosmwasm_std/struct.Coin.html) is:

```rs
pub struct Coin {
pub denom: String,
pub amount: Uint128,
}
```

0 comments on commit 36c7d81

Please sign in to comment.