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.
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:
Key Components
- Keeper: The module's keeper is responsible for managing the state of the CosmWasm pools, including creating and initializing pools,
querying pool data, and executing privileged operations such as swaps using the CosmWasm sudo message.
InitializePool
: Initializes a new CosmWasm pool by instantiating a Wasm contract and storing the pool model in the keeper.Swap operations
: Swap operations likeSwapExactAmountIn
andSwapExactAmountOut
are implemented, allowing users to perform swaps within the CosmWasm pools.Swap estimation
: Functions like CalcOutAmtGivenIn, and CalcInAmtGivenOut are provided to calculate prices and amounts for swap operations.Pool information
: Functions likeCalculateSpotPrice
,GetPool
,GetPoolAssets
,GetPoolBalances
,GetPoolTotalShares
allow for querying the state of the CosmWasm pools.
Query and Sudo functions: The module includes generic functions to query CosmWasm smart contracts and execute sudo messages. The Query and Sudo functions are used to interact with the smart contracts, while MustQuery and MustSudo variants panic if an error occurs during the query or sudo call, respectively.
poolmanager.PoolI
Interface: The CosmWasm Pool Model implements the PoolI interface from the Pool Manager Module to enable the creation and management of liquidity pools backed by CosmWasm smart contracts. By implementing the PoolI interface, the model ensures compatibility with the existing Pool Manager Module's structure and functionalities and integrates seamlessly with other modules such asx/concentrated-liquidity
andx/gamm
.poolmanager.PoolModule
Interface: To integrate the CosmWasm Pool Module with the existing Pool Manager Module, the module's keeper has to implement the PoolModule interface fromx/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 thex/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:
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 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
.
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.
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:
/// 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
.
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)
Query
#[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
#[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 the incentives module (or being composable in general), the contract needs to be able to create share tokens.
We handle this by utilizing the x/tokenfactory
module.
Each pool has share denom with this pattern: factory/{contract_address}/cw-pool/{custom-name}
.
The contract address uniquely identifies a pool. We also use cw-pool to make these denoms distinguishable
from other tokenfactory denoms and provide contracts the ability to customize the {custom-name}
.
Each contract is responsible for minting and burning its token factory shares. The chain does no interaction with tokenfactory.
To integrate x/cosmwasmpool
into the x/incentives
module, it also needs to create gauges.
This can be done by setting after_pool_created
on instantiate
response.
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:
#[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:
// 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 and Uint128 are represented as string in JSON.
Coin is:
pub struct Coin {
pub denom: String,
pub amount: Uint128,
}
Governance and Code Id Management
Despite code upload being permissioned by governance on Osmosis, it is allowed to be done by a certain set of addresses:
osmosisd q wasm params
code_upload_access:
address: ""
addresses:
- osmo1cd4nn8yzdrrsfqsmmvaafq8r03xn38qgqt8fzh
- osmo1wl59k23zngj34l7d42y9yltask7rjlnxgccawc7ltrknp6n52fps94qsjd
- osmo19vxp8vq8qm368dr026qxh8v82satwaf79y235lfv6wmgpwxx8dtskedaku
- osmo1e0x2hnhhwyek7eq3kcxu2x6pt77wdnwz0lutz9fespdr9utq963qr0y5p5
- osmo14n3a65fnqz9jve85l23al6m3pjugf0atvrfqh5
- osmo15wna5dwylkuzvljsudyn6zfsd4zl0rkg5ge888mzk4vtnjpp0z5q4e9w58
- osmo1r02tlyyaqs6tmrfa4jf37t7ewuxr57qp8ghzly
permission: AnyOfAddresses
instantiate_default_permission: Everybody
We would like to make sure that it is not possible to upload any pool code without governance approval. This is why we create two additional governance proposals:
Note, that in both cases, x/cosmwasmpool module account will act as the admin and creator of the contract.
1. Store code and update code id whitelist
Proposal Name: UploadCosmWasmPoolCodeAndWhiteListProposal
On successful passing of this proposal, the code id of the pool contract will be added to the whitelist. As a result, anyone would be able to instantiate a pool contract with this code id when creating a pol. No address will be able to maliciously upload a new code id and instantiate a pool contract with it without governance approval.
Inputs
uploadByteCode
-[]byte
- the raw wasm bytecode
The created code id is emitted via TypeEvtUploadedCosmwasmPoolCode
event.
2. Store code and migrate a specific code id to a new code id
Proposal Name: MigratePoolContractsProposal
Similarly, if we want to migrate a contract, anyone can do so but they will need to go through a custom governance proposal.
Migrates all given cw pool contracts specified by their IDs. It has two options to perform the migration.
a. If the codeID
is non-zero, it will migrate the pool contracts to a given codeID
assuming that it has already been uploaded. uploadByteCode must be empty in such a case. Fails if codeID
does not exist. Fails if uploadByteCode is not empty.
b. If the codeID
is zero, it will upload the given uploadByteCode
and use the new resulting code id to migrate the pool to. Errors if uploadByteCode is empty or invalid.
In both cases, if one of the pools specified by the given poolID
does not exist, the proposal fails.
The reason for having poolID
s be a slice of ids is to account for the potential need for emergency migration of all old code ids to new code ids, or simply having the flexibility of migrating multiple older pool contracts to a new one at once when there is a release.
poolD
s must be at the most size of PoolMigrationLimit
module parameter. It is configured to 20 at launch.
The proposal fails if more. Note that 20 was chosen arbitrarily to have a constant bound on the number of pools migrated at once.
Inputs
poolIDs
-[]uint64
codeID
-uint64
uploadByteCode
-[]byte
If the code is uploaded via proposal, the resulting code id is emitted via
TypeEvtMigratedCosmwasmPoolCode
.
Analysis of the Parameter Choice
Pros
- The flexibility is maximized as each pool id can be migrated individually either by uploading a new contract code or migrating to a pre-added one.
- There is still an ability to migrate all pools belonging to a list of code ids. The max number of pools migrated at once is bounded by a constant parameter.
Cons
- If there are multiple iterations of the same type, It might become cumbersome to keep track of which pool is on which code id, why one is migrated and the other one is not
- Some conditionality with proposal parameters. For example, if
codeId
is zero, byte code must be empty.
Overall, we concluded that pros outweigh the cons, and this is the best approach out of the other alternatives considered.
3. Whitelist Management via Params
Since the code id whitelist is implemented as a module parameter, in addition to the previous two proposals, the whitelist can be updated via parameter change proposal to either add or remove a code id from the whitelist independently of the code upload.
The relevant parameter for changing is CodeIdWhitelist
Note, that the update to the parameter overwrites all previous values so the proposer should be careful to include all code ids that should be whitelisted.
4. Pool Migration Limit via Params
Additionally, the maximum number of pools that can be migrated at once is also implemented
as a parameter. It is initialized to 20 in the v16 upgrade handler. However, governance
can tweak it by changing the PoolMigrationLimit
parameter.