04 Auth
4.8 Building a Decentralized Exchange (DEX)
Congratulations for getting through Chapter 4 on Authorization and Authentication! There was quite a bit to unpack in this chapter. But you will find this knowledge will help you build very powerful dApps on the Radix Network. This section will solidify and put into practice some of the concepts that you learned in this chapter by walking through how a Decentralized Exchange (DEX) can be built with Scrypto. The main takeaways from this section are:
- Build on top of what you’ve learned since Chapter 3.
- Put into practice some security implementations we learned in Chapter 4.
- Designing more complex logic in our Blueprint.
//Objectives
We want to build a Uniswap-like DEX that essentially that implements the standard Constant Product Market Maker Model,
x * y = k rule. So our design objectives are:- Create a liquidity pool that allows users to fluidly swap between a pair of tokens implementing the Constant Product Market Maker Model formula.
- Allow us to set a price to set a swap fee that will be applied for every token swap.
- Mint liquidity pool tokens to represent ownership of liquidity provided by liquidity providers.
- Provide users with seamless ability to add liquidity, remove liquidity, and swap tokens.
//Building a Conceptual Understanding of a DEX
While we now have proficient Scrypto abilities, we need to round out our skills by understanding some preliminary DeFi concepts to apply our Scrypto skills effectively. Being a developer employs us to consider what we are building by understanding how things work. When we have a schematic of what we are building, we can then focus on the how by expressing its mechanics in programming language using Scrypto.
DEX’s relies on liquidity pools to allow for efficient exchange between one token for another. In order for a user to exchange Token A for Token B, the user already has Token A to give, yet needs Token B from somewhere to receive from. How this mechanics is governed is done by the Liquidity Pool which houses both Token A and Token B. Let’s dive deeper.
DEX’s relies on liquidity pools to allow for efficient exchange between one token for another. In order for a user to exchange Token A for Token B, the user already has Token A to give, yet needs Token B from somewhere to receive from. How this mechanics is governed is done by the Liquidity Pool which houses both Token A and Token B. Let’s dive deeper.
The Mechanics of a Liquidity Pool
To help explain the concept of liquidity pools and how swaps happen, let's begin by giving an example for a person who wants to swap their tokens. Let's say that a guy called Tim wants to swap 10 BTC for XRD and he wishes to use a DEX to perform this swap of tokens. Tim goes to his favorite DEX and performs this swap and get's his XRD back in return. A question now begs itself: Where did the XRD given to Tim come from?
The XRD that Tim was given from the swap came from the BTC/XRD liquidity pool of the Automated market maker (AMM) which allows automatic and unrestricted trading of digital assets through the use of liquidity pools in place of a conventional marketplace between buyers and sellers. Liquidity pools are the core backbone of AMMs. They're typically implemented as smart contracts that hold the reserves of two tokens (BTC and XRD in this case) and allows users to trade these tokens for one another. The swap that Tim did is actually quite a simple operation: Tim sent his BTC to the pool to deposit it and the pool sent back some XRD to Tim. Another question now begs itself: How much XRD will Tim be given for his swap? How can we even calculate that?
The XRD that Tim was given from the swap came from the BTC/XRD liquidity pool of the Automated market maker (AMM) which allows automatic and unrestricted trading of digital assets through the use of liquidity pools in place of a conventional marketplace between buyers and sellers. Liquidity pools are the core backbone of AMMs. They're typically implemented as smart contracts that hold the reserves of two tokens (BTC and XRD in this case) and allows users to trade these tokens for one another. The swap that Tim did is actually quite a simple operation: Tim sent his BTC to the pool to deposit it and the pool sent back some XRD to Tim. Another question now begs itself: How much XRD will Tim be given for his swap? How can we even calculate that?
Primer on Constant Product Market Makers
This is where the concept of constant product market makers (CPMMs) comes in. The BTC/XRD liquidity pool from this example are constrained by the function
When Tim swaps an amount of BTC for XRD, the amount of XRD given back to Tim must be a value that keeps
x * y = k. In this example, we can define x to BTC and y to be XRD. k would then be the total liquidity of both BTC and XRD where k is required to remain constant. When Tim swaps an amount of BTC for XRD, the amount of XRD given back to Tim must be a value that keeps
k constant. Therefore, we can extend the constant market maker function to be as follows:Where dx is the input amount of BTC and dy is the XRD output that is returned to Tim. From the above defined equation we can derive other equations that we need for the DEX. So to calculate the output XRD returned to Tim, we’ll rearrange the equation such that:
As it can be seen from the equation above, the amount of XRD that will be given to Tim depends on three things:
Since the liquidity pool has access to all of this information, the amount of XRD that will be given to Tim can easily be calculated by the liquidity pool and then Tim can be sent that amount of XRD in a bucket.
- How much BTC did he provide as input dx
- How much BTC does the liquidity pool have x (before the swap)
- How much XRD does the liquidity pool have y (before the swap)
Since the liquidity pool has access to all of this information, the amount of XRD that will be given to Tim can easily be calculated by the liquidity pool and then Tim can be sent that amount of XRD in a bucket.
The Role of Liquidity Providers
As mentioned, liquidity pools are what allows tokens to be swapped from one to another. Liquidity pools then need a supply (liquidity) of these tokens held in reserves, ready to be used to perform swaps. Therefore we need liquidity providers to supply our pool with each tokens. So now, our question is, how do we incentivize people to provide liquidity to our liquidity pools?
Built-In Swap Fees
One way to incentivize liquidity providers to supply tokens to our liquidity pool is by paying them a fee. We can build in a fee as part of the CPMM equation to allow liquidity providers to collect a fee for each swap that is performed in our DEX. We can take our equation and modify to reflect as so.
We apply r as the fee that is applied to each swap. So now we have a primer on how a DEX works, let’s put what we learned in practice with Scrypto!
//Defining our Blueprint
Just as before, let’s start with a blank canvass by having an empty blueprint; I’ve already named my blueprint as Radiswap.
#[blueprint]mod radiswap_module { struct Radiswap { } impl Radiswap { pub fn instantiate_radiswap() -> Global<Radiswap> { Self { } .instantiate() .prepare_to_globalize(OwnerRole::None) .globalize() } }}//Defining our Struct
Based on what we learned of how a DEX works, we know that we need a liquidity pool and we know that this liquidity pool needs to hold two types of tokens so users can exchange one token for another. Additionally, we want to be able to determine the fees for our DEX so we can incentivize liquidity providers to supply tokens to our pool. So let’s define that in our struct.
#[blueprint]mod radiswap { struct Radiswap { vault_a: FungibleVault, vault_b: FungibleVault, pool_units_resource_manager: ResourceManager, fee: Decimal, } impl Radiswap { pub fn instantiate_radiswap() -> Global<Radiswap> { Self { } .instantiate() .prepare_to_globalize(OwnerRole::None) .globalize() } }}//Creating our Resource(s)
Since we want to create a resource to track a liquidity provider’s share of the pool, let’s put that as our first tangible thing our component will create when it’s instantiated.
use scrypto::prelude::*;#[blueprint]mod radiswap { struct Radiswap { vault_a: FungibleVault, vault_b: FungibleVault, pool_units_resource_manager: ResourceManager, fee: Decimal, } impl Radiswap { pub fn instantiate_radiswap() -> Global<Radiswap> { let pool_units: FungibleBucket = ResourceBuilder::new_fungible(OwnerRole::None) .metadata(metadata!( init { "name" => "Pool Units", locked; } )) .mint_roles(mint_roles!( minter => rule!(allow_all); minter_updater => rule!(deny_all); )) .burn_roles(burn_roles!( burner => rule!(allow_all); burner_updater => rule!(deny_all); )) .create_with_no_initial_supply(); Self { } .instantiate() .prepare_to_globalize(OwnerRole::None) .globalize() } }}The resource we use to represent our liquidity provider will be called pool_units. This resource will have two resource behavior that will be needed to fluidly mint supply when liquidity providers deposit liquidity to the pool to track their share of the pool and burn supply when liquidity providers decide to redeem their liquidity from the pool.
Assigning Resource Roles using Component Virtual Badge
You probably noticed the contrived
So the component needs to have the permission to do this and as we’ve learned throughout this chapter, we of course can create a “minter” badge that the component can hold in its vault, then create a proof to show that it is allowed to mint and burn
AccessRule we set up for minting and burning permissions. Let’s talk about that. When it comes to thinking about minting permissions we want the component to autonomously mint and burn the pool_units as liquidity providers deposit and redeem liquidity from the liquidity pool. So the component needs to have the permission to do this and as we’ve learned throughout this chapter, we of course can create a “minter” badge that the component can hold in its vault, then create a proof to show that it is allowed to mint and burn
pool_units as liquidity providers are depositing or redeeming liquidity. There’s nothing wrong with that approach specifically, but there’s actually a more elegant way to provide your component certain permissions without the need to create a dedicated badge for them. We didn’t get a chance to introduce them properly but these are called “virtual badges” and it looks something like this when we implement them for components:use scrypto::prelude::*;#[blueprint]mod radiswap { struct Radiswap { vault_a: FungibleVault, vault_b: FungibleVault, pool_units_resource_manager: ResourceManager, fee: Decimal, } impl Radiswap { pub fn instantiate_radiswap() -> Global<Radiswap> { let (address_reservation, component_address) = Runtime::allocate_component_address(Radiswap::blueprint_id()); let pool_units = ResourceBuilder::new_fungible(OwnerRole::None) .metadata(metadata!( init { "name" => "Pool Units", locked; } )) .mint_roles(mint_roles!( minter => rule!(require(global_caller(component_address))); minter_updater => rule!(deny_all); )) .burn_roles(burn_roles!( burner => rule!(require(global_caller(component_address))); burner_updater => rule!(deny_all); )) .create_with_no_initial_supply; Self { } .instantiate() .prepare_to_globalize(OwnerRole::None) .with_address(address_reservation) .globalize() } }}Line 15-16: Creating an “address reservation” and a
Line 25 & 29: Passing in
ComponentAddress ahead of time.Line 25 & 29: Passing in
global_caller(component_address) as a condition for the role’s AccessRule. This specifies that the component itself will be assigned as the minter and the burner for this resource.While we didn’t get a chance to properly introduce this concept in this chapter, the concept is quite simple and is an extension of the badge concept. Practically speaking, we are assigning the component itself to be the minter and burner role. This means that the component itself is allowed to mint or burn supply of this resource without the need for us to create a badge specifically for the component to hold. This is because the component has the ability to create its own “virtual badge” to present as proof that it is the minter and burner of this resource when we specified its ComponentAddress as a condition to the corresponding AccessRule.
//Defining our Component Instantiation Function
Now that we have a resource to represent liquidity providers and resource auth configured, let’s briefly switch our attention to the instantiation function. Here, the context we want to think about is how we imagine the component state will be when our component is instantiated. We have the vaults to represent the two sided liquidity in our liquidity pool, but we haven’t yet defined how those vault will be populated.
In this DEX, we want the component to be instantiated with an already existing supply of liquidity so that the component is ready to go from the get-go, so we want our liquidity pool to be instantiated with a supply of liquidity and the fee the liquidity pool will charge for each swap.
In this DEX, we want the component to be instantiated with an already existing supply of liquidity so that the component is ready to go from the get-go, so we want our liquidity pool to be instantiated with a supply of liquidity and the fee the liquidity pool will charge for each swap.
use scrypto::prelude::*;#[blueprint]mod radiswap { struct Radiswap { vault_a: FungibleVault, vault_b: FungibleVault, pool_units_resource_manager: ResourceManager, fee: Decimal, } impl Radiswap { pub fn instantiate_radiswap( bucket_a: FungibleBucket, bucket_b: FungibleBucket, fee: Decimal, ) -> Global<Radiswap> { .. } }}Line 14-16: Here, we require the person instantiating the component to first deposit the supplies of tokens in a bucket.Line 7: We also have a way for the instantiator to define the fee they would like to apply for each swap to incentivize more liquidity providers to join in and supply more tokens.
We almost have the complete picture of how our component will look like when it’s instantiated. Now, we need to complete how the tokens will be deposited and the values of the data(s) the component will maintain.
use scrypto::prelude::*;#[blueprint]mod radiswap { struct Radiswap { vault_a: FungibleVault, vault_b: FungibleVault, pool_units: ResourceManager, fee: Decimal, } impl Radiswap { pub fn instantiate_radiswap( bucket_a: FungibleBucket, bucket_b: FungibleBucket, fee: Decimal ) -> Global<Radiswap> { let (address_reservation, component_address) = Runtime::allocate_component_address(Radiswap::blueprint_id()); let pool_units = ResourceBuilder::new_fungible(OwnerRole::None) .metadata(metadata!( init { "name" => "Pool Units", locked; } )) .mint_roles(mint_roles!( minter => rule!(require(global_caller(component_address))); minter_updater => rule!(deny_all); )) .burn_roles(burn_roles!( burner => rule!(require(global_caller(component_address))); burner_updater => rule!(deny_all); )) .mint_initial_supply(100); let radiswap = Self { vault_a: FungibleVault::with_bucket(bucket_a), vault_b: FungibleVault::with_bucket(bucket_b), pool_units_resource_manager: pool_units.resource_manager(), fee: fee, } .instantiate() .prepare_to_globalize(OwnerRole::None) .with_address(address_reservation) .globalize() } }}With the expectation of fees defined and liquidity passed as part of the instantiation of this liquidity pool, we can populate the expected initial state of our component with these values. Additionally, you’ll notice that I changed the resource definition of the pool_units to have an initial supply of 100 when the component is instantiated. This is because as the component instantiator passed liquidity to the pool, they’ll be the first liquidity provider, therefore, representing 100% ownership of the pool.
Completing the Component Instantiation
We’re nearly close to completing how we want our component instantiation to look like. There are just a few more finishing touches here that we learned up until now that we want to include and commentary will be provided.
use scrypto::prelude::*;#[blueprint]mod radiswap { struct Radiswap { vault_a: FungibleVault, vault_b: FungibleVault, pool_units_resource_manager: ResourceManager, fee: Decimal, } impl Radiswap { pub fn instantiate_radiswap( bucket_a: FungibleBucket, bucket_b: FungibleBucket, fee: Decimal, ) -> (Global<Radiswap>, FungibleBucket) { assert!( !bucket_a.is_empty() && !bucket_b.is_empty(), "You must pass in an initial supply of each token" ); assert!( fee >= dec!("0") && fee <= dec!("1"), "Invalid fee in thousandths" ); let (address_reservation, component_address) = Runtime::allocate_component_address(Radiswap::blueprint_id()); let pool_units: FungibleBucket = ResourceBuilder::new_fungible(OwnerRole::None) .metadata(metadata!( init { "name" => "Pool Units", locked; } )) .mint_roles(mint_roles!( minter => rule!(require(global_caller(component_address))); minter_updater => rule!(deny_all); )) .burn_roles(burn_roles!( burner => rule!(require(global_caller(component_address))); burner_updater => rule!(deny_all); )) .mint_initial_supply(100); let radiswap = Self { vault_a: FungibleVault::with_bucket(bucket_a), vault_b: FungibleVault::with_bucket(bucket_b), pool_units_resource_manager: pool_units.resource_manager(), fee: fee, } .instantiate() .prepare_to_globalize(OwnerRole::None) .with_address(address_reservation) .globalize(); (radiswap, pool_units) } }}Line 17: We’ve modified the return type to not only include the strongly typed
Line 54: We talked about entity owners in this chapter. In a future iteration of this walkthrough example, we’ll include practical implementation of entity owners for this blueprint.
Line 55: We pass the
Line 58: We then return the globalized Radiswap component as well as the initial
ComponentAddress of the Radiswap component, but also the bucket which contains the pool_units to the initial liquidity provider.Line 19-26: There are also convenient assertions we’ve added to ensure that the arguments passed in are within the bounds of our expectations.Line 54: We talked about entity owners in this chapter. In a future iteration of this walkthrough example, we’ll include practical implementation of entity owners for this blueprint.
Line 55: We pass the
address_reservation we created earlier. Simply, this is to solve the chicken and egg problem when a ComponentAddress is given to a globalized component.Line 58: We then return the globalized Radiswap component as well as the initial
pool_units.With the completed instantiation function, we can expect a simple, yet clean instantiation of our component with its initial state. When someone wants instantiate Radiswap with a new liquidity pool, they’re expected to be the first supplier of liquidity and define the fee that the pool will charge. The resource,
pool_units, which is expected to track the ownership of the pool is created and minted with an initial supply to be returned to the first liquidity provider. This instantiated component will represent a basic, yet functional liquidity pool. We just now need to be able to allow users to swap tokens within the pool!//Defining Methods
At this point, our DEX is looking quite good! It now has a liquidity pool, two vaults in which to deposit and categorize the supply of tokens, and a way to represent liquidity providers. Now it’s just to create methods to interact with our component when it’s instantiated.
//Performing a Swap
The most critical feature of a DEX is of course allow the ability for the user to swap token A for token B, or vice versa. If you’re at all new to coding, this can look daunting as you don’t have the muscles yet to imagine, design, and write logic for a feature. It’s never a bad idea to start with small steps as we start to see the contours of what’s ahead:
pub fn swap(&mut self, input_tokens: Bucket) -> Bucket { ..}Line 1: Yep, this is just one simple line. A method that we call swap. The first argument will mutate self since we need to interact with our component’s state. The second argument,
input_tokens will take a bucket. We can imagine that when a user performs a swap, they’ll first deposit the tokens they’d like to swap in for something else. Finally, it’ll return a bucket, which will be the output tokens that the user will swap for.Now that we know that tokens will be going in an out of this method, let’s start to imagine how these tokens will move around in our method logic. We know that the
input_tokens passed in will be deposited into one of two vaults, but how can we tell where it will be deposited to? And how do we know which tokens will be taken out of which vault when we return the bucket of tokens? To do that, it’s a good exercise to organize this problem to make it easier for us to manage the tokens. We’ll create an if-else statement to represent this.pub fn swap(&mut self, input_tokens: FungibleBucket) -> FungibleBucket { // Getting the vault corresponding to the input tokens and the vault // corresponding to the output tokens based on what the input is. let (input_tokens_vault, output_tokens_vault): (&mut FungibleVault, &mut FungibleVault) = if input_tokens.resource_address() == self.vault_a.resource_address() { (&mut self.vault_a, &mut self.vault_b) } else if input_tokens.resource_address() == self.vault_b.resource_address() { (&mut self.vault_b, &mut self.vault_a) } else { panic!( "The given input tokens do not belong to this liquidity pool" ) };}Line 4: We’ve created two variables called
Line 8-9: This follows the same line of logic as line 5-6, but reversed.
Line 11-15: Finally, if the
Now we have a logic that automatically organizes the tokens so we know what we’re dealing with when we’re moving them around.
input_tokens_vault to represent the input_tokens that will be deposited and output_tokens_vault to represent the tokens that we will return. These will be mutable vaults since we need to interact with them. Line 5-6: All we’re saying here is that if the ResourceAddress of the input_tokens are the same as the ResourceAddress of the vault_a our component holds, then the input_tokens_vault will be vault_a and therefore, output_tokens_vault must correspond with vault_b.Line 8-9: This follows the same line of logic as line 5-6, but reversed.
Line 11-15: Finally, if the
input_tokens don’t match either of vault_a or vault_b then we will give an error message to the user.Now we have a logic that automatically organizes the tokens so we know what we’re dealing with when we’re moving them around.
//Applying the Constant Product Market Maker
Remember the equation
dy = (y * r * dx) / (x + r * dx) we concluded to determine how much of the tokens we should return to the user? We can now see it in practice.pub fn swap(&mut self, input_tokens: FungibleBucket) -> FungibleBucket { // Getting the vault corresponding to the input tokens and the vault // corresponding to the output tokens based on what the input is. let (input_tokens_vault, output_tokens_vault): (&mut FungibleVault, &mut FungibleVault) = if input_tokens.resource_address() == self.vault_a.resource_address() { (&mut self.vault_a, &mut self.vault_b) } else if input_tokens.resource_address() == self.vault_b.resource_address() { (&mut self.vault_b, &mut self.vault_a) } else { panic!( "The given input tokens do not belong to this liquidity pool" ) }; // Calculate the output amount of tokens based on the input amount // and the pool fees let output_amount: Decimal = (output_tokens_vault.amount() * (dec!("1") - self.fee) * input_tokens.amount()) / (input_tokens_vault.amount() + input_tokens.amount() * (dec!("1") - self.fee));}- dy = output_amount
- dx = input_tokens.amount()
- x = input_tokens_vault.amount()
- y = output_tokens_vault.amount()
- r = (dec!("1") - self.fee)
Look somewhat familiar? I admit, I finessed the equation to make it fit with what I need, but in the end, it means the same thing.
//Full Implementation of the Swap Method
Finally, here’s how our swap method looks like with some finishing touches.
pub fn swap(&mut self, input_tokens: FungibleBucket) -> FungibleBucket { // Getting the vault corresponding to the input tokens and the vault // corresponding to the output tokens based on what the input is. let (input_tokens_vault, output_tokens_vault): (&mut FungibleVault, &mut FungibleVault) = if input_tokens.resource_address() == self.vault_a.resource_address() { (&mut self.vault_a, &mut self.vault_b) } else if input_tokens.resource_address() == self.vault_b.resource_address() { (&mut self.vault_b, &mut self.vault_a) } else { panic!( "The given input tokens do not belong to this liquidity pool" ) }; // Calculate the output amount of tokens based on the input amount // and the pool fees let output_amount: Decimal = (output_tokens_vault.amount() * (dec!("1") - self.fee) * input_tokens.amount()) / (input_tokens_vault.amount() + input_tokens.amount() * (dec!("1") - self.fee)); // Perform the swapping operation input_tokens_vault.put(input_tokens); output_tokens_vault.take(output_amount)}Line 26: Because in line 4-15 where we organized how the tokens will be sorted, we now can confidently deposit the
input_tokens to the correct vault.Line 27: We also know which vault we can take from to return to the user. Likewise, based on the calculation of the equation we set up in line 19-23, we can determine how much we can take from this vault and return to the user.//Adding Liquidity
Now thinking through this method may seem daunting, but we’ll apply the same strategy we did before by compartmentalizing. Let’s start as we always do by thinking about inputs and outputs of our method.
pub fn add_liquidity( &mut self, bucket_a: FungibleBucket, bucket_b: FungibleBucket,) -> (FungibleBucket, FungibleBucket, FungibleBucket) { ..}Line 2: Since this method will interact with the component’s state, we need make self mutable as part of the argument.Line 3-4: We’ll need a bucket to contain the supply of tokens for each vault in our pool.
Line 5: Thinking ahead, we’ll send 3 bucket back to the liquidity provider. You’d be right to wonder why I’m returning 3 different bucket and I only know that because I’ve already seen the complete code, but more clarity on this will be shed as we walk through this process.
Line 5: Thinking ahead, we’ll send 3 bucket back to the liquidity provider. You’d be right to wonder why I’m returning 3 different bucket and I only know that because I’ve already seen the complete code, but more clarity on this will be shed as we walk through this process.
Of course, while we named our bucket variables
bucket_a and bucket_b, we can’t always expect the liquidity providers to send us in that order - or even the correct tokens. So similar to before, we should organize the buckets so we know what we’re dealing with.pub fn add_liquidity( &mut self, bucket_a: FungibleBucket, bucket_b: FungibleBucket,) -> (FungibleBucket, FungibleBucket, FungibleBucket) { let (mut bucket_a, mut bucket_b): (FungibleBucket, FungibleBucket) = if bucket_a.resource_address() == self.vault_a.resource_address() && bucket_b.resource_address() == self.vault_b.resource_address() { (bucket_a, bucket_b) } else if bucket_a.resource_address() == self.vault_b.resource_address() && bucket_b.resource_address() == self.vault_a.resource_address() { (bucket_b, bucket_a) } else { panic!("One of the tokens does not belong to the pool!") }; }This should look similar because we created this type of logic in our swap method, except, it’s now with bucket instead of vault. With this logic, we can now rely that bucket_a corresponds with
vault_a and bucket_b corresponds with vault_b; if the tokens sent in are incorrect, they will receive an error message.//Calculating the Amount to Deposit
One would think we should simply just deposit the tokens to each corresponding vault, but we are slapped with the fact that liquidity pools are a bit more complicated than that. If you remember the Constant Product Market Maker formula:
x * y = k. Where x and y were the liquidity of each tokens, then the liquidity of x and y must remain constant to stabilize the value of k so that the pricing formula remains consistent. Said another way, we must keep x and y proportional to each other. So before we deposit the bucket of tokens, we should do some calculation to ensure the ratio of each tokens.pub fn add_liquidity( &mut self, bucket_a: FungibleBucket, bucket_b: FungibleBucket,) -> (FungibleBucket, FungibleBucket, FungibleBucket) { // Give the buckets the same names as the vaults let (mut bucket_a, mut bucket_b): (FungibleBucket, FungibleBucket) = if bucket_a.resource_address() == self.vault_a.resource_address() && bucket_b.resource_address() == self.vault_b.resource_address() { (bucket_a, bucket_b) } else if bucket_a.resource_address() == self.vault_b.resource_address() && bucket_b.resource_address() == self.vault_a.resource_address() { (bucket_b, bucket_a) } else { panic!("One of the tokens does not belong to the pool!") }; // Getting the values of `dm` and `dn` based on the sorted buckets let dm: Decimal = bucket_a.amount(); let dn: Decimal = bucket_b.amount(); // Getting the values of m and n from the liquidity pool vaults let m: Decimal = self.vault_a.amount(); let n: Decimal = self.vault_b.amount(); // Calculate the amount of tokens which will be added to each one of //the vaults let (amount_a, amount_b): (Decimal, Decimal) = if ((m == Decimal::zero()) | (n == Decimal::zero())) | ((m / n) == (dm / dn)) { // Case 1 (dm, dn) } else if (m / n) < (dm / dn) { // Case 2 (dn * m / n, dn) } else { // Case 3 (dm, dm * n / m) }; // Depositing the amount of tokens calculated into the liquidity pool self.vault_a.put(bucket_a.take(amount_a)); self.vault_b.put(bucket_b.take(amount_b));}Line 22-23: Now this is starting to become an algebraic exercise we did back in high school. We’re defining our variables to keep things neat and organized.Line 26-27: We’re creating a variable that will hold the value of the amount of tokens we are meant to deposit in each vault.
Line 32-33: This logic essentially says that if either vault has no liquidity or that the ratio of liquidity between the liquidity deposited and the current liquidity of the vaults are the same, then we will deposit the bucket of tokens as is.
Line 37-39: This states that if the ratio of the tokens held in each vault is less than the ratio of the tokens in each bucket that was passed, then we will modify
Line 41-42: Otherwise, we will modify
Line 46-47: Now that we have the correct amount we need to deposit in each vault to keep k constant, we will only take the amount we need from each bucket.
Line 32-33: This logic essentially says that if either vault has no liquidity or that the ratio of liquidity between the liquidity deposited and the current liquidity of the vaults are the same, then we will deposit the bucket of tokens as is.
Line 37-39: This states that if the ratio of the tokens held in each vault is less than the ratio of the tokens in each bucket that was passed, then we will modify
bucket_a such that the ratio remains constant.Line 41-42: Otherwise, we will modify
bucket_b such that the ratio between token A and token B in our vault remains constant.Line 46-47: Now that we have the correct amount we need to deposit in each vault to keep k constant, we will only take the amount we need from each bucket.
//Minting Liquidity Provider Tokens
At this point, we now need to mint the correct amount
pool_units to the liquidity provider that correctly represents their ownership of the pool. Just a bit more math here:pub fn add_liquidity( &mut self, bucket_a: FungibleBucket, bucket_b: FungibleBucket,) -> (FungibleBucket, FungibleBucket, FungibleBucket) { // Give the buckets the same names as the vaults let (mut bucket_a, mut bucket_b): (FungibleBucket, FungibleBucket) = if bucket_a.resource_address() == self.vault_a.resource_address() && bucket_b.resource_address() == self.vault_b.resource_address() { (bucket_a, bucket_b) } else if bucket_a.resource_address() == self.vault_b.resource_address() && bucket_b.resource_address() == self.vault_a.resource_address() { (bucket_b, bucket_a) } else { panic!("One of the tokens does not belong to the pool!") }; // Getting the values of `dm` and `dn` based on the sorted buckets let dm: Decimal = bucket_a.amount(); let dn: Decimal = bucket_b.amount(); // Getting the values of m and n from the liquidity pool vaults let m: Decimal = self.vault_a.amount(); let n: Decimal = self.vault_b.amount(); // Calculate the amount of tokens which will be added to each one of //the vaults let (amount_a, amount_b): (Decimal, Decimal) = if ((m == Decimal::zero()) | (n == Decimal::zero())) | ((m / n) == (dm / dn)) { // Case 1 (dm, dn) } else if (m / n) < (dm / dn) { // Case 2 (dn * m / n, dn) } else { // Case 3 (dm, dm * n / m) }; // Depositing the amount of tokens calculated into the liquidity pool self.vault_a.put(bucket_a.take(amount_a)); self.vault_b.put(bucket_b.take(amount_b)); // Mint pool units tokens to the liquidity provider let pool_units_amount: Decimal = if self.pool_units_resource_manager.total_supply().unwrap() == Decimal::zero() { dec!("100.00") } else { amount_a * self.pool_units_resource_manager.total_supply().unwrap() / m }; let pool_units: FungibleBucket = self.pool_units_resource_manager.mint(pool_units_amount).as_fungible(); }The highlighted portion here is simply a calculation how many
pool_units we need to mint to represent the ownership of the pool. Of course, if the liquidity pool is empty, then this new supply of liquidity represents 100% ownership of the pool. Otherwise, we will calculate the proportion of the ownership relative to the existing supply of liquidity within the pool.Note that we didn’t really specify any special method to authorize the minting of these
pool_units. This is because when it comes to using the component itself as the minter and burner role, the proof generation to provide auth for such minting and burning is automatically handled by the Radix Engine.//Full Implementation of the Adding Liquidity Method
The full implementation of the
All of these logic to order the buckets
add_liquidity method is below and can look daunting with all the calculations we implemented. It can take a bit to absorb the logic we implemented here, but a simple commentary to help you contextualize this is that we’re simply ordering the liquidity that the component is provided when bucket_a and bucket_b is passed in. All of these logic to order the buckets
pub fn add_liquidity( &mut self, bucket_a: FungibleBucket, bucket_b: FungibleBucket,) -> (FungibleBucket, FungibleBucket, FungibleBucket) { // Give the buckets the same names as the vaults let (mut bucket_a, mut bucket_b): (FungibleBucket, FungibleBucket) = if bucket_a.resource_address() == self.vault_a.resource_address() && bucket_b.resource_address() == self.vault_b.resource_address() { (bucket_a, bucket_b) } else if bucket_a.resource_address() == self.vault_b.resource_address() && bucket_b.resource_address() == self.vault_a.resource_address() { (bucket_b, bucket_a) } else { panic!("One of the tokens does not belong to the pool!") }; // Getting the values of `dm` and `dn` based on the sorted buckets let dm: Decimal = bucket_a.amount(); let dn: Decimal = bucket_b.amount(); // Getting the values of m and n from the liquidity pool vaults let m: Decimal = self.vault_a.amount(); let n: Decimal = self.vault_b.amount(); // Calculate the amount of tokens which will be added to each one of //the vaults let (amount_a, amount_b): (Decimal, Decimal) = if ((m == Decimal::zero()) | (n == Decimal::zero())) | ((m / n) == (dm / dn)) { // Case 1 (dm, dn) } else if (m / n) < (dm / dn) { // Case 2 (dn * m / n, dn) } else { // Case 3 (dm, dm * n / m) }; // Depositing the amount of tokens calculated into the liquidity pool self.vault_a.put(bucket_a.take(amount_a)); self.vault_b.put(bucket_b.take(amount_b)); // Mint pool units tokens to the liquidity provider let pool_units_amount: Decimal = if self.pool_units_resource_manager.total_supply().unwrap() == Decimal::zero() { dec!("100.00") } else { amount_a * self.pool_units_resource_manager.total_supply().unwrap() / m }; let pool_units: FungibleBucket = self.pool_units_resource_manager.mint(pool_units_amount).as_fungible(); // Return the remaining tokens to the caller as well as the pool units // tokens (bucket_a, bucket_b, pool_units)}//Removing Liquidity
For this method, we can perform the same exercise we did for the swap method. Let’s start with the first step and think about what our method will require to return the tokens owed to the liquidity provider.
pub fn remove_liquidity(&mut self, pool_units: FungibleBucket) -> (FungibleBucket, FungibleBucket) { ..}Line 1: Since this method will interact with the component’s state, we need make self mutable as part of the argument. Secondly, let’s think about what the liquidity provider will need to withdraw their supply of tokens. We provided them the
pool_units token to represent the ownership of the liquidity pool; and the pool_units can act like a receipt that the liquidity provider sends. Lastly, we want to send back the liquidity provider two buckets of each token they supplied.The inputs and outputs mental model that we’ve been applying has been useful in helping us consider how the method logic should look like. So with inputs, which should be the
pool_units we expect. How do we really know that the user is sending us the component’s pool_units? We learned in Chapter 4 that we can create assertions to guarantee a scenario, so let’s see what that looks like:pub fn remove_liquidity(&mut self, pool_units: FungibleBucket) -> (FungibleBucket, FungibleBucket) { assert!( pool_units.resource_address() == self.pool_units_resource_manager.address(), "Wrong token type passed in" );}In the body of our method, we want to have this assertion to compare the
ResourceAddress of the resources contained in the bucket (which we named pool_units) that was sent in against the ResourceAddress of the pool_units we defined at component instantiation.//Calculating the Tokens Owed
Now that we can guarantee that the tokens sent in is the same
ResourceAddress as our pool_units we can begin calculating how much of each tokens in the liquidity pool we owe to the liquidity provider and this is how it looks like.pub fn remove_liquidity(&mut self, pool_units: FungibleBucket) -> (FungibleBucket, FungibleBucket) { assert!( pool_units.resource_address() == self.pool_units_resource_manager.address(), "Wrong token type passed in" ); // Calculate the share based on the input LP tokens. let share = pool_units.amount() / self.pool_units_resource_manager.total_supply().unwrap(); // Burn the LP tokens received pool_units.burn(); // Return the withdrawn tokens ( self.vault_a.take(self.vault_a.amount() * share), self.vault_b.take(self.vault_b.amount() * share), )}Line 9-10: Calculating the share the liquidity provider has.Line 13: After we’ve calculated the ownership share, we can then burn the LP tokens.
Line 17-18: Redeeming the liquidity owed by multiplying current liquidity amount by the ownership share percentage.
Line 17-18: Redeeming the liquidity owed by multiplying current liquidity amount by the ownership share percentage.
//Full Implementation of the Remove Liquidity Method
pub fn remove_liquidity(&mut self, pool_units: FungibleBucket) -> (FungibleBucket, FungibleBucket) { assert!( pool_units.resource_address() == self.pool_units_resource_manager.address(), "Wrong token type passed in" ); // Calculate the share based on the input LP tokens. let share = pool_units.amount() / self.pool_units_resource_manager.total_supply().unwrap(); // Burn the LP tokens received pool_units.burn(); // Return the withdrawn tokens ( self.vault_a.take(self.vault_a.amount() * share), self.vault_b.take(self.vault_b.amount() * share), )}Here, we do not need to add () enclosures, but can be a good habit to visually organize your code.
//Full Implementation of Radiswap
We now have every piece of our DEX.
use scrypto::prelude::*;#[blueprint]mod radiswap { struct Radiswap { /// A vault containing pool reverses of reserves of token A. vault_a: FungibleVault, /// A vault containing pool reverses of reserves of token B. vault_b: FungibleVault, /// The token address of a token representing pool units in this pool pool_units_resource_manager: ResourceManager, /// The amount of fees imposed by the pool on swaps where 0 <= fee <= 1. fee: Decimal, } impl Radiswap { /// Creates a new liquidity pool of the two tokens sent to the pool pub fn instantiate_radiswap( bucket_a: FungibleBucket, bucket_b: FungibleBucket, fee: Decimal, ) -> (Global<Radiswap>, FungibleBucket) { // Ensure that none of the buckets are empty and that an appropriate // fee is set. assert!( !bucket_a.is_empty() && !bucket_b.is_empty(), "You must pass in an initial supply of each token" ); assert!( fee >= dec!("0") && fee <= dec!("1"), "Invalid fee in thousandths" ); let (address_reservation, component_address) = Runtime::allocate_component_address(Radiswap::blueprint_id()); // Create the pool units token along with the initial supply specified // by the user. let pool_units: FungibleBucket = ResourceBuilder::new_fungible(OwnerRole::None) .metadata(metadata!( init { "name" => "Pool Units", locked; } )) .mint_roles(mint_roles!( minter => rule!(require(global_caller(component_address))); minter_updater => rule!(deny_all); )) .burn_roles(burn_roles!( burner => rule!(require(global_caller(component_address))); burner_updater => rule!(deny_all); )) .mint_initial_supply(100); // Create the Radiswap component and globalize it let radiswap = Self { vault_a: FungibleVault::with_bucket(bucket_a), vault_b: FungibleVault::with_bucket(bucket_b), pool_units_resource_manager: pool_units.resource_manager(), fee: fee, } .instantiate() .prepare_to_globalize(OwnerRole::None) .with_address(address_reservation) .globalize(); // Return the component address as well as the pool units tokens (radiswap, pool_units) } /// Swaps token A for B, or vice versa. pub fn swap(&mut self, input_tokens: FungibleBucket) -> FungibleBucket { // Getting the vault corresponding to the input tokens and the vault // corresponding to the output tokens based on what the input is. let (input_tokens_vault, output_tokens_vault): (&mut FungibleVault, &mut FungibleVault) = if input_tokens.resource_address() == self.vault_a.resource_address() { (&mut self.vault_a, &mut self.vault_b) } else if input_tokens.resource_address() == self.vault_b.resource_address() { (&mut self.vault_b, &mut self.vault_a) } else { panic!( "The given input tokens do not belong to this liquidity pool" ) }; // Calculate the output amount of tokens based on the input amount // and the pool fees let output_amount: Decimal = (output_tokens_vault.amount() * (dec!("1") - self.fee) * input_tokens.amount()) / (input_tokens_vault.amount() + input_tokens.amount() * (dec!("1") - self.fee)); // Perform the swapping operation input_tokens_vault.put(input_tokens); output_tokens_vault.take(output_amount) } /// Adds liquidity to the liquidity pool pub fn add_liquidity( &mut self, bucket_a: FungibleBucket, bucket_b: FungibleBucket, ) -> (FungibleBucket, FungibleBucket, FungibleBucket) { // Give the buckets the same names as the vaults let (mut bucket_a, mut bucket_b): (FungibleBucket, FungibleBucket) = if bucket_a.resource_address() == self.vault_a.resource_address() && bucket_b.resource_address() == self.vault_b.resource_address() { (bucket_a, bucket_b) } else if bucket_a.resource_address() == self.vault_b.resource_address() && bucket_b.resource_address() == self.vault_a.resource_address() { (bucket_b, bucket_a) } else { panic!("One of the tokens does not belong to the pool!") }; // Getting the values of `dm` and `dn` based on the sorted buckets let dm: Decimal = bucket_a.amount(); let dn: Decimal = bucket_b.amount(); // Getting the values of m and n from the liquidity pool vaults let m: Decimal = self.vault_a.amount(); let n: Decimal = self.vault_b.amount(); // Calculate the amount of tokens which will be added to each one of //the vaults let (amount_a, amount_b): (Decimal, Decimal) = if ((m == Decimal::zero()) | (n == Decimal::zero())) | ((m / n) == (dm / dn)) { // Case 1 (dm, dn) } else if (m / n) < (dm / dn) { // Case 2 (dn * m / n, dn) } else { // Case 3 (dm, dm * n / m) }; // Depositing the amount of tokens calculated into the liquidity pool self.vault_a.put(bucket_a.take(amount_a)); self.vault_b.put(bucket_b.take(amount_b)); // Mint pool units tokens to the liquidity provider let pool_units_amount: Decimal = if self.pool_units_resource_manager.total_supply().unwrap() == Decimal::zero() { dec!("100.00") } else { amount_a * self.pool_units_resource_manager.total_supply().unwrap() / m }; let pool_units: FungibleBucket = self.pool_units_resource_manager.mint(pool_units_amount).as_fungible(); // Return the remaining tokens to the caller as well as the pool units // tokens (bucket_a, bucket_b, pool_units) } /// Removes the amount of funds from the pool corresponding to the pool units. pub fn remove_liquidity(&mut self, pool_units: FungibleBucket) -> (FungibleBucket, FungibleBucket) { assert!( pool_units.resource_address() == self.pool_units_resource_manager.address(), "Wrong token type passed in" ); // Calculate the share based on the input LP tokens. let share = pool_units.amount() / self.pool_units_resource_manager.total_supply().unwrap(); // Burn the LP tokens received pool_units.burn(); // Return the withdrawn tokens ( self.vault_a.take(self.vault_a.amount() * share), self.vault_b.take(self.vault_b.amount() * share), ) } }}