Links

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.

Table of contents

About

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 conract. 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) or with the Hardhat's built in network emulator.
Let's take a look!
NOTE: You can refer to the complete code of this tutorial at https://github.com/AcalaNetwork/hardhat-tutorials/tree/master/DEX

Smart contract

As mentioned in the introduction, this tutorial doesn't include the smart contract, so the contracts folder can be removed as well. We will however be using the @acala-network/contracts dependency in order to gain access to the precompiled resources of the DEX smart contract.

Test

Tests for this tutorial will validate the expected values returned by the DEX predeployed smart contract. The test file in our case is called DEX.js. Within it we import the expect from chai dependency and Contract from the ethers dependency. We are using Contract in stead of ContractFactory, because the contract is already deployed to the network. The ACA, AUSD, LP_ACA_AUSD, DOT, RENBTC and DEX, which are exports from the ADDRESS utility of @acala-network/contracts dependency, are imported and they hold the values of the addresses of the corresponding smart contracts. Additionally we are importing the compiled DEX smart contract from the @acala-network/contracts dependency, which we will use to instantiate the smart contract. Token precompile is imported from @acala-network/contracts so that we can instantiate the predeployed token smart contracts and validate the balance changes after interacting with the DEX.
NOTE: Since the ACA ERC20 token mirrors the balance of the native ACA currency, we can not expect that sending x amount of ACA into the DEX, will decrease our ACA balance by that exact amount. Some of it will also be used to pay for the transaction fees. We have to keep this in mind while writing the tests and scripts.
The test file with import statements and an empty test should look like this:
const { expect } = require("chai");
const { Contract } = require("ethers");
const { ACA, AUSD, LP_ACA_AUSD, DOT, RENBTC, DEX } = require("@acala-network/contracts/utils/MandalaAddress");
const DEXContract = require("@acala-network/contracts/build/contracts/DEX.json");
const TokenContract = require("@acala-network/contracts/build/contracts/Token.json");
const { parseUnits } = require("@acala-network/eth-providers/node_modules/@ethersproject/units");
const NULL_ADDRESS = "0x0000000000000000000000000000000000000000";
describe("DEX contract", function () {
});
To prepare for the testing, we have to define the global variables, instance, ACAinstance, AUSDinstance, deployer and deployerAddress. The instance will store the predeployed DEX smart contract instance, the ACAinstance and AUSDinstance variables will store their respective ERC20 predeployed token smart contracts. The deployer will store Signer and the deployerAddress will store its address. Let's assign these values in the beforeEach action:
let instance;
let ACAinstance;
let AUSDinstance;
let deployer;
let deployerAddress;
beforeEach(async function () {
[deployer] = await ethers.getSigners();
deployerAddress = await deployer.getAddress();
instance = new Contract(DEX, DEXContract.abi, deployer);
ACAinstance = new Contract(ACA, TokenContract.abi, deployer);
AUSDinstance = new Contract(AUSD, TokenContract.abi, deployer);
});
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. 1.
    getLiquidityPool function to get the liquidity pool of the desired pair.
  2. 2.
    getLiquidityTokenAddress function to get the address of the liquidity token for the desired pair.
  3. 3.
    getSwapTargetAddress function to get the informative amount of the swap egress token based on set supply of the ingress token.
  4. 4.
    getSwapSupplyAmount function to get the informative amount of the swap supply based on the set target amount of egress token.
  5. 5.
    swapWithExactSupply function to swap tokens based on the set supply of the ingress token.
  6. 6.
    swapWithExactTarget function to swap tokens based on the set target of the egress token.
  7. 7.
    addLiquidity function that adds liquidity of the desired pair.
  8. 8.
    removeLiquidity function that removes liquidity of the desired pair.
