DEX: Factory

The factory contract is a central smart contract in many decentralized exchanges (DEXs) built on Automated Market Makers (AMMs), such as Uniswap. This contract handles the creation and tracking of individual liquidity pools for each token pair traded on the platform. Here’s a closer look at its workings, as well as how to test a factory contract.

Factory Contract:

In an AMM, each token pair (e.g., ETH/DAI) has its own pair contract (or pool), responsible for managing liquidity and enabling swaps between those tokens. The factory contract is responsible for:

  • Creating new pairs: The factory contract deploys a new pool for each unique token pair, ensuring that there’s only one pool per pair.

  • Storing pair addresses: It maintains a mapping between token pairs and their corresponding pool addresses, allowing the DEX to identify and interact with the correct pool.

  • Tracking all pairs: For platforms with multiple pools, the factory contract also serves as a directory, enabling easy tracking and querying of all created pairs.

For example, in a Uniswap-style DEX, the factory contract uses a createPair() function to create pools for each token pair. If a user tries to swap tokens for a new pair, the factory checks if a pool exists and, if not, deploys a new pool contract.

Factory Contract Example (Pseudo Solidity Code)

Here's an example of what a factory contract might look like:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Factory {
    mapping(address => mapping(address => address)) public getPair;  // Tracks pairs
    address[] public allPairs;  // List of all pairs

    event PairCreated(address indexed tokenA, address indexed tokenB, address pair, uint);

    function createPair(address tokenA, address tokenB) external returns (address pair) {
        require(tokenA != tokenB, "Identical addresses not allowed");
        require(getPair[tokenA][tokenB] == address(0), "Pair already exists");

        bytes memory bytecode = abi.encodePacked(type(Pair).creationCode, abi.encode(tokenA, tokenB));
        bytes32 salt = keccak256(abi.encodePacked(tokenA, tokenB));
        assembly {
            pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
        }

        getPair[tokenA][tokenB] = pair;
        getPair[tokenB][tokenA] = pair;
        allPairs.push(pair);

        emit PairCreated(tokenA, tokenB, pair, allPairs.length);
    }

    function getAllPairs() external view returns (address[] memory) {
        return allPairs;
    }
}
  • getPair[tokenA][tokenB]: Maps a token pair to its respective pool.

  • createPair(): Deploys a new pair contract for a token pair.

  • Event Logging: The PairCreated event logs the new pair creation, which can be useful for tracking.

Testing the Factory Contract

To test the factory contract and ensure it operates as expected, you can use a framework like Hardhat or Truffle. Below is a guide on setting up a test for this contract:

Step 1: Install Dependencies

Ensure you have Node.js and npm installed, then install Hardhat:

npm install --save-dev hardhat

Step 2: Create and Configure Your Testing Project

Inside your Hardhat project, set up the factory contract in contracts/Factory.sol, then create a test file in test/Factory.test.js for JavaScript tests.

Step 3: Write Unit Tests for the Factory Contract

Here’s a sample test script for the factory contract using Hardhat and Mocha:

const { expect } = require("chai");

describe("Factory Contract", function () {
  let Factory;
  let factory;
  let owner;
  let tokenA;
  let tokenB;

  beforeEach(async function () {
    [owner, tokenA, tokenB] = await ethers.getSigners();
    Factory = await ethers.getContractFactory("Factory");
    factory = await Factory.deploy();
    await factory.deployed();
  });

  it("Should create a new pair", async function () {
    await factory.createPair(tokenA.address, tokenB.address);

    const pairAddress = await factory.getPair(tokenA.address, tokenB.address);
    expect(pairAddress).to.not.equal(ethers.constants.AddressZero);
  });

  it("Should not allow identical addresses", async function () {
    await expect(factory.createPair(tokenA.address, tokenA.address)).to.be.revertedWith("Identical addresses not allowed");
  });

  it("Should not allow duplicate pairs", async function () {
    await factory.createPair(tokenA.address, tokenB.address);
    await expect(factory.createPair(tokenA.address, tokenB.address)).to.be.revertedWith("Pair already exists");
  });

  it("Should list all pairs", async function () {
    await factory.createPair(tokenA.address, tokenB.address);
    const allPairs = await factory.getAllPairs();
    expect(allPairs.length).to.equal(1);
  });
});

Explanation of Tests:

  • Create a New Pair: Verifies that calling createPair() for unique tokens successfully creates a new pair.

  • Identical Address Check: Ensures that creating a pair with the same token address fails.

  • Duplicate Pair Check: Confirms that a pair for the same tokens cannot be created more than once.

  • Get All Pairs: Checks that getAllPairs() correctly lists the created pairs.

Running Tests

  1. Compile the contracts:

    npx hardhat compile
  2. Run the tests:

    npx hardhat test

If all tests pass, this confirms that your factory contract functions as expected, allowing only unique pairs, deploying them correctly, and listing them accurately.

Additional Test Cases

To expand on testing:

  • Event Emission: Check that the PairCreated event is emitted with correct data upon pair creation.

  • Gas Cost Analysis: Measure gas costs for pair creation, as this can impact usability on mainnet deployments.

For additional information on setting up Hardhat and contract testing, visit:

Testing your factory contract is essential for ensuring reliable operation within a DEX environment. By verifying contract creation, preventing duplicate pairs, and listing pairs accurately, you ensure a more stable and predictable exchange functionality.

Last updated