DEX tutorial
This tutorial utilizes the predeployed DEX smart contract to swap the ERC20 tokens of the predeployed Token smart contracts, which we instantiate with the help of the ADDRESS utility.
This example introduces the use of Acala EVM+ predeployed DEX that is present on every network at a fixed address (the address of a predeployed contract is the same on a local development network, public test network as well as the production network). As this example focuses on showcasing the interactions with the predeployed
DEX
, it doesn't have its own smart contract. We will get all of the required imports from the @acala-network/contracts
dependency. The precompiles and predeploys are a specific feature of the Acala EVM+, so this tutorial is no longer compatible with traditional EVM development networks (like Ganache).Let's take a look!
NOTE: You can refer to the complete code of this tutorial https://github.com/AcalaNetwork/truffle-tutorials/tree/master/DEX
The smart contract in this tutorial is only used to satisfy the Truffle's requirement to have a smart contract to compile. For this, we will create an empty smart contract that will inherit the
DEX
from @acala-network/contracts
dependency. Your skeleton smart contract should look like this:// SPDX-License-Identifier: MIT
pragma solidity =0.8.9;
contract PrecompiledDEX {
}
Import of the
DEX
from @acala-network/contracts
is done between the pragma
definition and the start od the contract
block:import "@acala-network/contracts/dex/DEX.sol";
As we now have access to
DEX.sol
from @acala-network/contracts
, we can set the inheritance of our PrecompiledDEX
contract:contract PrecompiledDEX is DEX {
This concludes our
PrecompiledDEX
smart contract.NOTE: in order for Truffle to properly use the Token precompiles (used in the following section), we need to add the PrecompiledToken.sol smart contract as well.
Tests for this tutorial will validate the expected values returned and expected behaviour of DEX predeployed smart contract. The test file in our case is called
DEX.js
. Within it we import the DEX
and Token
from @acala-network/contracts
dependency and assign it to PrecompiledDEX
and PrecompiledToken
variables. The ACA
, AUSD
, LP_ACA_AUSD
, DOT
, RENBTC
and DEX
, which are the exports from the ADDRESS
utility of @acala-network/contracts
dependency, are imported and they hold the values of the addresses of the predeployed smart contracts. The MandalaAddress
utility holds the values of the predeployed smart contracts in the local development and Mandala network and we will be using this one. There are also AcalaNetwork
and KaruraNetwork
, that hold the addresses of their respective networks. We are also importing truffleAssert
and parseUnits
in order to ease our verification of the expected values and we are defining the NULL_ADDRESS
constant, so we don't have to copy-paste the value when needed.The test file with import statements and an empty test should look like this:
const PrecompiledDEX = artifacts.require("@acala-network/contracts/build/contracts/DEX");
const PrecompiledToken = artifacts.require("@acala-network/contracts/build/contracts/Token");
const truffleAssert = require("truffle-assertions");
const { parseUnits } = require("ethers/lib/utils");
const { ACA, AUSD, LP_ACA_AUSD, DOT, RENBTC, DEX } = require("@acala-network/contracts/utils/MandalaAddress");
const NULL_ADDRESS = "0x0000000000000000000000000000000000000000";
/*
* uncomment accounts to access the test accounts made available by the
* Ethereum client
* See docs: https://www.trufflesuite.com/docs/truffle/testing/writing-tests-in-javascript
*/
contract("PrecompiledDEX", function (accounts) {
});
To prepare for the testing, we have to define four global variables,
instance
, AUSDinstance
, ACAinstance
and deployer
. The instance
will store the predeployed DEX smart contract instance. AUSDinstance
and ACAinstance
will store the values of the token predeployed smart contracts. he deployer
will store the account that we will be using in our tests. Let's assign them values in the beforeEach
action: let instance;
let ACAinstance;
let AUSDinstance;
let deployer;
beforeEach("setup development environment", async function () {
deployer = accounts[0];
instance = await PrecompiledDEX.at(DEX);
ACAinstance = await PrecompiledToken.at(ACA);
AUSDinstance = await PrecompiledToken.at(AUSD);
});
You can see how we used the
DEX
, ACA
and AUSD
from the ADDRESS
utility in order to set the addresses of our predeployed smart contract.Our test will only contain one top-level section called
Operation
in which we will be checking the following functions (which will each be tested in its own section):- 1.
getLiquidityPool
function to get the liquidity pool of the desired pair. - 2.
getLiquidityTokenAddress
function to get the address of the liquidity token for the desired pair. - 3.
getSwapTargetAddress
function to get the informative amount of the swap egress token based on set supply of the ingress token. - 4.
getSwapSupplyAmount
function to get the informative amount of the swap supply based on the set target amount of egress token. - 5.
swapWithExactSupply
function to swap tokens based on the set supply of the ingress token. - 6.
swapWithExactTarget
function to swap tokens based on the set target of the egress token. - 7.
addLiquidity
function that adds liquidity of the desired pair. - 8.
removeLiquidity
function that removes liquidity of the desired pair.
The structure described above without the checks, should look like this:
describe("Operation", function () {
describe("getLiquidityPool", function () {
});
describe("getLiquidityTokenAddress", function () {
});
describe("getSwapTargetAddress", function () {
});
describe("getSwapSupplyAmount", function () {
});
describe("swapWithExactSupply", function () {
});
describe("swapWithExactTarget", function () {
});
describe("addLiquidity", function () {
});
describe("removeLiquidity", function () {
});
});
When validating the
getLiquidityPool
function, we will check for the following examples:- 1.
tokenA
should not be a0x0
address. - 2.
tokenB
should not be a0x0
address. - 3.Liquidity of the non-existent pair should be 0.
- 4.Liquidity should be returned for the existing pairs.
The section should look like this:
it("should not allow tokenA to be a 0x0 address", async function () {
await truffleAssert.reverts(
instance.getLiquidityPool(NULL_ADDRESS, ACA),
"DEX: tokenA is zero address"
);
});
it("should not allow tokenB to be a 0x0 address", async function () {
await truffleAssert.reverts(
instance.getLiquidityPool(ACA, NULL_ADDRESS),
"DEX: tokenB is zero address"
);
});
it("should return 0 liquidity for nonexistent pair", async function () {
const response = await instance.getLiquidityPool(ACA, DOT);
const liquidityA = response[0];
const liquidityB = response[1];
expect(liquidityA.isZero()).to.be.true;
expect(liquidityB.isZero()).to.be.true;
});
it("should return liquidity for existing pairs", async function () {
const response = await instance.getLiquidityPool(ACA, AUSD);
const liquidityA = response[0];
const liquidityB = response[1];
expect(liquidityA.gt(web3.utils.toBN("0"))).to.be.true;
expect(liquidityB.gt(web3.utils.toBN("0"))).to.be.true;
});
When validating the
getLiquidityTokenAddress
function, we will check for the following examples:- 1.
tokenA
should not be a0x0
address. - 2.
tokenB
should not be a0x0
address. - 3.Liquidity token address should be returned for the existing pairs.
The section should look like this:
it("should not allow tokenA to be a 0x0 address", async function () {
await truffleAssert.reverts(
instance.getLiquidityTokenAddress(NULL_ADDRESS, ACA),
"DEX: tokenA is zero address"
);
});
it("should not allow tokenB to be a 0x0 address", async function () {
await truffleAssert.reverts(
instance.getLiquidityTokenAddress(ACA, NULL_ADDRESS),
"DEX: tokenB is zero address"
);
});
it("should return liquidity token address for an existing pair", async function () {
const response = await instance.getLiquidityTokenAddress(ACA, AUSD);
expect(response).to.equal(LP_ACA_AUSD);
});
When validating the
getSwapTargetAddress
function, we will check for the following examples:- 1.
path
should not include the0x0
address. - 2.
supplyAmount
should not be 0. - 3.Getting swap target amount should return 0 for an incompatible path.
- 4.Swap target amount should be returned when all parameters are correct.
The section should look like this:
it("should not allow for the path to include a 0x0 address", async function () {
await truffleAssert.reverts(
instance.getSwapTargetAmount([NULL_ADDRESS, ACA, DOT, RENBTC], 12345678990),
"DEX: token is zero address"
);
await truffleAssert.reverts(
instance.getSwapTargetAmount([ACA, NULL_ADDRESS, DOT, RENBTC], 12345678990),
"DEX: token is zero address"
);
await truffleAssert.reverts(
instance.getSwapTargetAmount([ACA, DOT, NULL_ADDRESS, RENBTC], 12345678990),
"DEX: token is zero address"
);
await truffleAssert.reverts(
instance.getSwapTargetAmount([ACA, DOT, RENBTC, NULL_ADDRESS], 12345678990),
"DEX: token is zero address"
);
});
it("should not allow supplyAmount to be 0", async function () {
await truffleAssert.reverts(
instance.getSwapTargetAmount([ACA, AUSD], 0),
"DEX: supplyAmount is zero"
);
});
it("should return 0 for an incompatible path", async function () {
const expected_target = await instance.getSwapTargetAmount([ACA, DOT], 100);
expect(expected_target).to.deep.equal(web3.utils.toBN('0'));
});
it("should return a swap target amount", async function () {
const response = await instance.getSwapTargetAmount([ACA, AUSD], 100);
expect(response.gt(web3.utils.toBN("0"))).to.be.true;
});
When validating the
getSwapSupplyAmount
function, we will check for the following examples:- 1.
path
should not include the0x0
address. - 2.
targetAmount
should not be 0. - 3.Getting swap supply amount should return 0 for an incompatible path.
- 4.Swap supply amount should be returned when all parameters are correct.
The section should look like this:
it("should not allow an address in the path to be a 0x0 address", async function () {
await truffleAssert.reverts(
instance.getSwapSupplyAmount([NULL_ADDRESS, ACA, DOT, RENBTC], 12345678990),
"DEX: token is zero address"
);
await truffleAssert.reverts(
instance.getSwapSupplyAmount([ACA, NULL_ADDRESS, DOT, RENBTC], 12345678990),
"DEX: token is zero address"
);
await truffleAssert.reverts(
instance.getSwapSupplyAmount([ACA, DOT, NULL_ADDRESS, RENBTC], 12345678990),
"DEX: token is zero address"
);
await truffleAssert.reverts(
instance.getSwapSupplyAmount([ACA, DOT, RENBTC, NULL_ADDRESS], 12345678990),
"DEX: token is zero address"
);
});
it("should not allow targetAmount to be 0", async function () {
await truffleAssert.reverts(
instance.getSwapSupplyAmount([ACA, AUSD], 0),
"DEX: targetAmount is zero"
);
});
it("should return 0 for an incompatible path", async function () {
const expected_supply = await instance.getSwapSupplyAmount([ACA, DOT], 100);
expect(expected_supply).to.deep.equal(web3.utils.toBN('0'));
});
it("should return the supply amount", async function () {
const response = await instance.getSwapSupplyAmount([ACA, AUSD], 100);
expect(response.gt(web3.utils.toBN("0"))).to.be.true;
});
When validating the
swapWithExactSupply
function, we will check for the following examples:- 1.
path
should not include the0x0
address. - 2.
supplyAmount
should not be 0. - 3.Egress token balance of the caller should increase.
- 4.Successful execution should emit a
Swaped
event.
The section should look like this:
it("should not allow path to contain a 0x0 address", async function () {
await truffleAssert.reverts(
instance.swapWithExactSupply([NULL_ADDRESS, ACA, DOT, RENBTC], 12345678990, 1),
"DEX: token is zero address"
);
await truffleAssert.reverts(
instance.swapWithExactSupply([ACA, NULL_ADDRESS, DOT, RENBTC], 12345678990, 1),
"DEX: token is zero address"
);
await truffleAssert.reverts(
instance.swapWithExactSupply([ACA, DOT, NULL_ADDRESS, RENBTC], 12345678990, 1),
"DEX: token is zero address"
);
await truffleAssert.reverts(
instance.swapWithExactSupply([ACA, DOT, RENBTC, NULL_ADDRESS], 12345678990, 1),
"DEX: token is zero address"
);
});
it("should not allow supplyAmount to be 0", async function () {
await truffleAssert.reverts(
instance.swapWithExactSupply([ACA, AUSD], 0, 1),
"DEX: supplyAmount is zero"
);
});
it("should allocate the tokens to the caller", async function () {
const initalBalance = await ACAinstance.balanceOf(deployer);
const initBal = await AUSDinstance.balanceOf(deployer);
const path = [ACA, AUSD];
const expected_target = await instance.getSwapTargetAmount(path, 100);
await instance.swapWithExactSupply(path, 100, 1, { from: deployer });
const finalBalance = await ACAinstance.balanceOf(deployer);
const finBal = await AUSDinstance.balanceOf(deployer);
// The following assertion needs to check for the balance to be below the initialBalance - 100, because some of the ACA balance is used to pay for the transaction fee.
expect(finalBalance.lt(initalBalance.sub(web3.utils.toBN(100)))).to.be.true;
expect(finBal.eq(initBal.add(expected_target))).to.be.true;
});
it("should emit a Swaped event", async function () {
const path = [ACA, AUSD];
const expected_target = await instance.getSwapTargetAmount(path, 100);
const tx = await instance.swapWithExactSupply(path, 100, 1, { from: deployer });
const event = tx.logs[0].event;
const sender = tx.logs[0].args.sender;
const event_path = tx.logs[0].args.path;
const supplyAmount = tx.logs[0].args.supplyAmount;
const targetAmount = tx.logs[0].args.targetAmount;
expect(event).to.equal("Swaped");
expect(sender).to.equal(deployer);
expect(event_path).to.deep.equal(path);
expect(supplyAmount).to.deep.equal(web3.utils.toBN(100));
expect(targetAmount).to.deep.equal(expected_target);
});
When validating the
swapWithExactTarget
function, we will check for the following examples:- 1.
path
should not include the0x0
address. - 2.
targetAmount
should not be 0. - 3.Egress token balance of the caller should increase.
- 4.Successful execution should emit a
Swaped
event.
The section should look like this:
it("should not allow a token in a path to be a 0x0 address", async function () {
await truffleAssert.reverts(
instance.swapWithExactTarget([NULL_ADDRESS, ACA, DOT, RENBTC], 1, 12345678990),
"DEX: token is zero address"
);
await truffleAssert.reverts(
instance.swapWithExactTarget([ACA, NULL_ADDRESS, DOT, RENBTC], 1, 12345678990),
"DEX: token is zero address"
);
await truffleAssert.reverts(
instance.swapWithExactTarget([ACA, DOT, NULL_ADDRESS, RENBTC], 1, 12345678990),
"DEX: token is zero address"
);
await truffleAssert.reverts(
instance.swapWithExactTarget([ACA, DOT, RENBTC, NULL_ADDRESS], 1, 12345678990),
"DEX: token is zero address"
);
});
it("should not allow targetAmount to be 0", async function () {
await truffleAssert.reverts(
instance.swapWithExactTarget([ACA, AUSD], 0, 1234567890),
"DEX: targetAmount is zero"
);
});
it("should allocate tokens to the caller", async function () {
const initalBalance = await ACAinstance.balanceOf(deployer);
const initBal = await AUSDinstance.balanceOf(deployer);
const path = [ACA, AUSD];
const expected_supply = await instance.getSwapSupplyAmount(path, 100);
await instance.swapWithExactTarget(path, 100, 1234567890, { from: deployer });
const finalBalance = await ACAinstance.balanceOf(deployer);
const finBal = await AUSDinstance.balanceOf(deployer);
// The following assertion needs to check for the balance to be below the initialBalance - 100, because some of the ACA balance is used to pay for the transaction fee.
expect(finalBalance.lt(initalBalance.sub(expected_supply))).to.be.true;
expect(finBal.eq(initBal.add(web3.utils.toBN(100)))).to.be.true;
});
it("should emit Swaped event", async function () {
const path = [ACA, AUSD];
const expected_supply = await instance.getSwapSupplyAmount(path, 100);
const tx = await instance.swapWithExactTarget(path, 100, 1234567890, { from: deployer });
const event = tx.logs[0].event;
const sender = tx.logs[0].args.sender;
const event_path = tx.logs[0].args.path;
const supplyAmount = tx.logs[0].args.supplyAmount;
const targetAmount = tx.logs[0].args.targetAmount;
expect(event).to.equal("Swaped");
expect(sender).to.equal(deployer);
expect(event_path).to.deep.equal(path);
expect(supplyAmount).to.deep.equal(expected_supply);
expect(targetAmount).to.deep.equal(web3.utils.toBN(100));
});
When validating the
addLiquidity
function, we will check for the following examples:- 1.
tokenA
should not be a0x0
address. - 2.
tokenB
should not be a0x0
address. - 3.
maxAmountA
should not be 0. - 4.
maxAmountB
should not be 0. - 5.Successfull execution should increase the liquidity of the pair.
- 6.Successfull execution should emit
AddedLiquidity
event.
The section should look like this:
it("should not allow tokenA to be 0x0 address", async function () {
await truffleAssert.reverts(
instance.addLiquidity(NULL_ADDRESS, AUSD, 1000, 1000, 1),
"DEX: tokenA is zero address"
);
});
it("should not allow tokenB to be 0x0 address", async function () {
await truffleAssert.reverts(
instance.addLiquidity(ACA, NULL_ADDRESS, 1000, 1000, 1),
"DEX: tokenB is zero address"
);
});
it("should not allow maxAmountA to be 0", async function () {
await truffleAssert.reverts(
instance.addLiquidity(ACA, AUSD, 0, 1000, 1),
"DEX: maxAmountA is zero"
);
});
it("should not allow maxAmountB to be 0", async function () {
await truffleAssert.reverts(
instance.addLiquidity(ACA, AUSD, 1000, 0, 1),
"DEX: maxAmountB is zero"
);
});
it("should increase liquidity", async function () {
const intialLiquidity = await instance.getLiquidityPool(ACA, AUSD);
await instance.addLiquidity(ACA, AUSD, parseUnits("2", 12), parseUnits("2", 12), 1);
const finalLiquidity = await instance.getLiquidityPool(ACA, AUSD);
expect(finalLiquidity[0].gt(intialLiquidity[0])).to.be.true;
expect(finalLiquidity[1].gt(intialLiquidity[1])).to.be.true;
});
it("should emit AddedLiquidity event", async function () {
const tx = await instance.addLiquidity(ACA, AUSD, 1000, 1000, 1, { from: deployer });
const event = tx.logs[0].event;
const sender = tx.logs[0].args.sender;
const tokenA = tx.logs[0].args.tokenA;
const tokenB = tx.logs[0].args.tokenB;
const maxAmountA = tx.logs[0].args.maxAmountA;
const maxAmountB = tx.logs[0].args.maxAmountB;
expect(event).to.equal("AddedLiquidity");
expect(sender).to.equal(deployer);
expect(tokenA).to.deep.equal(ACA);
expect(tokenB).to.deep.equal(AUSD);
expect(maxAmountA).to.deep.equal(web3.utils.toBN(1000));
expect(maxAmountB).to.deep.equal(web3.utils.toBN(1000));
});
When validating the
removeLiquidity
function, we will check for the following examples:- 1.
tokenA
should not be a0x0
address. - 2.
tokenB
should not be a0x0
address. - 3.
removeShare
should not be 0. - 4.Successfull execution should reduce the liquidity of the pair.
- 5.Successfull execution should emit
RemovedLiquidity
event.
The section should look like this:
it("should not allow tokenA to be a 0x0 address", async function () {
await truffleAssert.reverts(
instance.removeLiquidity(NULL_ADDRESS, AUSD, 1, 0, 0),
"DEX: tokenA is zero address"
);
});
it("should not allow tokenB to be a 0x0 address", async function () {
await truffleAssert.reverts(
instance.removeLiquidity(ACA, NULL_ADDRESS, 1, 0, 0),
"DEX: tokenB is zero address"
);
});
it("should not allow removeShare to be 0", async function () {
await truffleAssert.reverts(
instance.removeLiquidity(ACA, AUSD, 0, 0, 0),
"DEX: removeShare is zero"
);
});
it("should reduce the liquidity", async function () {
const intialLiquidity = await instance.getLiquidityPool(ACA, AUSD);
await instance.removeLiquidity(ACA, AUSD, 10, 1, 1);
const finalLiquidity = await instance.getLiquidityPool(ACA, AUSD);
expect(intialLiquidity[0].gt(finalLiquidity[0])).to.be.true;
expect(intialLiquidity[1].gt(finalLiquidity[1])).to.be.true;
});
it("should emit RemovedLiquidity event", async function () {
const tx = await instance.removeLiquidity(ACA, AUSD, 1, 0, 0, { from: deployer });
const event = tx.logs[0].event;
const sender = tx.logs[0].args.sender;
const tokenA = tx.logs[0].args.tokenA;
const tokenB = tx.logs[0].args.tokenB;
const removeShare = tx.logs[0].args.removeShare;
expect(event).to.equal("RemovedLiquidity");
expect(sender).to.equal(deployer);
expect(tokenA).to.deep.equal(ACA);
expect(tokenB).to.deep.equal(AUSD);
expect(removeShare).to.deep.equal(web3.utils.toBN(1));
});
With that, our test is ready to be run.
const PrecompiledDEX = artifacts.require('@acala-network/contracts/build/contracts/DEX');
const PrecompiledToken = artifacts.require('@acala-network/contracts/build/contracts/Token');
const truffleAssert = require('truffle-assertions');
const { parseUnits } = require('ethers/lib/utils');
const { ACA, AUSD, LP_ACA_AUSD, DOT, RENBTC, DEX } = require('@acala-network/contracts/utils/MandalaAddress');
const NULL_ADDRESS = '0x0000000000000000000000000000000000000000';
/*
* uncomment accounts to access the test accounts made available by the
* Ethereum client
* See docs: https://www.trufflesuite.com/docs/truffle/testing/writing-tests-in-javascript
*/
contract('PrecompiledDEX', function (accounts) {
let instance;
let ACAinstance;
let AUSDinstance;
let deployer;
beforeEach('setup development environment', async function () {
deployer = accounts[0];
instance = await PrecompiledDEX.at(DEX);
ACAinstance = await PrecompiledToken.at(ACA);
AUSDinstance = await PrecompiledToken.at(AUSD);
});
describe('Operation', function () {
describe('getLiquidityPool', function () {
it('should not allow tokenA to be a 0x0 address', async function () {
await truffleAssert.reverts(instance.getLiquidityPool(NULL_ADDRESS, ACA), 'DEX: tokenA is zero address');
});
it('should not allow tokenB to be a 0x0 address', async function () {
await truffleAssert.reverts(instance.getLiquidityPool(ACA, NULL_ADDRESS), 'DEX: tokenB is zero address');
});
it('should return 0 liquidity for nonexistent pair', async function () {
const response = await instance.getLiquidityPool(ACA, DOT);
const liquidityA = response[0];
const liquidityB = response[1];
expect(liquidityA.isZero()).to.be.true;
expect(liquidityB.isZero()).to.be.true;
});
it('should return liquidity for existing pairs', async function () {
const response = await instance.getLiquidityPool(ACA, AUSD);
const liquidityA = response[0];
const liquidityB = response[1];
expect(liquidityA.gt(web3.utils.toBN('0'))).to.be.true;
expect(liquidityB.gt(web3.utils.toBN('0'))).to.be.true;
});
});
describe('getLiquidityTokenAddress', function () {
it('should not allow tokenA to be a 0x0 address', async function () {
await truffleAssert.reverts(
instance.getLiquidityTokenAddress(NULL_ADDRESS, ACA),
'DEX: tokenA is zero address'
);
});
it('should not allow tokenB to be a 0x0 address', async function () {
await truffleAssert.reverts(
instance.getLiquidityTokenAddress(ACA, NULL_ADDRESS),
'DEX: tokenB is zero address'
);
});
it('should return liquidity token address for an existing pair', async function () {
const response = await instance.getLiquidityTokenAddress(ACA, AUSD);
expect(response).to.equal(LP_ACA_AUSD);
});
});
describe('getSwapTargetAddress', function () {
it('should not allow for the path to include a 0x0 address', async function () {
await truffleAssert.reverts(
instance.getSwapTargetAmount([NULL_ADDRESS, ACA, DOT, RENBTC], 12345678990),
'DEX: token is zero address'
);
await truffleAssert.reverts(
instance.getSwapTargetAmount([ACA, NULL_ADDRESS, DOT, RENBTC], 12345678990),
'DEX: token is zero address'
);
await truffleAssert.reverts(
instance.getSwapTargetAmount([ACA, DOT, NULL_ADDRESS, RENBTC], 12345678990),
'DEX: token is zero address'
);
await truffleAssert.reverts(
instance.getSwapTargetAmount([ACA, DOT, RENBTC, NULL_ADDRESS],