Before we add the inner describe blocks within the Operation describe block, we should increase the timeout for this test to 50s, to make sure that the tests can be run on the public test network in addition to the local development network:
describe("Operation", function () {
this.timeout(50000);
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. 1.
    tokenA should not be a 0x0 address.
  2. 2.
    tokenB should not be a 0x0 address.
  3. 3.
    Liquidity of the non-existent pair should be 0.
  4. 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 expect(instance.getLiquidityPool(NULL_ADDRESS, ACA)).to
.be.revertedWith("DEX: tokenA is zero address");
});
it("should not allow tokenB to be a 0x0 address", async function () {
await expect(instance.getLiquidityPool(ACA, NULL_ADDRESS)).to
.be.revertedWith("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).to.equal(0);
expect(liquidityB).to.equal(0);
});
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).to.be.above(0);
expect(liquidityB).to.be.above(0);
});
When validating the getLiquidityTokenAddress function, we will check for the following examples:
  1. 1.
    tokenA should not be a 0x0 address.
  2. 2.
    tokenB should not be a 0x0 address.
  3. 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 expect(instance.getLiquidityTokenAddress(NULL_ADDRESS, ACA)).to
.be.revertedWith("DEX: tokenA is zero address");
});
it("should not allow tokenB to be a 0x0 address", async function () {
await expect(instance.getLiquidityTokenAddress(ACA, NULL_ADDRESS)).to
.be.revertedWith("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. 1.
    path should not include the 0x0 address.
  2. 2.
    supplyAmount should not be 0.
  3. 3.
    Getting swap target amount should return 0 for an incompatible path.
  4. 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 () {
let path = [NULL_ADDRESS, ACA, DOT, RENBTC];
await expect(instance.getSwapTargetAmount(path, 12345678990)).to
.be.revertedWith("DEX: token is zero address");
path = [ACA, NULL_ADDRESS, DOT, RENBTC];
await expect(instance.getSwapTargetAmount(path, 12345678990)).to
.be.revertedWith("DEX: token is zero address");
path = [ACA, DOT, NULL_ADDRESS, RENBTC];
await expect(instance.getSwapTargetAmount(path, 12345678990)).to
.be.revertedWith("DEX: token is zero address");
path = [ACA, DOT, RENBTC, NULL_ADDRESS];
await expect(instance.getSwapTargetAmount(path, 12345678990)).to
.be.revertedWith("DEX: token is zero address");
});
it("should not allow supplyAmount to be 0", async function () {
await expect(instance.getSwapTargetAmount([ACA, DOT], 0)).to
.be.revertedWith("DEX: supplyAmount is zero");
});
it("should return 0 for an incompatible path", async function () {
const response = await instance.getSwapTargetAmount([ACA, DOT], 100);
expect(response.toString()).to.equal('0');
})
it("should return a swap target amount", async function () {
const response = await instance.getSwapTargetAmount([ACA, AUSD], 100);
expect(response).to.be.above(0);
});
When validating the getSwapSupplyAmount function, we will check for the following examples:
  1. 1.
    path should not include the 0x0 address.
  2. 2.
    targetAmount should not be 0.
  3. 3.
    Getting swap supply amount should revert for an incompatible path.
  4. 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 () {
let path = [NULL_ADDRESS, ACA, DOT, RENBTC];
await expect(instance.getSwapSupplyAmount(path, 12345678990)).to
.be.revertedWith("DEX: token is zero address");
path = [ACA, NULL_ADDRESS, DOT, RENBTC];
await expect(instance.getSwapSupplyAmount(path, 12345678990)).to
.be.revertedWith("DEX: token is zero address");
path = [ACA, DOT, NULL_ADDRESS, RENBTC];
await expect(instance.getSwapSupplyAmount(path, 12345678990)).to
.be.revertedWith("DEX: token is zero address");
path = [ACA, DOT, RENBTC, NULL_ADDRESS];
await expect(instance.getSwapSupplyAmount(path, 12345678990)).to
.be.revertedWith("DEX: token is zero address");
});
it("should not allow targetAmount to be 0", async function () {
await expect(instance.getSwapSupplyAmount([ACA, AUSD], 0)).to
.be.revertedWith("DEX: targetAmount is zero");
});
it("should return 0 for an incompatible path", async function () {
const response = await instance.getSwapSupplyAmount([ACA, DOT], 100);
expect(response.toString()).to.equal('0');
});
it("should return the supply amount", async function () {
const response = await instance.getSwapSupplyAmount([ACA, AUSD], 100);
expect(response).to.be.above(0);
});
When validating the swapWithExactSupply function, we will check for the following examples:
  1. 1.
    path should not include the 0x0 address.
  2. 2.
    supplyAmount should not be 0.
  3. 3.
    Egress token balance of the caller should increase.
  4. 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 () {
let path = [NULL_ADDRESS, ACA, DOT, RENBTC];
await expect(instance.swapWithExactSupply(path, 12345678990, 1)).to
.be.revertedWith("DEX: token is zero address");
path = [ACA, NULL_ADDRESS, DOT, RENBTC];
await expect(instance.swapWithExactSupply(path, 12345678990, 1)).to
.be.revertedWith("DEX: token is zero address");
path = [ACA, DOT, NULL_ADDRESS, RENBTC];
await expect(instance.swapWithExactSupply(path, 12345678990, 1)).to
.be.revertedWith("DEX: token is zero address");
path = [ACA, DOT, RENBTC, NULL_ADDRESS];
await expect(instance.swapWithExactSupply(path, 12345678990, 1)).to
.be.revertedWith("DEX: token is zero address");
});
it("should not allow supplyAmount to be 0", async function () {
await expect(instance.swapWithExactSupply([ACA, AUSD], 0, 1)).to
.be.revertedWith("DEX: supplyAmount is zero");
});
it("should allocate the tokens to the caller", async function () {
const initalBalance = await ACAinstance.balanceOf(deployerAddress);
const initBal = await AUSDinstance.balanceOf(deployerAddress);
const path = [ACA, AUSD];
const expected_target = await instance.getSwapTargetAmount(path, 100);
await instance.connect(deployer).swapWithExactSupply(path, 100, 1);
const finalBalance = await ACAinstance.balanceOf(deployerAddress);
const finBal = await AUSDinstance.balanceOf(deployerAddress);
// 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).to.be.below(initalBalance.sub(100));
expect(finBal).to.equal(initBal.add(expected_target));
});
it("should emit a Swaped event", async function () {
const path = [ACA, AUSD];
const expected_target = await instance.getSwapTargetAmount(path, 100);
await expect(instance.connect(deployer).swapWithExactSupply(path, 100, 1)).to
.emit(instance, "Swaped")
.withArgs(deployerAddress, path, 100, expected_target);
});
When validating the swapWithExactTarget function, we will check for the following examples:
  1. 1.
    path should not include the 0x0 address.
  2. 2.
    targetAmount should not be 0.
  3. 3.
    Egress token balance of the caller should increase.
  4. 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 () {
let path = [NULL_ADDRESS, ACA, DOT, RENBTC];
await expect(instance.swapWithExactTarget(path, 1, 12345678990)).to
.be.revertedWith("DEX: token is zero address");
path = [ACA, NULL_ADDRESS, DOT, RENBTC];
await expect(instance.swapWithExactTarget(path, 1, 12345678990)).to
.be.revertedWith("DEX: token is zero address");
path = [ACA, DOT, NULL_ADDRESS, RENBTC];
await expect(instance.swapWithExactTarget(path, 1, 12345678990)).to
.be.revertedWith("DEX: token is zero address");
path = [ACA, DOT, RENBTC, NULL_ADDRESS];
await expect(instance.swapWithExactTarget(path, 1, 12345678990)).to
.be.revertedWith("DEX: token is zero address");
});
it("should not allow targetAmount to be 0", async function () {
await expect(instance.swapWithExactTarget([ACA, AUSD], 0, 1234567890)).to
.be.revertedWith("DEX: targetAmount is zero");
});
it("should allocate tokens to the caller", async function () {
const initalBalance = await ACAinstance.balanceOf(deployerAddress);
const initBal = await AUSDinstance.balanceOf(deployerAddress);
const path = [ACA, AUSD];
const expected_supply = await instance.getSwapSupplyAmount(path, 100);
await instance.connect(deployer).swapWithExactTarget(path, 100, 1234567890);
const finalBalance = await ACAinstance.balanceOf(deployerAddress);
const finBal = await AUSDinstance.balanceOf(deployerAddress);
// 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).to.be.below(initalBalance.sub(expected_supply));
expect(finBal).to.equal(initBal.add(100));
});
it("should emit Swaped event", async function () {
const path = [ACA, AUSD];
const expected_supply = await instance.getSwapSupplyAmount(path, 100);
await expect(instance.connect(deployer).swapWithExactTarget(path, 100, 1234567890)).to
.emit(instance, "Swaped")
.withArgs(deployerAddress, path, expected_supply, 100);
});
When validating the addLiquidity function, we will check for the following examples:
  1. 1.
    tokenA should not be a 0x0 address.
  2. 2.
    tokenB should not be a 0x0 address.
  3. 3.
    maxAmountA should not be 0.
  4. 4.
    maxAmountB should not be 0.
  5. 5.
    Successfull execution should increase the liquidity of the pair.
  6. 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 expect(instance.addLiquidity(NULL_ADDRESS, AUSD, 1000, 1000, 1)).to
