---
# System prepended metadata

title: "\U0001F43B‍❄️ Logic Exchange"
tags: [Documentation, dex, blockchain]

---

# 🐻‍❄️ Logic Exchange Whitepaper
⚠️⚠️⚠️ **WORK IN PROGRESS** ⚠️⚠️⚠️
👾👾👾 **CURRENTLY ON SEPOLIA TESTNET** 👾👾👾

**Free private keys of accounts with some LOG, ETH, DAI, USDT Tokens**

**cddb7d0e6a2b9c79c68bc79b2602c4c4e1027c605393630c4c6b796bebacbc8b
ce358d2f7e4a6fc1be290e86125dab6e2fc8fb6ed14c4c0db7d06526974ca800**

**[Github](https://github.com/eshumanohare/logicexchange)**
**[Website](https://eshumanohare.github.io/logicexchange/)**
> [color=#c792ea] [TOC] 

# Introduction

Logic Exchange is a DEX platform for exchanging ERC20 compatible tokens on-chain. It is designed like Uniswap which implements an automated market maker for exchange of tokens and providing on-chain liquidity.

Traditional exchanges maintains an order book to match buyers and sellers. A trader can always exchange a token for a fiat currency and then buy a new token with the real money. But this process introduces a time gap between selling the first token and buying the second token. The trader also have to pay gas fee two times and wait for completion of 2 transactions one after the other.

Logic Exchange just like Uniswap holds liquidity of tokens in their pair contract and these reserves are utilized to provide liquidity while swapping. It utilizes an optimal one-sided supply formula to calculate the optimal number of token A and B to swap. This formula is based on the constant product maker `x*y=k`. The liquidity providers(lp's) benifit from two sources in Logic Exchange. 

The first one is 0.3% trade fee that is distribited among all the liquidity providers of the pool according to their proportional share of reserves in the pool. 

The second is through reward that is given in form of Logic Tokens whenever a swap happens. The lp of a pool get logic tokens from the core contract whenever swap happens in that pool, and this share of reward is decided by their maximum share among the two tokens in the pool.

`eg:` Let's say lp Alan holds 10 A and 20 B tokens in A/B pool. Some trader swaps 5 A for some 'x' B. Now Alan will get a share of`0.003 * 5 = 0.015` A tokens. This share is basically `10/Total_A_Reserves`. Also, Alan will get `max(10/Total_A_Reserves, 20/Total_B_Reserves)`times the Logics tokens set to distribute. This is discussed later.

This document outlines the core and pair contract mechanics and technical details for Logic Exchange. The full source code is availible on GitHub.

**Website:** 
[logicexchange.io](https://eshumanohare.github.io/logicexchange)

**Code:** 
[github.com/eshumanohare/logicexchange](https://github.com/eshumanohare/logicexchange)

**Model:**

![](https://i.imgur.com/ilLLoZa.png)

# Creating Pool

The [`CoreLogic.sol`](https://github.com/eshumanohare/logicexchange/blob/cee7d700ac8a1056df41f4149a3d269931b5ad41/contracts/CoreLogic.sol#L38) contract serves as the interface contract to interact with the PairLogic contract where the actual reserves are stored. The external function `createPair` allows any user on the sepolia testnet to create a new `PairLogic` instance or pool.

```solidity=
function createPair(
        address _addrToken0,
        address _addrToken1
    )
        external
        AreTokensAddrInvalid(_addrToken0, _addrToken1)
        returns (address pair)
    {
        if (pairMap[_addrToken0][_addrToken1] == address(0)) {
            // Pair created for the first time
            bytes32 salt = keccak256(
                abi.encodePacked(_addrToken0, _addrToken1)
            ); // order of tokens matters
            pair = address(
                new PairLogic{salt: salt}(
                    _addrToken0,
                    _addrToken1,
                    address(this)
                )
            );
            require(
                pair != address(0),
                "CoreLogic: Error => Failed creating pair contract"
            );

            pairMap[_addrToken0][_addrToken1] = pair;
            pairMap[_addrToken1][_addrToken0] = pair;

            address ADDR_LOGICCOIN = PairLogic(pair).ADDR_LOGICCOIN();
            address ADDR_ETH = PairLogic(pair).ADDR_ETH();
            bool success = PairLogic(pair).setLogicEthPool(
                pairMap[ADDR_LOGICCOIN][ADDR_ETH]
            );
            require(
                success,
                "CoreLogic: Error => Failed while setting LOGIC/ETH pool"
            );

            emit Deployed(pair, _addrToken0, _addrToken1);
        }
        return pair;
    }
```

The `AreTokensAddrInvalid` modifier as its name suggest checks the validity of token addresses. It should not be `address(0)` and both token addresses must be different.

The `pairMap` is a 3D mapping from 2 token address to pair contract address. If the pair is already not created, it basically creates a new pair using `new` keyword.

After the pair contract is created; the `LOG/ETH` pool address it set. There is a specific reason for setting it and retrieving the addresses from Pair contract itself. I will explain that later.

:::info
`NOTE:` The core contract creates a pair instance of any address as long as it is an ERC20 Token address, but it does not check if it is trustworthy or not. The user is expected to check that on their own by simply searching the contract address on `Sepolia` testnet.
:::


# Swaps

The [`PairLogic.sol`](https://github.com/eshumanohare/logicexchange/blob/cee7d700ac8a1056df41f4149a3d269931b5ad41/contracts/PairLogic.sol#L120) contract `swap` function is an external function that only the `CoreLogic.sol` contract can call. Actually all the state changing functions of `PairLogic.sol` can only be called by the `Core` contract or are private.

```solidity=
function swap(
        address _addrToken0,
        address _addrToken1,
        uint256 token0In,
        address to,
        address swapper
    ) external CoreCalling returns (uint256[2] memory) {
        require(
            to != address(0),
            "PairLogic: Error => Receiver of tokens address cannot be zero"
        );
        // checks
        // calculate the optimal amount of token0 tokens that should be swapped with 0.3% trade fee
        // calculate the optimal amount of token1 tokens that will be released with 0.3% trade fee
        // distribute the 0.3% trade fee among the lp by updating their respective supply
        // mint LogicCoins according to the proportion of each lp in the pool with the value of LogicCoin relative to LogicCoin/ETH pool

        uint _token0In = _calculateOptimalToken0ToSwap(token0In);

        require(
            IERC20(_addrToken0).balanceOf(swapper) >= _token0In,
            "PairLogic: Error => Balance insufficient for Token 0"
        );

        uint _token1Out = _calculateOptimalToken1ToSwap(_token0In);
        require(
            reserveToken1 > _token1Out,
            "PairLogic: Error => Reserves too small for this amount"
        );

        bool success = IERC20(_addrToken0).transferFrom(
            swapper,
            address(this),
            token0In
        );
        require(success, "PairLogic: Error => Swap failed for Token 0");

        success = IERC20(_addrToken1).transfer(to, _token1Out);
        require(success, "PairLogic: Error => Swap failed for Token 1");

        if (_addrToken0 == addrToken0) {
            reserveToken0 = reserveToken0.add(token0In); // all token0 tokens are added to the reserve because of 0.3% token0 trade fee
            reserveToken1 = reserveToken1.sub(_token1Out); // token1Out is from 0.997 token0 tokens, the rest 0.003 token0 tokens goes to lp
        } else {
            reserveToken1 = reserveToken1.add(token0In); // all token0 tokens are added to the reserve because of 0.3% token0 trade fee
            reserveToken0 = reserveToken0.sub(_token1Out); // token1Out is from 0.997 token0 tokens, the rest 0.003 token0 tokens goes to lp
        }

        _updateAmountsAfterSwapping();

        return [_token0In, _token1Out];
    }

```

The `swap` contract calls two private functions to calculate the optimal one sided supply of `Token A` and `Token B`.

![](https://i.imgur.com/ZwgZWoC.png)

In the above formula, `k` is the constant product of reserves, `R0` and `R1` are the reserves of `Token A` and `Token B`, `f` is the trade fee `= 0.003`, `s` and `b` are the optimal number of `Token A` and `Token B` to swap to maintain equilibrium.

## Example: DAI → USDT

Let's say at time `t` the reserves in `DAI/USDT` pool are 10 DAI and 20 USDT. So the constant product `k = 10 * 20 = 200`. 

:::info
`Note:` The trade fee is set at 0.003 in the Pair contract by default.
:::
Now a trader swaps 5 DAI for some USDT. Let's calculate the optimal number of tokens that Logic Exchange will take in and give out.

> s = (-1997 * 10 + sqrt(3988009 * 100 + 3988000 * 10 * 5))/1994
> s = 2.2508254174035547
> b = (20*0.997*s)/(10 + 0.997*s)
> b = 3.665566110128592

To maintain the constant product `k` only 0.997 * `s` amount of A tokens are converted to b amount of B tokens. The rest 0.003 * `s` are added to the Token A reserve so that the lp's shares increase.

In the swap function, the pair contract first `transferFrom` the swapper to itself `_token0In` amount of Token A. Then transfers `_token1Out` to `to` address. 

After the swap `_updateAmountsAfterSwapping` is called which is a private function that updates the amount of tokens each lp holds corresponding to their share in the pool.

```solidity=
  function _updateAmountsAfterSwapping() private {
        // when someone swaps tokens
        // share of the lp remain constant
        // amount corresponding to their share changes as reserves changes
        // one reserve goes up and other goes down
        for (uint256 i = 0; i < liquidityProviders.length; i++) {
            address lp_i = liquidityProviders[i];
            lpShares[lp_i][0] = lpShares[lp_i][2].mul(reserveToken0).div(
                10 ** 18
            );
            lpShares[lp_i][1] = lpShares[lp_i][3].mul(reserveToken1).div(
                10 ** 18
            );
        }
    }
```

# Add Liquidity

When a lp adds liquidity to a newly created pool, the lp has the power to set the intial exchange rate of the tokens inside the pool. This power is limited due to a restricition inside the pair contract that prevents lp to add liquidity with difference more than 1 ether or 10 ^ 18 tokens.

```solidity=
modifier TokensInRange(uint256 token0In, uint256 token1In) {
        if (token0In >= token1In) {
            require(
                token1In >= 1000,
                "PairLogic: Error => Liquidity added too less"
            );
            require(
                (token0In - token1In) <= 1 ether,
                "PairLogic: Error => Liquidity difference too much"
            );
        } else {
            require(
                token0In >= 1000,
                "PairLogic: Error => Liquidity added too less"
            );
            require(
                (token1In - token0In) <= 1 ether,
                "PairLogic: Error => Liquidity difference too much"
            );
        }
        _;
    }

```

This unbalanced ratio is an opportunity for arbitrage traders to profit and in the process balance the reserve ratio inside the pool. All the other lp have to deposit tokens by the initial exchange rate until the arbitrageurs correct the exchange rate.

Liquidity providers just have to call the function `addLiquidity` inside the core contract to deposit their funds. Unlike Uniswap this process does not mint any liquidity tokens.

Add liquidity source code inside `PairLogic.sol`

```solidity=
    function addLiquidity(
        address lp,
        address _addrToken0,
        address _addrToken1,
        uint256 _token0In,
        uint256 _token1In
    )
        external
        CoreCalling
        TokensNotZero(_token0In, _token1In)
        TokensInRange(_token0In, _token1In)
        returns (bool)
    {
        uint256 _reserve0 = reserveToken0;
        uint256 _reserve1 = reserveToken1;
        bool changed = false;

        if (_addrToken0 != addrToken0) {
            _addrToken0 = addrToken1;
            _addrToken1 = addrToken0;
            _reserve0 = reserveToken1;
            _reserve1 = reserveToken0;
            changed = true;
        }

        if (_reserve0 != 0) {
            // check for one reserve is sufficient
            // update the balanced amount of tokens to deposit
            _token0In = _reserve0.mul(_token1In).div(_reserve1);
            _token1In = _reserve1.mul(_token0In).div(_reserve0);
        }

        // rechecking of balances is necessary
        require(
            IERC20(_addrToken0).balanceOf(lp) >= _token0In,
            "PairLogic: Error => Balance insufficient for Token 0"
        );
        require(
            IERC20(_addrToken1).balanceOf(lp) >= _token1In,
            "PairLogic: Error => Balance insufficient for Token 1"
        );

        bool success = IERC20(_addrToken0).transferFrom(
            lp,
            address(this),
            _token0In
        );
        require(success, "PairLogic: Error => Transfer Failed for Token 0");

        success = IERC20(_addrToken1).transferFrom(
            lp,
            address(this),
            _token1In
        );
        require(success, "PairLogic: Error => Transfer Failed for Token 1");

        liquidityProviders.push(lp);

        _reserve0 = _reserve0.add(_token0In);
        _reserve1 = _reserve1.add(_token1In);

        if (changed) {
            reserveToken0 = _reserve1;
            reserveToken1 = _reserve0;
        } else {
            reserveToken0 = _reserve0;
            reserveToken1 = _reserve1;
        }

        _increaseLiquidity(lp, _addrToken0, _token0In, _token1In);
        _updateSharesAfterAddingLiq();

        return true;
    }
```

When the lp's adds liquidity after the first lp; the `token0In` and `token1In` is set according to the pool ratio. Then the respective tokens are transferred from the lp to the pair contract. The reserves are updated using `_increaseLiquidity` function and the shares of all lp's of that pool are updated using the `_updateSharesAfterAddingLiq` function.

:::info
`Note:` When a new lp adds liquidity to the pool, the remaining lp's shares decrease as the reserve size increase for both the tokens.
:::

# Burning Liquidity

The liquidity providers can anytime remove their liquidity from a pool and get their investment at the current exchange rate. This is achieved using their share in the pool at the time of burning liquidity. The shares of lp's or amount of tokens lp's hold are updated after every swap, add or burn of tokens.

```solidity=
function burnTokens(address owner) external CoreCalling returns (bool) {
        uint256 _amount0 = lpShares[owner][2].mul(reserveToken0).div(10 ** 18);
        uint256 _amount1 = lpShares[owner][3].mul(reserveToken1).div(10 ** 18);

        bool success = IERC20(addrToken0).transfer(owner, _amount0);
        require(success, "PairLogic: Error => Burning Failed for Token 0");
        reserveToken0 = reserveToken0.sub(_amount0);
        lpShares[owner][0] = 0;
        lpShares[owner][2] = 0;

        success = IERC20(addrToken1).transfer(owner, _amount1);
        require(success, "PairLogic: Error => Burning Failed for Token 1");
        reserveToken1 = reserveToken1.sub(_amount1);
        lpShares[owner][1] = 0;
        lpShares[owner][3] = 0;
        if (reserveToken0 != 0) {
            _updateAfterBurning();
        }
        return true;
    }

```

Using the current share, the amount of tokens to burn are calculated. These tokens are then transferred to the lp address and corresponding reserves are reduced by tokens burned. Since the burn process decrease the reserve size, the share of other lp's increase along with the number of tokens they hold.

# Minting LOG Tokens

:::info
`Note:` This process does not actually mint LOG tokens but rather transfers from the Core contract to the lp's address. The Core contract is approved for 50 million LOG tokens by the `creator` or basically `me`.
:::
As already discussed in swaps, LOG tokens are distributed among the lp's of a pool according to their maximum share in the pool.

```solidity=
 function _mintLPTokens(
        address addrToken0,
        address addrToken1
    ) private returns (bool) {
        address pair = pairMap[addrToken0][addrToken1];
        address[] memory liquidityProviders = PairLogic(pair)
            .getliquidityProviders();

        uint256 logicCoinsAllowed = _calculateLogicCoinProp(pair);

        for (uint256 i = 0; i < liquidityProviders.length; i++) {
            // mint logic coins
            uint256 lpShareToken0 = PairLogic(pair).lpShares(
                liquidityProviders[i],
                2
            );
            uint256 lpShareToken1 = PairLogic(pair).lpShares(
                liquidityProviders[i],
                3
            );
            uint256 maxShare = lpShareToken0 >= lpShareToken1
                ? lpShareToken0
                : lpShareToken1;
            uint256 amount = logicCoinsAllowed.mul(maxShare).div(10 ** 18);
            bool success = IERC20(PairLogic(pair).ADDR_LOGICCOIN())
                .transferFrom(
                    IERC20(PairLogic(pair).ADDR_LOGICCOIN()).getCreator(),
                    address(this),
                    amount
                );
            require(
                success,
                "PairLogic: Error => Transfer Failed from logic contract to core contract"
            );

            success = IERC20(PairLogic(pair).ADDR_LOGICCOIN()).transfer(
                liquidityProviders[i],
                amount
            );
            require(success, "PairLogic: Error => Minting failed");
        }
        return true;
    }
```

Another function inside the core contract `_calculateLogicCoinProp` computes the exchange rate of ETH for LOG token. This is retrieved from the LOG/ETH pool. It is taken to be the number of ETH tokens per LOG token because I assume that traders who are lp's would want to swap their LOG tokens for a more valuable asset like ETH. This trade will increase the LOG reserves and decrease the ETH reserves thus making the ETH more expensive in the process. Though this unbalanced ratio can always be set in equilibrium by the arbitrage traders. 

The motive `_calculateLogicCoinProp` is not to make ETH a more valuable than LOG, but to make sure that the reward being given to the lp's is not so much that it drains out the core contract allowance. With time I assume the reward to decrease by a small rate though my assumption barely has some grounds and it might be wrong.

```solidity=
    function _calculateLogicCoinProp(
        address pair
    ) private view returns (uint256) {
        address ADDR_LOGIC_ETH_POOL = pairMap[PairLogic(pair).ADDR_LOGICCOIN()][
            PairLogic(pair).ADDR_ETH()
        ];
        uint256 reservesLogic = PairLogic(ADDR_LOGIC_ETH_POOL).reserveToken0();
        uint256 reserveETH = PairLogic(ADDR_LOGIC_ETH_POOL).reserveToken1();
        uint256 prop = reserveETH.mul(10 ** 18).div(reservesLogic);
        return prop;
    }

```

:::danger
`Note:` This is just a fun project I created for testing out my knowledge. That's why its on Sepolia. But I will really appreciate if somebody takes out their time to read all this and try out the website. Cheers 🍺
:::