- 10 Jul 2024
- 19 Minutes to read
- DarkLight
- PDF
Pool
- Updated on 10 Jul 2024
- 19 Minutes to read
- DarkLight
- PDF
Resource Pools
Liquidity pools are a concept used pervasively in a very wide range of DeFi applications.
Users that participate in contributing to liquidity pools receive a token that represents their proportional contribution. These tokens are often called "LP tokens"; we call them more generically "pool units". The contents of the pool may shift over time (depending on the application) and ultimately the pool units are redeemable for the user’s proportion of the pool.
This makes pool units an important type of asset where the user would like to have a clear indication in their wallet exactly what those pool units are worth from their pool at any given time, and be confident that there is no question of their ability to redeem them. On other networks this is virtually impossible to do with any guarantees because each pool is implemented with arbitrary logic. To show users what pool units are worth consistently and without risk, and to ensure redeemability, pools and pool units must have guaranteed predictable behavior.
Fortunately the fundamental concept of the pool and pool unit is quite universal and so have created a native pool package that allows any developer to instantiate pools for their application without constraining application functionality. These native pool components and the pool units they issue allow the wallet to provide the information and guaranteed behavior that they desire, similar to other native components like accounts and validators.
The pool package has three blueprints: a one-resource pool blueprint, a two-resource pool blueprint, and a more general multi-resource pool blueprint. This page documents the two-resource pool blueprint. However, information provided here is still relevant to other blueprints but with small differences to data types.
Goals
While pool-based application functionality varies enormously, the pool concept itself is quite simple and has a set of consistent properties:
The pool has one or more predefined token types that it holds.
Users can contribute tokens to the pool.
If there is more than one token type in the pool, the ratio of token types contributed must match the ratio of the token types in the pool.
When users contribute, they always receive back a quantity of newly-minted pool unit tokens. For a contribution of tokens equal to X% of the pool, the user receives a quantity of pool units equal to X% of the total supply of those pool units at that moment.
Users can redeem pool units for tokens from the pool.
If there is more than one token type in the pool, the ratio of token types returned in the redemption matches the ratio of the token types in the pool.
When users redeem, they send a quantity of pool units to the pool. For a quantity of pool units equal to X% of the total supply of pool units at that moment, the pool returns tokens equal to X% of the pool. The redeemed pool units are burned.
Special entities outside of the pool have the rights to directly deposit tokens to or withdraw tokens from the pool according to application-specific logic.
With the above universal behavior, all of the variation of application usage of pools can be served with just three elements of pool configuration:
What token type(s) is the pool configured to accept?
What is the metadata configuration of the pool unit token, to "brand” it for users of the application?
Who/what has the rights to directly deposit and withdraw tokens from the pool according to the business logic of the application?
For example, a DEX:
The DEX system instantiates a pool with two token types for the two sides of a trading pair, XRD and ZOMBO.
It sets the pool unit metadata to have the name "CoolDEX: XRD/ZOMBO”, and a specified icon URL.
It sets the authorities for the protected_deposit/protected_withdraw methods to a badge held by the DEX’s component logic. That component logic would then use those methods to conduct XRD/ZOMBO trades out of the pool according to its preferred bonding curve, as well as perform any distribution of fees to the pool.
The native Pool component doesn’t presuppose what the pool means or who controls it via the protected_deposit/protected_withdraw methods; it only provides the basic universal pool functions: Contributions and withdrawals are of the correct type and adhere to the current pool ratio, and proportional pool unit minting/burning is done correctly to always represent the right share of the pool.
This in turn means that, with a native pool component, a wallet or dashboard UI for pool units knows some important things with certainty:
This is in fact a pool unit that was minted by a pool (not something that behaves oddly)
A quantity of pool units is in fact redeemable at this moment for a known quantity of tokens in the pool
No application logic may stop the holder of the pool units from redeeming them at the pool
Auth Roles
All three of the pool blueprints come with two auth roles whose definition is configurable by the instantiator of the blueprint. These two roles have the following responsibilities:
owner
: TheAccessRule
associated with the owner role can be configured by the instantiator of the pool. Theinstantiate
function on the pool will set this as the owner of both the pool unit resource and the pool component. The owner is given the ability to update the metadata of the pool component and pool unit resource.pool_manager_role
: This role is given the ability to call theprotected_withdraw
,protected_deposit
, andcontribute
methods on the pool components to manage and utilize the funds in the pool.
Based on the above, the following is an example configuration of the owner
and pool_manager_role
roles that developers who use the pool blueprints may wish to adopt. Say you’re developing a radiswap style blueprint of a Constant Function Market Maker (CFMM) which makes use of the two-resource pool under the hood for elegant management of pool units and pool ownership proportions. The owner role could be configured to be a badge that is stored in the account of the owner of the protocol such that they can update metadata on their pool components and pool unit resources freely after the instantiation of their components. The pool_manager_role
role could be configured to be a badge owned by the Radiswap component (or a virtual component caller badge) to allow the Radiswap component to manage the funds of the pool.
Pool Unit
Contributing to a pool provides liquidity providers with pool units that represent their proportion of ownership in the pool and can be redeemed for said proportion of the pool. Pool units have the following access rules configuration:
Role | Role Updater | |
---|---|---|
Mint | Pool | DenyAll |
Burn | Pool | DenyAll |
Withdraw | AllowAll | DenyAll |
Deposit | AllowAll | DenyAll |
Recall | DenyAll | DenyAll |
Update Metadata | Owner Role | DenyAll |
API Reference
This section documents the interface of the two-resource pool blueprint. The information provided here is also relevant for the one-resource pool and multi-resource pool blueprints but some of the arguments and return types might be different. However, the core concepts still apply.
Additional Details for the various pool blueprints can be found in the Rust docs
instantiate
Name |
|
---|---|
Type | Function |
Description | This function instantiates a new two-resource pool of the two resources provided in the There are certain cases where this function panics and the creation of the pool fails. These cases are as follows:
|
Callable by | Public |
Arguments |
|
Returns |
|
contribute
Name |
|
---|---|
Type | Method |
Description | A method that is only callable by the When this method is called, there are four states that the pool could be in which change the behavior of the pool slightly.
|
Callable by |
|
Arguments |
|
Returns |
|
Note | This method takes into account the case where one or both of the resources in the pool have divisibility that is not 18. In this case, the amount of resources that the pool accepts of the resource of non-18 divisibility is always rounded down to the nearest decimal point allowed for by the resource’s divisibility. The amount of pool units minted take this into account. |
redeem
Name |
|
---|---|
Type | Method |
Description | Given a There are certain cases where this method panics and redemption of pool units fails. These cases are as follows:
|
Callable By | Public |
Arguments |
|
Returns | ( |
Note | This method takes into account the case where one or both of the resources in the pool have divisibility that is not 18. In this case, the amount of resources given back to the caller is always rounded down to fit into the divisibility of the resource. In this case, a pool that gets completely drained out may have some dust remaining in one or more of its vaults. |
protected_deposit
Name |
|
---|---|
Type | Method |
Description | Given a There are certain cases where this method panics and the deposit fails. These cases are as follows:
|
Callable By |
|
Arguments |
|
Returns | Nothing |
protected_withdraw
Name |
|
---|---|
Type | Method |
Description | Given a There are certain cases where this method panics and the withdraw fails. These cases are as follows:
|
Callable By |
|
Arguments |
|
Returns |
|
get_redemption_value
Name |
|
---|---|
Type | Method |
Description | Calculates the amount of pool resources that some amount of pool units can be redeemed for. |
Callable By | Public |
Arguments |
|
Returns |
|
get_vault_amounts
Name |
|
---|---|
Type | Method |
Description | Returns the amount of reserves in the pool. |
Callable By | Public |
Arguments | none |
Returns |
|
Events
ContributionEvent
Name |
|
---|---|
Description | An event emitted when resources are contributed to the pool through the contribute method. |
Fields |
|
RedemptionEvent
Name |
|
---|---|
Description | An event that is emitted whenever pool units are redeemed from the pool through the redeem method. |
Fields |
|
WithdrawEvent
Name |
|
---|---|
Description | An event that is emitted whenever resources are withdrawn from the pool through the protected_withdraw method. |
Fields |
|
DepositEvent
Name |
|
---|---|
Description | An event that is emitted whenever resources are deposited into the pool through the |
Fields |
|
Metadata
Pool Component
Key | Value Type | Description |
---|---|---|
|
| The number of vaults that the pool component has. |
|
| The addresses of the resources in the pool. |
|
| The address of the pool unit resource associated with this pool. |
Pool Unit Resource
Key | Value Type | Description |
---|---|---|
|
| The address of the pool component that this pool unit resource is associated with. |
Example
A CFMM pool can be built on top of a two-resource pool, requiring the CFMM’s blueprint to only implement the functionality of a CFMM while letting the two-resource pool component handle the proportion of ownership of the pool and providing the liquidity providers with pool unit tokens which are recognized by the Babylon wallet and can be redeemed at any time directly through the wallet.
The following example is of a Radiswap blueprint, a CFMM pool blueprint that’s utilizes a two-resource pool.
use scrypto::prelude::*;
#[blueprint]
#[events(InstantiationEvent, AddLiquidityEvent, RemoveLiquidityEvent, SwapEvent)]
mod radiswap {
struct Radiswap {
pool_component: Global<TwoResourcePool>,
}
impl Radiswap {
pub fn new(
owner_role: OwnerRole,
resource_address1: ResourceAddress,
resource_address2: ResourceAddress,
dapp_definition_address: ComponentAddress,
) -> Global<Radiswap> {
let (address_reservation, component_address) =
Runtime::allocate_component_address(Radiswap::blueprint_id());
let global_component_caller_badge =
NonFungibleGlobalId::global_caller_badge(component_address);
// Creating a new pool will check the following for us:
// 1. That both resources are not the same.
// 2. That none of the resources are non-fungible
let pool_component = Blueprint::<TwoResourcePool>::instantiate(
owner_role.clone(),
rule!(require(global_component_caller_badge)),
(resource_address1, resource_address2),
None,
);
let component = Self { pool_component }
.instantiate()
.prepare_to_globalize(owner_role.clone())
.with_address(address_reservation)
.metadata(metadata!(
init {
"name" => "Radiswap", updatable;
"dapp_definition" => dapp_definition_address, updatable;
}
))
.globalize();
Runtime::emit_event(InstantiationEvent {
component_address: component.address(),
resource_address1,
resource_address2,
owner_role,
});
component
}
pub fn add_liquidity(
&mut self,
resource1: Bucket,
resource2: Bucket,
) -> (Bucket, Option<Bucket>) {
Runtime::emit_event(AddLiquidityEvent([
(resource1.resource_address(), resource1.amount()),
(resource2.resource_address(), resource2.amount()),
]));
// All the checks for correctness of buckets and everything else is handled by the pool
// component! Just pass it the resources and it will either return the pool units back
// if it succeeds or abort on failure.
self.pool_component.contribute((resource1, resource2))
}
/// This method does not need to be here - the pool units are redeemable without it by the
/// holders of the pool units directly from the pool. In this case this is just a nice proxy
/// so that users are only interacting with one component and do not need to know about the
/// address of Radiswap and the address of the Radiswap pool.
pub fn remove_liquidity(&mut self, pool_units: Bucket) -> (Bucket, Bucket) {
let pool_units_amount = pool_units.amount();
let (bucket1, bucket2) = self.pool_component.redeem(pool_units);
Runtime::emit_event(RemoveLiquidityEvent {
pool_units_amount,
redeemed_resources: [
(bucket1.resource_address(), bucket1.amount()),
(bucket2.resource_address(), bucket2.amount()),
],
});
(bucket1, bucket2)
}
pub fn swap(&mut self, input_bucket: Bucket) -> Bucket {
let mut reserves = self.vault_reserves();
let input_amount = input_bucket.amount();
let input_reserves = reserves
.remove(&input_bucket.resource_address())
.expect("Resource does not belong to the pool");
let (output_resource_address, output_reserves) = reserves.into_iter().next().unwrap();
let output_amount = input_amount
.checked_mul(output_reserves)
.unwrap()
.checked_div(input_reserves.checked_add(input_amount).unwrap())
.unwrap();
Runtime::emit_event(SwapEvent {
input: (input_bucket.resource_address(), input_bucket.amount()),
output: (output_resource_address, output_amount),
});
// NOTE: It's the responsibility of the user of the pool to do the appropriate rounding
// before calling the withdraw method.
self.deposit(input_bucket);
self.withdraw(output_resource_address, output_amount)
}
fn vault_reserves(&self) -> IndexMap<ResourceAddress, Decimal> {
self.pool_component.get_vault_amounts()
}
fn deposit(&mut self, bucket: Bucket) {
self.pool_component.protected_deposit(bucket)
}
fn withdraw(&mut self, resource_address: ResourceAddress, amount: Decimal) -> Bucket {
self.pool_component.protected_withdraw(
resource_address,
amount,
WithdrawStrategy::Rounded(RoundingMode::ToZero),
)
}
}
}
#[derive(ScryptoSbor, ScryptoEvent)]
pub struct InstantiationEvent {
pub owner_role: OwnerRole,
pub resource_address1: ResourceAddress,
pub resource_address2: ResourceAddress,
pub component_address: ComponentAddress,
}
#[derive(ScryptoSbor, ScryptoEvent)]
pub struct AddLiquidityEvent([(ResourceAddress, Decimal); 2]);
#[derive(ScryptoSbor, ScryptoEvent)]
pub struct RemoveLiquidityEvent {
pub pool_units_amount: Decimal,
pub redeemed_resources: [(ResourceAddress, Decimal); 2],
}
#[derive(ScryptoSbor, ScryptoEvent)]
pub struct SwapEvent {
pub input: (ResourceAddress, Decimal),
pub output: (ResourceAddress, Decimal),
}
All three of the pool blueprints come with stubs defined in Scrypto which provides type safety and allows for a rust-like way of invoking methods on the pool components. The possible stubs to use are: OneResourcePool, TwoResourcePool, and MultiResourcePool.
There are two cases where this function can panic: a) if both resources are the same, b) if any of the resources are non-fungible.
The pool does all of the necessary checks to ensure that the correct resources were provided and contains all of the logic for determining how much pool units to mint in return and whether there is any change to return back to the caller.
This method does not need to be here - the pool units are redeemable without it by the holders of the pool units directly from the pool. In this case this is just a nice proxy so that users are only interacting with one component and do not need to know about the address of Radiswap and the address of the Radiswap pool.
In the above example, the only method that the Radiswap blueprint needed to implement was the swap method which defines how the resources in the pool can be used by the manager of the pool to conduct an exchange or swap of resources. Additionally, most of the methods on the Radiswap blueprint are pass-through methods implemented purely for a nicer interface.