.be.revertedWith("DEX: tokenA is zero address");
});
it("should not allow tokenB to be 0x0 address", async function () {
await expect(instance.addLiquidity(ACA, NULL_ADDRESS, 1000, 1000, 1)).to
.be.revertedWith("DEX: tokenB is zero address");
});
it("should not allow maxAmountA to be 0", async function () {
await expect(instance.addLiquidity(ACA, AUSD, 0, 1000, 1)).to
.be.revertedWith("DEX: maxAmountA is zero");
});
it("should not allow maxAmountB to be 0", async function () {
await expect(instance.addLiquidity(ACA, AUSD, 1000, 0, 1)).to
.be.revertedWith("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]).to.be.above(intialLiquidity[0]);
expect(finalLiquidity[1]).to.be.above(intialLiquidity[1]);
});
it("should emit AddedLiquidity event", async function () {
await expect(instance.connect(deployer).addLiquidity(ACA, AUSD, 1000, 1000, 1)).to
.emit(instance, "AddedLiquidity")
.withArgs(deployerAddress, ACA, AUSD, 1000, 1000);
});
When validating the removeLiquidity function, we will check for the following examples:
  1. 1.
    tokenA should not be a 0x0 address.
  2. 2.
    tokenB should not be a 0x0 address.
  3. 3.
    removeShare should not be 0.
  4. 4.
    Successfull execution should reduce the liquidity of the pair.
  5. 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 expect(instance.removeLiquidity(NULL_ADDRESS, AUSD, 1, 0, 0)).to
.be.revertedWith("DEX: tokenA is zero address");
});
it("should not allow tokenB to be a 0x0 address", async function () {
await expect(instance.removeLiquidity(ACA, NULL_ADDRESS, 1, 0, 0)).to
.be.revertedWith("DEX: tokenB is zero address");
});
it("should not allow removeShare to be 0", async function () {
await expect(instance.removeLiquidity(ACA, AUSD, 0, 0, 0)).to
.be.revertedWith("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(finalLiquidity[0]).to.be.below(intialLiquidity[0]);
expect(finalLiquidity[1]).to.be.below(intialLiquidity[1]);
});
it("should emit RemovedLiquidity event", async function () {
await expect(instance.connect(deployer).removeLiquidity(ACA, AUSD, 1, 0, 0)).to
.emit(instance, "RemovedLiquidity")
.withArgs(deployerAddress, ACA, AUSD, 1);
});
With that, our test is ready to be run.
Your test/DEX.js should look like this:
const { expect } = require('chai');
const { Contract } = require('ethers');
const { ACA, AUSD, LP_ACA_AUSD, DOT, RENBTC, DEX } = require('@acala-network/contracts/utils/MandalaAddress');
const DEXContract = require('@acala-network/contracts/build/contracts/DEX.json');
const TokenContract = require('@acala-network/contracts/build/contracts/Token.json');
const { parseUnits } = require('@ethersproject/units');
const NULL_ADDRESS = '0x0000000000000000000000000000000000000000';
describe('DEX contract', function () {
let instance;
let ACAinstance;
let AUSDinstance;
let deployer;
let deployerAddress;
beforeEach(async function () {
[deployer] = await ethers.getSigners();
deployerAddress = await deployer.getAddress();
instance = new Contract(DEX, DEXContract.abi, deployer);
ACAinstance = new Contract(ACA, TokenContract.abi, deployer);
AUSDinstance = new Contract(AUSD, TokenContract.abi, deployer);
});
describe('Operation', function () {
this.timeout(50000);
describe('getLiquidityPool', function () {
it('should not allow tokenA to be a 0x0 address', async function () {
await expect(instance.getLiquidityPool(NULL_ADDRESS, ACA)).to.be.revertedWith('DEX: tokenA is zero address');
});
it('should not allow tokenB to be a 0x0 address', async function () {
await expect(instance.getLiquidityPool(ACA, NULL_ADDRESS)).to.be.revertedWith('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).to.equal(0);
expect(liquidityB).to.equal(0);
});
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).to.be.above(0);
expect(liquidityB).to.be.above(0);
});
});
describe('getLiquidityTokenAddress', function () {
it('should not allow tokenA to be a 0x0 address', async function () {
await expect(instance.getLiquidityTokenAddress(NULL_ADDRESS, ACA)).to.be.revertedWith(
'DEX: tokenA is zero address'
);
});
it('should not allow tokenB to be a 0x0 address', async function () {
await expect(instance.getLiquidityTokenAddress(ACA, NULL_ADDRESS)).to.be.revertedWith(
'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 () {
let path = [NULL_ADDRESS, ACA, DOT, RENBTC];
await expect(instance.getSwapTargetAmount(path, 12345678990)).to.be.revertedWith('DEX: token is zero address');
path = [ACA, NULL_ADDRESS, DOT, RENBTC];
await expect(instance.getSwapTargetAmount(path, 12345678990)).to.be.revertedWith('DEX: token is zero address');
path = [ACA, DOT, NULL_ADDRESS, RENBTC];
await expect(instance.getSwapTargetAmount(path, 12345678990)).to.be.revertedWith('DEX: token is zero address');
path = [ACA, DOT, RENBTC, NULL_ADDRESS];
await expect(instance.getSwapTargetAmount(path, 12345678990)).to.be.revertedWith('DEX: token is zero address');
});
it('should not allow supplyAmount to be 0', async function () {
await expect(instance.getSwapTargetAmount([ACA, DOT], 0)).to.be.revertedWith('DEX: supplyAmount is zero');
});
it('should return 0 for an incompatible path', async function () {
const response = await instance.getSwapTargetAmount([ACA, DOT], 100);
expect(response.toString()).to.equal("0");
});
it('should return a swap target amount', async function () {
const response = await instance.getSwapTargetAmount([ACA, AUSD], 100);
expect(response).to.be.above(0);
});
});
describe('getSwapSupplyAmount', function () {
it('should not allow an address in the path to be a 0x0 address', async function () {
let path = [NULL_ADDRESS, ACA, DOT, RENBTC];
await expect(instance.getSwapSupplyAmount(path, 12345678990)).to.be.revertedWith('DEX: token is zero address');
path = [ACA, NULL_ADDRESS, DOT, RENBTC];
await expect(instance.getSwapSupplyAmount(path, 12345678990)).to.be.revertedWith('DEX: token is zero address');
path = [ACA, DOT, NULL_ADDRESS, RENBTC];
await expect(instance.getSwapSupplyAmount(path, 12345678990)).to.be.revertedWith('DEX: token is zero address');
path = [ACA, DOT, RENBTC, NULL_ADDRESS];
await expect(instance.getSwapSupplyAmount(path, 12345678990)).to.be.revertedWith('DEX: token is zero address');
});
it('should not allow targetAmount to be 0', async function () {
await expect(instance.getSwapSupplyAmount([ACA, AUSD], 0)).to.be.revertedWith('DEX: targetAmount is zero');
});
it('should return 0 for an incompatible path', async function () {
const response = await instance.getSwapSupplyAmount([ACA, DOT], 100);
expect(response.toString()).to.equal("0");
});
it('should return the supply amount', async function () {
const response = await instance.getSwapSupplyAmount([ACA, AUSD], 100);
expect(response).to.be.above(0);
});
});
describe('swapWithExactSupply', function () {
it('should not allow path to contain a 0x0 address', async function () {
let path = [NULL_ADDRESS, ACA, DOT, RENBTC];
await expect(instance.swapWithExactSupply(path, 12345678990, 1)).to.be.revertedWith(
'DEX: token is zero address'
);
path = [ACA, NULL_ADDRESS, DOT, RENBTC];
await expect(instance.swapWithExactSupply(path, 12345678990, 1)).to.be.revertedWith(
'DEX: token is zero address'
);