├── .gitattributes ├── .githooks ├── commit-msg │ └── must-include-issue-number └── prepare-commit-msg │ └── prepend-issue-number-from-branch ├── .github └── workflows │ └── CI.yml ├── .gitignore ├── .mocharc.json ├── .prettierrc ├── .waffle.json ├── .yarnrc ├── CODEOWNERS ├── LICENSE ├── README.md ├── contracts ├── CroDefiSwapERC20.sol ├── CroDefiSwapFactory.sol ├── CroDefiSwapPair.sol ├── Migrations.sol ├── interfaces │ ├── ICroDefiSwapCallee.sol │ ├── ICroDefiSwapERC20.sol │ ├── ICroDefiSwapFactory.sol │ ├── ICroDefiSwapPair.sol │ └── IERC20.sol ├── libraries │ ├── Math.sol │ ├── SafeMath.sol │ └── UQ112x112.sol └── test │ └── ERC20.sol ├── migrations ├── 1_initial_migration.js └── 2_deploy_contracts.js ├── package.json ├── pull_request_template.md ├── scripts └── replaceFactory.js ├── test ├── CropSwapERC20.spec.ts ├── CropSwapFactory.spec.ts ├── CropSwapPair.spec.ts └── shared │ ├── fixtures.ts │ └── utilities.ts ├── truffle-config.js ├── tsconfig.json └── yarn.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity -------------------------------------------------------------------------------- /.githooks/commit-msg/must-include-issue-number: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD) 4 | if [ "$BRANCH_NAME"x == "master"x ] || [ "$BRANCH_NAME"x == "staging"x ]; then 5 | exit 0 6 | fi 7 | if [ -z "$(head -n1 "$1" | grep -o -E 'SWAP-[0-9]+')" ]; then 8 | echo >&2 ERROR: Commit message must include issue number. 9 | exit 1 10 | fi 11 | 12 | exit 0 13 | -------------------------------------------------------------------------------- /.githooks/prepare-commit-msg/prepend-issue-number-from-branch: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -z $(head -n1 "$1" | grep -o -E '#[0-9]+') ]; then 4 | BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD) 5 | ISSUE_NUMBER=$(echo "$BRANCH_NAME" | sed -E 's/^feature\/#?([Ss][Ww][Aa][Pp]-[0-9]+).*/\1/g') 6 | if [ "$ISSUE_NUMBER"x != "$BRANCH_NAME"x ]; then 7 | sed -i.bak -e "1 s/^/Issue $ISSUE_NUMBER: /" $1 8 | echo "[Prepare Commit Message Hook] Prepened issue number ${ISSUE_NUMBER} to commit message" 9 | fi 10 | fi 11 | 12 | exit 0 13 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | test: 8 | strategy: 9 | matrix: 10 | node: ['10.x', '12.x'] 11 | os: [ubuntu-latest] 12 | 13 | runs-on: ${{ matrix.os }} 14 | 15 | steps: 16 | - uses: actions/checkout@v1 17 | - uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node }} 20 | 21 | - run: npm install -g yarn 22 | 23 | - id: yarn-cache 24 | run: echo "::set-output name=dir::$(yarn cache dir)" 25 | - uses: actions/cache@v1 26 | with: 27 | path: ${{ steps.yarn-cache.outputs.dir }} 28 | key: ${{ matrix.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 29 | restore-keys: | 30 | ${{ matrix.os }}-yarn- 31 | 32 | - run: yarn 33 | - run: yarn lint 34 | - run: yarn test 35 | deploy-ganache: 36 | runs-on: ubuntu-latest 37 | name: Deploy to Ganache 38 | services: 39 | ganache: 40 | image: trufflesuite/ganache-cli 41 | ports: 42 | - 8545:8545 43 | steps: 44 | - uses: actions/checkout@v1 45 | - uses: actions/setup-node@v1 46 | with: 47 | node-version: '12.x' 48 | - run: npm install -g yarn 49 | - id: yarn-cache 50 | run: echo "::set-output name=dir::$(yarn cache dir)" 51 | - uses: actions/cache@v1 52 | with: 53 | path: ${{ steps.yarn-cache.outputs.dir }} 54 | key: ${{ matrix.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 55 | restore-keys: | 56 | ${{ matrix.os }}-yarn- 57 | - name: Create .env 58 | run: | 59 | echo "INFURA_API_KEY=$INFURA_API_KEY" >> .env 60 | echo "MNEMONIC=$MNEMONIC" >> .env 61 | shell: bash 62 | env: 63 | INFURA_API_KEY: ${{ secrets.INFURA_API_KEY }} 64 | MNEMONIC: ${{ secrets.MNEMONIC }} 65 | - run: yarn 66 | - run: yarn compile 67 | - run: yarn truffle-compile 68 | - run: yarn replace-factory 69 | - run: yarn truffle-migrate 70 | deploy-ropsten: 71 | runs-on: ubuntu-latest 72 | name: Deploy to Ropsten 73 | if: github.ref == 'refs/heads/staging' 74 | steps: 75 | - uses: actions/checkout@v1 76 | - uses: actions/setup-node@v1 77 | with: 78 | node-version: '12.x' 79 | - run: npm install -g yarn 80 | - id: yarn-cache 81 | run: echo "::set-output name=dir::$(yarn cache dir)" 82 | - uses: actions/cache@v1 83 | with: 84 | path: ${{ steps.yarn-cache.outputs.dir }} 85 | key: ${{ matrix.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 86 | restore-keys: | 87 | ${{ matrix.os }}-yarn- 88 | - name: Create SSH key 89 | run: | 90 | mkdir -p ~/.ssh/ 91 | echo "$GITHUB_PRIVATE_KEY" | base64 -d > ~/.ssh/id_rsa 92 | sudo chmod 600 ~/.ssh/id_rsa 93 | shell: bash 94 | env: 95 | GITHUB_PRIVATE_KEY: ${{ secrets.READ_ONLY_GITHUB_SSH_KEY }} 96 | - name: Create .env 97 | run: | 98 | echo "INFURA_API_KEY=$INFURA_API_KEY" >> .env 99 | echo "MNEMONIC=$MNEMONIC" >> .env 100 | shell: bash 101 | env: 102 | INFURA_API_KEY: ${{ secrets.INFURA_API_KEY }} 103 | MNEMONIC: ${{ secrets.MNEMONIC }} 104 | - run: yarn 105 | - run: yarn compile 106 | - run: yarn truffle-compile 107 | - run: yarn replace-factory 108 | - run: yarn truffle-migrate-ropsten 109 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | cache/ 4 | .env 5 | .idea/ 6 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extension": ["ts"], 3 | "spec": "./test/**/*.spec.ts", 4 | "require": "ts-node/register", 5 | "timeout": 12000 6 | } 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "printWidth": 120 5 | } 6 | -------------------------------------------------------------------------------- /.waffle.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerVersion": "./node_modules/solc", 3 | "outputType": "all", 4 | "compilerOptions": { 5 | "outputSelection": { 6 | "*": { 7 | "*": [ 8 | "evm.bytecode.object", 9 | "evm.deployedBytecode.object", 10 | "abi", 11 | "evm.bytecode.sourceMap", 12 | "evm.deployedBytecode.sourceMap", 13 | "metadata" 14 | ], 15 | "": ["ast"] 16 | } 17 | }, 18 | "evmVersion": "istanbul", 19 | "optimizer": { 20 | "enabled": true, 21 | "runs": 999999 22 | } 23 | }, 24 | "outputDirectory": "./build/" 25 | } 26 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | --frozen-lockfile true 2 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners will be the default owners for everything in 2 | # the repo. Unless a later match takes precedence, 3 | # below PIC will be requested for 4 | # review when someone opens a pull request. 5 | * @landanhu @FrancoCRO 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DeFi Swap 2 | Copyright (C) 2020 Uniswap 3 | Modifications Copyright (C) 2020 Crypto.com 4 | Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. 5 | This program is free software released under the GNU General Public License as published by the Free Software Foundation, version 3 of the License and any conditions added under section 7 of the License. You can redistribute it and/or modify it under the terms of the GNU General Public License. 6 | The modifications in this program are as follows: (i) change in user interface, addition of new tabs, modification of contracts addresses, removal of references to name of the original protocol and inclusion of DeFi Swap SDK in the interface; (ii) change in user interface and references to Crypto.com subgraphs in the analytics; (iii) change of the allowed token list; (iv) renaming and continuous improvement or deployment script changes; (v) polishing of unit test; and (vi) making fees configurable. 7 | This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 8 | You should have received a copy of the GNU General Public License along with this program. If not, see . 9 | Contact: contact@crypto.com 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DeFi Swap 2 | 3 | ## Local Development 4 | 5 | The following assumes the use of `node@>=10`. 6 | 7 | ## Install Dependencies 8 | 9 | `yarn` 10 | 11 | ## Compile Contracts 12 | 13 | `yarn compile` 14 | 15 | ## Run Tests 16 | 17 | `yarn test` 18 | 19 | ## Steps to deploy 20 | `yarn compile` 21 | `yarn truffle-compile` 22 | `yarn replace-factory` 23 | `yarn truffle-migrate` OR `yarn truffle-migrate-ropsten` 24 | -------------------------------------------------------------------------------- /contracts/CroDefiSwapERC20.sol: -------------------------------------------------------------------------------- 1 | pragma solidity =0.5.16; 2 | 3 | import './interfaces/ICroDefiSwapERC20.sol'; 4 | import './libraries/SafeMath.sol'; 5 | 6 | contract CroDefiSwapERC20 is ICroDefiSwapERC20 { 7 | using SafeMath for uint; 8 | 9 | string public constant name = 'CRO Defi Swap'; 10 | string public constant symbol = 'CRO-SWAP'; 11 | uint8 public constant decimals = 18; 12 | uint public totalSupply; 13 | mapping(address => uint) public balanceOf; 14 | mapping(address => mapping(address => uint)) public allowance; 15 | 16 | bytes32 public DOMAIN_SEPARATOR; 17 | // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); 18 | bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9; 19 | mapping(address => uint) public nonces; 20 | 21 | event Approval(address indexed owner, address indexed spender, uint value); 22 | event Transfer(address indexed from, address indexed to, uint value); 23 | 24 | constructor() public { 25 | uint chainId; 26 | assembly { 27 | chainId := chainid 28 | } 29 | DOMAIN_SEPARATOR = keccak256( 30 | abi.encode( 31 | keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'), 32 | keccak256(bytes(name)), 33 | keccak256(bytes('1')), 34 | chainId, 35 | address(this) 36 | ) 37 | ); 38 | } 39 | 40 | function _mint(address to, uint value) internal { 41 | totalSupply = totalSupply.add(value); 42 | balanceOf[to] = balanceOf[to].add(value); 43 | emit Transfer(address(0), to, value); 44 | } 45 | 46 | function _burn(address from, uint value) internal { 47 | balanceOf[from] = balanceOf[from].sub(value); 48 | totalSupply = totalSupply.sub(value); 49 | emit Transfer(from, address(0), value); 50 | } 51 | 52 | function _approve(address owner, address spender, uint value) private { 53 | allowance[owner][spender] = value; 54 | emit Approval(owner, spender, value); 55 | } 56 | 57 | function _transfer(address from, address to, uint value) private { 58 | balanceOf[from] = balanceOf[from].sub(value); 59 | balanceOf[to] = balanceOf[to].add(value); 60 | emit Transfer(from, to, value); 61 | } 62 | 63 | function approve(address spender, uint value) external returns (bool) { 64 | _approve(msg.sender, spender, value); 65 | return true; 66 | } 67 | 68 | function transfer(address to, uint value) external returns (bool) { 69 | _transfer(msg.sender, to, value); 70 | return true; 71 | } 72 | 73 | function transferFrom(address from, address to, uint value) external returns (bool) { 74 | if (allowance[from][msg.sender] != uint(-1)) { 75 | allowance[from][msg.sender] = allowance[from][msg.sender].sub(value); 76 | } 77 | _transfer(from, to, value); 78 | return true; 79 | } 80 | 81 | function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external { 82 | require(deadline >= block.timestamp, 'CroDefiSwap: EXPIRED'); 83 | bytes32 digest = keccak256( 84 | abi.encodePacked( 85 | '\x19\x01', 86 | DOMAIN_SEPARATOR, 87 | keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline)) 88 | ) 89 | ); 90 | address recoveredAddress = ecrecover(digest, v, r, s); 91 | require(recoveredAddress != address(0) && recoveredAddress == owner, 'CroDefiSwap: INVALID_SIGNATURE'); 92 | _approve(owner, spender, value); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /contracts/CroDefiSwapFactory.sol: -------------------------------------------------------------------------------- 1 | pragma solidity =0.5.16; 2 | 3 | import './interfaces/ICroDefiSwapFactory.sol'; 4 | import './CroDefiSwapPair.sol'; 5 | 6 | contract CroDefiSwapFactory is ICroDefiSwapFactory { 7 | address public feeTo; 8 | address public feeSetter; 9 | uint public feeToBasisPoint; 10 | uint public totalFeeBasisPoint; 11 | uint public MAX_EVER_ALLOWED_TOTAL_FEE_BASIS_POINT = 50; // 50 basis points 12 | 13 | mapping(address => mapping(address => address)) public getPair; 14 | address[] public allPairs; 15 | 16 | event PairCreated(address indexed token0, address indexed token1, address pair, uint); 17 | 18 | constructor(address _feeSetter, uint _totalFeeBasisPoint, uint _feeToBasisPoint) public { 19 | require(_totalFeeBasisPoint>=_feeToBasisPoint, '_totalFeeBasisPoint should be larger than or equal to _feeToBasisPoint'); 20 | feeSetter = _feeSetter; 21 | totalFeeBasisPoint = _totalFeeBasisPoint; 22 | feeToBasisPoint = _feeToBasisPoint; 23 | } 24 | 25 | function allPairsLength() external view returns (uint) { 26 | return allPairs.length; 27 | } 28 | 29 | function createPair(address tokenA, address tokenB) external returns (address pair) { 30 | require(tokenA != tokenB, 'CroDefiSwap: IDENTICAL_ADDRESSES'); 31 | (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); 32 | require(token0 != address(0), 'CroDefiSwap: ZERO_ADDRESS'); 33 | require(getPair[token0][token1] == address(0), 'CroDefiSwap: PAIR_EXISTS'); // single check is sufficient 34 | bytes memory bytecode = type(CroDefiSwapPair).creationCode; 35 | bytes32 salt = keccak256(abi.encodePacked(token0, token1)); 36 | assembly { 37 | pair := create2(0, add(bytecode, 32), mload(bytecode), salt) 38 | } 39 | ICroDefiSwapPair(pair).initialize(token0, token1); 40 | getPair[token0][token1] = pair; 41 | getPair[token1][token0] = pair; // populate mapping in the reverse direction 42 | allPairs.push(pair); 43 | emit PairCreated(token0, token1, pair, allPairs.length); 44 | } 45 | 46 | function setFeeTo(address _feeTo) external { 47 | require(msg.sender == feeSetter, 'CroDefiSwap: FORBIDDEN - only current feeSetter can update feeTo'); 48 | address previousFeeTo = feeTo; 49 | feeTo = _feeTo; 50 | emit FeeToUpdated(feeTo, previousFeeTo); 51 | } 52 | 53 | function setFeeSetter(address _feeSetter) external { 54 | require(msg.sender == feeSetter, 'CroDefiSwap: FORBIDDEN - only current feeSetter can update next feeSetter'); 55 | address previousFeeSetter = feeSetter; 56 | feeSetter = _feeSetter; 57 | emit FeeSetterUpdated(feeSetter, previousFeeSetter); 58 | } 59 | 60 | function setFeeToBasisPoint(uint _feeToBasisPoint) external { 61 | require(msg.sender == feeSetter, 'CroDefiSwap: FORBIDDEN - only current feeSetter can update feeToBasisPoint'); 62 | require(_feeToBasisPoint >= 0, 'CroDefiSwap: FORBIDDEN - _feeToBasisPoint need to be bigger than or equal to 0'); 63 | require(_feeToBasisPoint <= totalFeeBasisPoint, 'CroDefiSwap: FORBIDDEN - _feeToBasisPoint need to be smaller than or equal to totalFeeBasisPoint'); 64 | uint previousFeeToBasisPoint = feeToBasisPoint; 65 | feeToBasisPoint = _feeToBasisPoint; 66 | emit FeeToBasisPointUpdated(feeToBasisPoint, previousFeeToBasisPoint); 67 | } 68 | 69 | function setTotalFeeBasisPoint(uint _totalFeeBasisPoint) external { 70 | require(msg.sender == feeSetter, 'CroDefiSwap: FORBIDDEN - only current feeSetter can update feeToBasisPoint'); 71 | require(_totalFeeBasisPoint >= feeToBasisPoint, 'CroDefiSwap: FORBIDDEN - _totalFeeBasisPoint need to be bigger than or equal to feeToBasisPoint'); 72 | require(_totalFeeBasisPoint <= MAX_EVER_ALLOWED_TOTAL_FEE_BASIS_POINT, 'CroDefiSwap: FORBIDDEN - _totalFeeBasisPoint could never go beyond MAX_EVER_ALLOWED_TOTAL_FEE_BASIS_POINT'); 73 | uint previousTotalFeeBasisPoint = totalFeeBasisPoint; 74 | totalFeeBasisPoint = _totalFeeBasisPoint; 75 | emit TotalFeeBasisPointUpdated(totalFeeBasisPoint, previousTotalFeeBasisPoint); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /contracts/CroDefiSwapPair.sol: -------------------------------------------------------------------------------- 1 | pragma solidity =0.5.16; 2 | 3 | import './interfaces/ICroDefiSwapPair.sol'; 4 | import './CroDefiSwapERC20.sol'; 5 | import './libraries/Math.sol'; 6 | import './libraries/UQ112x112.sol'; 7 | import './interfaces/IERC20.sol'; 8 | import './interfaces/ICroDefiSwapFactory.sol'; 9 | import './interfaces/ICroDefiSwapCallee.sol'; 10 | 11 | contract CroDefiSwapPair is ICroDefiSwapPair, CroDefiSwapERC20 { 12 | using SafeMath for uint; 13 | using UQ112x112 for uint224; 14 | 15 | uint public constant MINIMUM_LIQUIDITY = 10**3; 16 | bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)'))); 17 | 18 | address public factory; 19 | address public token0; 20 | address public token1; 21 | 22 | uint112 private reserve0; // uses single storage slot, accessible via getReserves 23 | uint112 private reserve1; // uses single storage slot, accessible via getReserves 24 | uint32 private blockTimestampLast; // uses single storage slot, accessible via getReserves 25 | 26 | uint public price0CumulativeLast; 27 | uint public price1CumulativeLast; 28 | uint public kLast; // reserve0 * reserve1, as of immediately after the most recent liquidity event 29 | 30 | uint private unlocked = 1; 31 | modifier lock() { 32 | require(unlocked == 1, 'CroDefiSwap: LOCKED'); 33 | unlocked = 0; 34 | _; 35 | unlocked = 1; 36 | } 37 | 38 | function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) { 39 | _reserve0 = reserve0; 40 | _reserve1 = reserve1; 41 | _blockTimestampLast = blockTimestampLast; 42 | } 43 | 44 | function _safeTransfer(address token, address to, uint value) private { 45 | (bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value)); 46 | require(success && (data.length == 0 || abi.decode(data, (bool))), 'CroDefiSwap: TRANSFER_FAILED'); 47 | } 48 | 49 | event Mint(address indexed sender, uint amount0, uint amount1); 50 | event Burn(address indexed sender, uint amount0, uint amount1, address indexed to); 51 | event Swap( 52 | address indexed sender, 53 | uint amount0In, 54 | uint amount1In, 55 | uint amount0Out, 56 | uint amount1Out, 57 | address indexed to 58 | ); 59 | event Sync(uint112 reserve0, uint112 reserve1); 60 | 61 | constructor() public { 62 | factory = msg.sender; 63 | } 64 | 65 | // called once by the factory at time of deployment 66 | function initialize(address _token0, address _token1) external { 67 | require(msg.sender == factory, 'CroDefiSwap: FORBIDDEN'); // sufficient check 68 | token0 = _token0; 69 | token1 = _token1; 70 | } 71 | 72 | // update reserves and, on the first call per block, price accumulators 73 | function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private { 74 | require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'CroDefiSwap: OVERFLOW'); 75 | uint32 blockTimestamp = uint32(block.timestamp % 2**32); 76 | uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired 77 | if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) { 78 | // * never overflows, and + overflow is desired 79 | price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed; 80 | price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed; 81 | } 82 | reserve0 = uint112(balance0); 83 | reserve1 = uint112(balance1); 84 | blockTimestampLast = blockTimestamp; 85 | emit Sync(reserve0, reserve1); 86 | } 87 | 88 | // if fee is on, mint liquidity equivalent to 1/6th of the growth in sqrt(k) 89 | function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) { 90 | address feeTo = ICroDefiSwapFactory(factory).feeTo(); 91 | uint feeToBasisPoint = ICroDefiSwapFactory(factory).feeToBasisPoint(); 92 | 93 | feeOn = (feeTo != address(0)) && (feeToBasisPoint > 0); 94 | uint _kLast = kLast; // gas savings 95 | if (feeOn) { 96 | if (_kLast != 0) { 97 | uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1)); 98 | uint rootKLast = Math.sqrt(_kLast); 99 | if (rootK > rootKLast) { 100 | uint numerator = totalSupply.mul(rootK.sub(rootKLast)); 101 | uint denominator = rootK.mul(feeToBasisPoint).add(rootKLast); 102 | uint liquidity = numerator / denominator; 103 | if (liquidity > 0) _mint(feeTo, liquidity); 104 | } 105 | } 106 | } else if (_kLast != 0) { 107 | kLast = 0; 108 | } 109 | } 110 | 111 | // this low-level function should be called from a contract which performs important safety checks 112 | function mint(address to) external lock returns (uint liquidity) { 113 | (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings 114 | uint balance0 = IERC20(token0).balanceOf(address(this)); 115 | uint balance1 = IERC20(token1).balanceOf(address(this)); 116 | uint amount0 = balance0.sub(_reserve0); 117 | uint amount1 = balance1.sub(_reserve1); 118 | 119 | bool feeOn = _mintFee(_reserve0, _reserve1); 120 | uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee 121 | if (_totalSupply == 0) { 122 | liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY); 123 | _mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens 124 | } else { 125 | liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1); 126 | } 127 | require(liquidity > 0, 'CroDefiSwap: INSUFFICIENT_LIQUIDITY_MINTED'); 128 | _mint(to, liquidity); 129 | 130 | _update(balance0, balance1, _reserve0, _reserve1); 131 | if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date 132 | emit Mint(msg.sender, amount0, amount1); 133 | } 134 | 135 | // this low-level function should be called from a contract which performs important safety checks 136 | function burn(address to) external lock returns (uint amount0, uint amount1) { 137 | (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings 138 | address _token0 = token0; // gas savings 139 | address _token1 = token1; // gas savings 140 | uint balance0 = IERC20(_token0).balanceOf(address(this)); 141 | uint balance1 = IERC20(_token1).balanceOf(address(this)); 142 | uint liquidity = balanceOf[address(this)]; 143 | 144 | bool feeOn = _mintFee(_reserve0, _reserve1); 145 | uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee 146 | amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution 147 | amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution 148 | require(amount0 > 0 && amount1 > 0, 'CroDefiSwap: INSUFFICIENT_LIQUIDITY_BURNED'); 149 | _burn(address(this), liquidity); 150 | _safeTransfer(_token0, to, amount0); 151 | _safeTransfer(_token1, to, amount1); 152 | balance0 = IERC20(_token0).balanceOf(address(this)); 153 | balance1 = IERC20(_token1).balanceOf(address(this)); 154 | 155 | _update(balance0, balance1, _reserve0, _reserve1); 156 | if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date 157 | emit Burn(msg.sender, amount0, amount1, to); 158 | } 159 | 160 | // this low-level function should be called from a contract which performs important safety checks 161 | function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock { 162 | require(amount0Out > 0 || amount1Out > 0, 'CroDefiSwap: INSUFFICIENT_OUTPUT_AMOUNT'); 163 | (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings 164 | require(amount0Out < _reserve0 && amount1Out < _reserve1, 'CroDefiSwap: INSUFFICIENT_LIQUIDITY'); 165 | 166 | uint balance0; 167 | uint balance1; 168 | { // scope for _token{0,1}, avoids stack too deep errors 169 | address _token0 = token0; 170 | address _token1 = token1; 171 | require(to != _token0 && to != _token1, 'CroDefiSwap: INVALID_TO'); 172 | if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens 173 | if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens 174 | if (data.length > 0) ICroDefiSwapCallee(to).croDefiSwapCall(msg.sender, amount0Out, amount1Out, data); 175 | balance0 = IERC20(_token0).balanceOf(address(this)); 176 | balance1 = IERC20(_token1).balanceOf(address(this)); 177 | } 178 | uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0; 179 | uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0; 180 | require(amount0In > 0 || amount1In > 0, 'CroDefiSwap: INSUFFICIENT_INPUT_AMOUNT'); 181 | { // scope for reserve{0,1}Adjusted, avoids stack too deep errors 182 | uint magnifier = 10000; 183 | uint totalFeeBasisPoint = ICroDefiSwapFactory(factory).totalFeeBasisPoint(); 184 | uint balance0Adjusted = balance0.mul(magnifier).sub(amount0In.mul(totalFeeBasisPoint)); 185 | uint balance1Adjusted = balance1.mul(magnifier).sub(amount1In.mul(totalFeeBasisPoint)); 186 | // reference: https://uniswap.org/docs/v2/protocol-overview/glossary/#constant-product-formula 187 | require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(magnifier**2), 'CroDefiSwap: Constant product formula condition not met!'); 188 | } 189 | 190 | _update(balance0, balance1, _reserve0, _reserve1); 191 | emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to); 192 | } 193 | 194 | // force balances to match reserves 195 | function skim(address to) external lock { 196 | address _token0 = token0; // gas savings 197 | address _token1 = token1; // gas savings 198 | _safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0)); 199 | _safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1)); 200 | } 201 | 202 | // force reserves to match balances 203 | function sync() external lock { 204 | _update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.4.25 <0.7.0; 3 | 4 | contract Migrations { 5 | address public owner; 6 | uint public last_completed_migration; 7 | 8 | modifier restricted() { 9 | if (msg.sender == owner) _; 10 | } 11 | 12 | constructor() public { 13 | owner = msg.sender; 14 | } 15 | 16 | function setCompleted(uint completed) public restricted { 17 | last_completed_migration = completed; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /contracts/interfaces/ICroDefiSwapCallee.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.5.0; 2 | 3 | interface ICroDefiSwapCallee { 4 | function croDefiSwapCall(address sender, uint amount0, uint amount1, bytes calldata data) external; 5 | } 6 | -------------------------------------------------------------------------------- /contracts/interfaces/ICroDefiSwapERC20.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.5.0; 2 | 3 | interface ICroDefiSwapERC20 { 4 | event Approval(address indexed owner, address indexed spender, uint value); 5 | event Transfer(address indexed from, address indexed to, uint value); 6 | 7 | function name() external pure returns (string memory); 8 | function symbol() external pure returns (string memory); 9 | function decimals() external pure returns (uint8); 10 | function totalSupply() external view returns (uint); 11 | function balanceOf(address owner) external view returns (uint); 12 | function allowance(address owner, address spender) external view returns (uint); 13 | 14 | function approve(address spender, uint value) external returns (bool); 15 | function transfer(address to, uint value) external returns (bool); 16 | function transferFrom(address from, address to, uint value) external returns (bool); 17 | 18 | function DOMAIN_SEPARATOR() external view returns (bytes32); 19 | function PERMIT_TYPEHASH() external pure returns (bytes32); 20 | function nonces(address owner) external view returns (uint); 21 | 22 | function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external; 23 | } 24 | -------------------------------------------------------------------------------- /contracts/interfaces/ICroDefiSwapFactory.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.5.0; 2 | 3 | interface ICroDefiSwapFactory { 4 | event PairCreated(address indexed token0, address indexed token1, address pair, uint); 5 | event FeeSetterUpdated(address latestFeeSetter, address previousFeeSetter); 6 | event FeeToUpdated(address latestFeeTo, address previousFeeTo); 7 | event FeeToBasisPointUpdated(uint latestFeeToBasisPoint, uint previousFeeToBasisPoint); 8 | event TotalFeeBasisPointUpdated(uint latestTotalFeeBasisPoint, uint previousTotalFeeBasisPoint); 9 | 10 | function feeTo() external view returns (address); 11 | function feeToBasisPoint() external view returns (uint); 12 | 13 | // technically must be bigger than or equal to feeToBasisPoint 14 | function totalFeeBasisPoint() external view returns (uint); 15 | 16 | function feeSetter() external view returns (address); 17 | 18 | function getPair(address tokenA, address tokenB) external view returns (address pair); 19 | function allPairs(uint) external view returns (address pair); 20 | function allPairsLength() external view returns (uint); 21 | 22 | function createPair(address tokenA, address tokenB) external returns (address pair); 23 | 24 | function setFeeTo(address) external; 25 | function setFeeToBasisPoint(uint) external; 26 | function setTotalFeeBasisPoint(uint) external; 27 | 28 | function setFeeSetter(address) external; 29 | } 30 | -------------------------------------------------------------------------------- /contracts/interfaces/ICroDefiSwapPair.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.5.0; 2 | 3 | interface ICroDefiSwapPair { 4 | event Approval(address indexed owner, address indexed spender, uint value); 5 | event Transfer(address indexed from, address indexed to, uint value); 6 | 7 | function name() external pure returns (string memory); 8 | function symbol() external pure returns (string memory); 9 | function decimals() external pure returns (uint8); 10 | function totalSupply() external view returns (uint); 11 | function balanceOf(address owner) external view returns (uint); 12 | function allowance(address owner, address spender) external view returns (uint); 13 | 14 | function approve(address spender, uint value) external returns (bool); 15 | function transfer(address to, uint value) external returns (bool); 16 | function transferFrom(address from, address to, uint value) external returns (bool); 17 | 18 | function DOMAIN_SEPARATOR() external view returns (bytes32); 19 | function PERMIT_TYPEHASH() external pure returns (bytes32); 20 | function nonces(address owner) external view returns (uint); 21 | 22 | function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external; 23 | 24 | event Mint(address indexed sender, uint amount0, uint amount1); 25 | event Burn(address indexed sender, uint amount0, uint amount1, address indexed to); 26 | event Swap( 27 | address indexed sender, 28 | uint amount0In, 29 | uint amount1In, 30 | uint amount0Out, 31 | uint amount1Out, 32 | address indexed to 33 | ); 34 | event Sync(uint112 reserve0, uint112 reserve1); 35 | 36 | function MINIMUM_LIQUIDITY() external pure returns (uint); 37 | function factory() external view returns (address); 38 | function token0() external view returns (address); 39 | function token1() external view returns (address); 40 | function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast); 41 | function price0CumulativeLast() external view returns (uint); 42 | function price1CumulativeLast() external view returns (uint); 43 | function kLast() external view returns (uint); 44 | 45 | function mint(address to) external returns (uint liquidity); 46 | function burn(address to) external returns (uint amount0, uint amount1); 47 | function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external; 48 | function skim(address to) external; 49 | function sync() external; 50 | 51 | function initialize(address, address) external; 52 | } 53 | -------------------------------------------------------------------------------- /contracts/interfaces/IERC20.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.5.0; 2 | 3 | interface IERC20 { 4 | event Approval(address indexed owner, address indexed spender, uint value); 5 | event Transfer(address indexed from, address indexed to, uint value); 6 | 7 | function name() external view returns (string memory); 8 | function symbol() external view returns (string memory); 9 | function decimals() external view returns (uint8); 10 | function totalSupply() external view returns (uint); 11 | function balanceOf(address owner) external view returns (uint); 12 | function allowance(address owner, address spender) external view returns (uint); 13 | 14 | function approve(address spender, uint value) external returns (bool); 15 | function transfer(address to, uint value) external returns (bool); 16 | function transferFrom(address from, address to, uint value) external returns (bool); 17 | } 18 | -------------------------------------------------------------------------------- /contracts/libraries/Math.sol: -------------------------------------------------------------------------------- 1 | pragma solidity =0.5.16; 2 | 3 | // a library for performing various math operations 4 | 5 | library Math { 6 | function min(uint x, uint y) internal pure returns (uint z) { 7 | z = x < y ? x : y; 8 | } 9 | 10 | // babylonian method (https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method) 11 | function sqrt(uint y) internal pure returns (uint z) { 12 | if (y > 3) { 13 | z = y; 14 | uint x = y / 2 + 1; 15 | while (x < z) { 16 | z = x; 17 | x = (y / x + x) / 2; 18 | } 19 | } else if (y != 0) { 20 | z = 1; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /contracts/libraries/SafeMath.sol: -------------------------------------------------------------------------------- 1 | pragma solidity =0.5.16; 2 | 3 | // a library for performing overflow-safe math, courtesy of DappHub (https://github.com/dapphub/ds-math) 4 | 5 | library SafeMath { 6 | function add(uint x, uint y) internal pure returns (uint z) { 7 | require((z = x + y) >= x, 'ds-math-add-overflow'); 8 | } 9 | 10 | function sub(uint x, uint y) internal pure returns (uint z) { 11 | require((z = x - y) <= x, 'ds-math-sub-underflow'); 12 | } 13 | 14 | function mul(uint x, uint y) internal pure returns (uint z) { 15 | require(y == 0 || (z = x * y) / y == x, 'ds-math-mul-overflow'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /contracts/libraries/UQ112x112.sol: -------------------------------------------------------------------------------- 1 | pragma solidity =0.5.16; 2 | 3 | // a library for handling binary fixed point numbers (https://en.wikipedia.org/wiki/Q_(number_format)) 4 | 5 | // range: [0, 2**112 - 1] 6 | // resolution: 1 / 2**112 7 | 8 | library UQ112x112 { 9 | uint224 constant Q112 = 2**112; 10 | 11 | // encode a uint112 as a UQ112x112 12 | function encode(uint112 y) internal pure returns (uint224 z) { 13 | z = uint224(y) * Q112; // never overflows 14 | } 15 | 16 | // divide a UQ112x112 by a uint112, returning a UQ112x112 17 | function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) { 18 | z = x / uint224(y); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /contracts/test/ERC20.sol: -------------------------------------------------------------------------------- 1 | pragma solidity =0.5.16; 2 | 3 | import '../CroDefiSwapERC20.sol'; 4 | 5 | contract ERC20 is CroDefiSwapERC20 { 6 | constructor(uint _totalSupply) public { 7 | _mint(msg.sender, _totalSupply); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | const Migrations = artifacts.require("Migrations"); 2 | 3 | module.exports = function(deployer) { 4 | deployer.deploy(Migrations); 5 | }; 6 | -------------------------------------------------------------------------------- /migrations/2_deploy_contracts.js: -------------------------------------------------------------------------------- 1 | const CroDefiSwapFactory = artifacts.require('CroDefiSwapFactory') 2 | 3 | module.exports = async function(deployer, network, accounts) { 4 | let feeSetter 5 | if (network === 'mainnet') { 6 | feeSetter = '0x3459e5cb6be361b4f52dA94173Dc8d216013C57a' 7 | } else { 8 | feeSetter = accounts[0].toString() 9 | } 10 | await deployer.deploy(CroDefiSwapFactory, feeSetter, 30, 5) 11 | console.log(`Deployed CroDefiSwapFactory on network ${network} with ${feeSetter} as feeSetter`) 12 | } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@crypto-com/swap-contracts-core", 3 | "description": "🎛 Core contracts for the CroDefiSwap protocol", 4 | "version": "0.0.1", 5 | "homepage": "https://crypto.com/defi/swap", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/crypto-com/swap-contracts-core" 9 | }, 10 | "keywords": [ 11 | "DeFiSwap", 12 | "ethereum", 13 | "core", 14 | "crypto.com", 15 | "DEcentralised FInance", 16 | "DEFI", 17 | "Automated Market Making", 18 | "AMM", 19 | "DEcentralised eXchange", 20 | "DEX", 21 | "Liquity Mining" 22 | ], 23 | "files": [ 24 | "contracts", 25 | "build" 26 | ], 27 | "engines": { 28 | "node": ">=10" 29 | }, 30 | "license": "GPL-3.0-or-later", 31 | "scripts": { 32 | "lint": "yarn prettier ./test/*.ts --check", 33 | "lint:fix": "yarn prettier ./test/*.ts --write", 34 | "clean": "rimraf ./build/ && rimraf ./cache/", 35 | "precompile": "yarn clean", 36 | "compile": "waffle .waffle.json", 37 | "pretest": "yarn compile", 38 | "test": "mocha", 39 | "prepublishOnly": "yarn test", 40 | "truffle-compile": "./node_modules/.bin/truffle compile", 41 | "truffle-migrate": "./node_modules/.bin/truffle migrate --reset;", 42 | "truffle-migrate-ropsten": "./node_modules/.bin/truffle migrate --reset --network ropsten", 43 | "truffle-migrate-mainnet": "./node_modules/.bin/truffle migrate --reset --network mainnet", 44 | "replace-factory": "node ./scripts/replaceFactory.js" 45 | }, 46 | "dependencies": { 47 | "truffle": "5.1.42" 48 | }, 49 | "devDependencies": { 50 | "@truffle/hdwallet-provider": "1.0.43", 51 | "@types/chai": "4.2.6", 52 | "@types/mocha": "5.2.7", 53 | "chai": "4.2.0", 54 | "dotenv": "8.2.0", 55 | "ethereum-waffle": "2.4.1", 56 | "ethereumjs-util": "6.2.0", 57 | "git-hooks": "1.1.10", 58 | "mocha": "6.2.2", 59 | "prettier": "1.19.1", 60 | "rimraf": "3.0.0", 61 | "solc": "0.5.16", 62 | "ts-node": "8.5.4", 63 | "typescript": "3.7.3" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /pull_request_template.md: -------------------------------------------------------------------------------- 1 | - [ ] why is this PR needed? / why does it matter? 2 | - [ ] does this PR contain breaking change? 3 | - [ ] YES 4 | - [ ] NO 5 | - [ ] if YES, please point out (by hyperlink) the breaking change. 6 | - [ ] does this PR block any other PRs? 7 | - [ ] YES 8 | - [ ] NO 9 | - [ ] if YES, please point out (by hyperlink) the blocked PRs. 10 | - [ ] is this PR blocked by any other PRs? 11 | - [ ] YES 12 | - [ ] NO 13 | - [ ] if YES, please point out (by hyperlink) the blocker PRs. 14 | - [ ] is this PR independently releasable as long as it passes review? 15 | - [ ] YES 16 | - [ ] NO 17 | - [ ] if NO, please highlight the dependencies 18 | -------------------------------------------------------------------------------- /scripts/replaceFactory.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | 3 | const croDefiSwapFactoryFromTruffle = JSON.parse(fs.readFileSync("./build/contracts/CroDefiSwapFactory.json", "utf8")); 4 | const croDefiSwapFactoryFromWaffle = JSON.parse(fs.readFileSync("./build/CroDefiSwapFactory.json", "utf8")); 5 | croDefiSwapFactoryFromTruffle.bytecode = croDefiSwapFactoryFromWaffle.bytecode 6 | fs.writeFileSync("./build/contracts/CroDefiSwapFactory.json", JSON.stringify(croDefiSwapFactoryFromTruffle), "utf8"); 7 | 8 | console.log("[ReplaceFactory] CroDefiSwapFactory Truffle now using bytecode compiled from Waffle!") 9 | -------------------------------------------------------------------------------- /test/CropSwapERC20.spec.ts: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai' 2 | import { Contract } from 'ethers' 3 | import { MaxUint256 } from 'ethers/constants' 4 | import { bigNumberify, hexlify, keccak256, defaultAbiCoder, toUtf8Bytes } from 'ethers/utils' 5 | import { solidity, MockProvider, deployContract } from 'ethereum-waffle' 6 | import { ecsign } from 'ethereumjs-util' 7 | 8 | import { expandTo18Decimals, getApprovalDigest } from './shared/utilities' 9 | 10 | import ERC20 from '../build/ERC20.json' 11 | 12 | chai.use(solidity) 13 | 14 | const TOTAL_SUPPLY = expandTo18Decimals(10000) 15 | const TEST_AMOUNT = expandTo18Decimals(10) 16 | 17 | describe('CroDefiSwapERC20', () => { 18 | const provider = new MockProvider({ 19 | hardfork: 'istanbul', 20 | mnemonic: 'horn horn horn horn horn horn horn horn horn horn horn horn', 21 | gasLimit: 9999999 22 | }) 23 | const [wallet, other] = provider.getWallets() 24 | 25 | let token: Contract 26 | beforeEach(async () => { 27 | token = await deployContract(wallet, ERC20, [TOTAL_SUPPLY]) 28 | }) 29 | 30 | it('name, symbol, decimals, totalSupply, balanceOf, DOMAIN_SEPARATOR, PERMIT_TYPEHASH', async () => { 31 | const name = await token.name() 32 | expect(name).to.eq('CRO Defi Swap') 33 | expect(await token.symbol()).to.eq('CRO-SWAP') 34 | expect(await token.decimals()).to.eq(18) 35 | expect(await token.totalSupply()).to.eq(TOTAL_SUPPLY) 36 | expect(await token.balanceOf(wallet.address)).to.eq(TOTAL_SUPPLY) 37 | expect(await token.DOMAIN_SEPARATOR()).to.eq( 38 | keccak256( 39 | defaultAbiCoder.encode( 40 | ['bytes32', 'bytes32', 'bytes32', 'uint256', 'address'], 41 | [ 42 | keccak256( 43 | toUtf8Bytes('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)') 44 | ), 45 | keccak256(toUtf8Bytes(name)), 46 | keccak256(toUtf8Bytes('1')), 47 | 1, 48 | token.address 49 | ] 50 | ) 51 | ) 52 | ) 53 | expect(await token.PERMIT_TYPEHASH()).to.eq( 54 | keccak256(toUtf8Bytes('Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)')) 55 | ) 56 | }) 57 | 58 | it('approve', async () => { 59 | await expect(token.approve(other.address, TEST_AMOUNT)) 60 | .to.emit(token, 'Approval') 61 | .withArgs(wallet.address, other.address, TEST_AMOUNT) 62 | expect(await token.allowance(wallet.address, other.address)).to.eq(TEST_AMOUNT) 63 | }) 64 | 65 | it('transfer', async () => { 66 | await expect(token.transfer(other.address, TEST_AMOUNT)) 67 | .to.emit(token, 'Transfer') 68 | .withArgs(wallet.address, other.address, TEST_AMOUNT) 69 | expect(await token.balanceOf(wallet.address)).to.eq(TOTAL_SUPPLY.sub(TEST_AMOUNT)) 70 | expect(await token.balanceOf(other.address)).to.eq(TEST_AMOUNT) 71 | }) 72 | 73 | it('transfer:fail', async () => { 74 | await expect(token.transfer(other.address, TOTAL_SUPPLY.add(1))).to.be.reverted // ds-math-sub-underflow 75 | await expect(token.connect(other).transfer(wallet.address, 1)).to.be.reverted // ds-math-sub-underflow 76 | }) 77 | 78 | it('transferFrom', async () => { 79 | await token.approve(other.address, TEST_AMOUNT) 80 | await expect(token.connect(other).transferFrom(wallet.address, other.address, TEST_AMOUNT)) 81 | .to.emit(token, 'Transfer') 82 | .withArgs(wallet.address, other.address, TEST_AMOUNT) 83 | expect(await token.allowance(wallet.address, other.address)).to.eq(0) 84 | expect(await token.balanceOf(wallet.address)).to.eq(TOTAL_SUPPLY.sub(TEST_AMOUNT)) 85 | expect(await token.balanceOf(other.address)).to.eq(TEST_AMOUNT) 86 | }) 87 | 88 | it('transferFrom:max', async () => { 89 | await token.approve(other.address, MaxUint256) 90 | await expect(token.connect(other).transferFrom(wallet.address, other.address, TEST_AMOUNT)) 91 | .to.emit(token, 'Transfer') 92 | .withArgs(wallet.address, other.address, TEST_AMOUNT) 93 | expect(await token.allowance(wallet.address, other.address)).to.eq(MaxUint256) 94 | expect(await token.balanceOf(wallet.address)).to.eq(TOTAL_SUPPLY.sub(TEST_AMOUNT)) 95 | expect(await token.balanceOf(other.address)).to.eq(TEST_AMOUNT) 96 | }) 97 | 98 | it('permit', async () => { 99 | const nonce = await token.nonces(wallet.address) 100 | const deadline = MaxUint256 101 | const digest = await getApprovalDigest( 102 | token, 103 | { owner: wallet.address, spender: other.address, value: TEST_AMOUNT }, 104 | nonce, 105 | deadline 106 | ) 107 | 108 | const { v, r, s } = ecsign(Buffer.from(digest.slice(2), 'hex'), Buffer.from(wallet.privateKey.slice(2), 'hex')) 109 | 110 | await expect(token.permit(wallet.address, other.address, TEST_AMOUNT, deadline, v, hexlify(r), hexlify(s))) 111 | .to.emit(token, 'Approval') 112 | .withArgs(wallet.address, other.address, TEST_AMOUNT) 113 | expect(await token.allowance(wallet.address, other.address)).to.eq(TEST_AMOUNT) 114 | expect(await token.nonces(wallet.address)).to.eq(bigNumberify(1)) 115 | }) 116 | }) 117 | -------------------------------------------------------------------------------- /test/CropSwapFactory.spec.ts: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai' 2 | import { Contract } from 'ethers' 3 | import { AddressZero } from 'ethers/constants' 4 | import { bigNumberify } from 'ethers/utils' 5 | import { solidity, MockProvider, createFixtureLoader } from 'ethereum-waffle' 6 | 7 | import { getCreate2Address } from './shared/utilities' 8 | import { factoryFixture } from './shared/fixtures' 9 | 10 | import CroDefiSwapPair from '../build/CroDefiSwapPair.json' 11 | 12 | chai.use(solidity) 13 | 14 | const TEST_ADDRESSES: [string, string] = [ 15 | '0x1000000000000000000000000000000000000000', 16 | '0x2000000000000000000000000000000000000000' 17 | ] 18 | 19 | describe('CroDefiSwapFactory', () => { 20 | const provider = new MockProvider({ 21 | hardfork: 'istanbul', 22 | mnemonic: 'horn horn horn horn horn horn horn horn horn horn horn horn', 23 | gasLimit: 9999999 24 | }) 25 | const [wallet, other] = provider.getWallets() 26 | const loadFixture = createFixtureLoader(provider, [wallet, other]) 27 | 28 | let factory: Contract 29 | beforeEach(async () => { 30 | const fixture = await loadFixture(factoryFixture) 31 | factory = fixture.factory 32 | }) 33 | 34 | it('feeTo, feeSetter, allPairsLength', async () => { 35 | expect(await factory.feeTo()).to.eq(AddressZero) 36 | expect(await factory.feeSetter()).to.eq(wallet.address) 37 | expect(await factory.allPairsLength()).to.eq(0) 38 | }) 39 | 40 | async function createPair(tokens: [string, string]) { 41 | const bytecode = `0x${CroDefiSwapPair.evm.bytecode.object}` 42 | const create2Address = getCreate2Address(factory.address, tokens, bytecode) 43 | await expect(factory.createPair(...tokens)) 44 | .to.emit(factory, 'PairCreated') 45 | .withArgs(TEST_ADDRESSES[0], TEST_ADDRESSES[1], create2Address, bigNumberify(1)) 46 | 47 | await expect(factory.createPair(...tokens)).to.be.reverted // CroDefiSwap: PAIR_EXISTS 48 | await expect(factory.createPair(...tokens.slice().reverse())).to.be.reverted // CroDefiSwap: PAIR_EXISTS 49 | expect(await factory.getPair(...tokens)).to.eq(create2Address) 50 | expect(await factory.getPair(...tokens.slice().reverse())).to.eq(create2Address) 51 | expect(await factory.allPairs(0)).to.eq(create2Address) 52 | expect(await factory.allPairsLength()).to.eq(1) 53 | 54 | const pair = new Contract(create2Address, JSON.stringify(CroDefiSwapPair.abi), provider) 55 | expect(await pair.factory()).to.eq(factory.address) 56 | expect(await pair.token0()).to.eq(TEST_ADDRESSES[0]) 57 | expect(await pair.token1()).to.eq(TEST_ADDRESSES[1]) 58 | } 59 | 60 | it('createPair', async () => { 61 | await createPair(TEST_ADDRESSES) 62 | }) 63 | 64 | it('createPair:reverse', async () => { 65 | await createPair(TEST_ADDRESSES.slice().reverse() as [string, string]) 66 | }) 67 | 68 | it('createPair:gas', async () => { 69 | const tx = await factory.createPair(...TEST_ADDRESSES) 70 | const receipt = await tx.wait() 71 | expect(receipt.gasUsed).to.eq(2590571) 72 | }) 73 | 74 | it('setFeeTo', async () => { 75 | await expect(factory.connect(other).setFeeTo(other.address)).to.be.revertedWith('CroDefiSwap: FORBIDDEN') 76 | await factory.setFeeTo(wallet.address) 77 | expect(await factory.feeTo()).to.eq(wallet.address) 78 | }) 79 | 80 | it('setFeeSetter', async () => { 81 | await expect(factory.connect(other).setFeeSetter(other.address)).to.be.revertedWith('CroDefiSwap: FORBIDDEN') 82 | await factory.setFeeSetter(other.address) 83 | expect(await factory.feeSetter()).to.eq(other.address) 84 | await expect(factory.setFeeSetter(wallet.address)).to.be.revertedWith('CroDefiSwap: FORBIDDEN') 85 | }) 86 | }) 87 | -------------------------------------------------------------------------------- /test/CropSwapPair.spec.ts: -------------------------------------------------------------------------------- 1 | import chai, { expect } from 'chai' 2 | import { Contract } from 'ethers' 3 | import { solidity, MockProvider, createFixtureLoader } from 'ethereum-waffle' 4 | import { BigNumber, bigNumberify } from 'ethers/utils' 5 | 6 | import { expandTo18Decimals, mineBlock, encodePrice } from './shared/utilities' 7 | import { pairFixture } from './shared/fixtures' 8 | import { AddressZero } from 'ethers/constants' 9 | 10 | const MINIMUM_LIQUIDITY = bigNumberify(10).pow(3) 11 | 12 | chai.use(solidity) 13 | 14 | const overrides = { 15 | gasLimit: 9999999 16 | } 17 | 18 | describe('CroDefiSwapPair', () => { 19 | const provider = new MockProvider({ 20 | hardfork: 'istanbul', 21 | mnemonic: 'horn horn horn horn horn horn horn horn horn horn horn horn', 22 | gasLimit: 9999999 23 | }) 24 | const [defaultLiquidityProviderWallet, defaultFeeToWallet, defaultLiquidityTakerWallet] = provider.getWallets() 25 | const loadFixture = createFixtureLoader(provider, [defaultLiquidityProviderWallet, defaultLiquidityTakerWallet]) 26 | 27 | let factory: Contract 28 | let token0: Contract 29 | let token1: Contract 30 | let pair: Contract 31 | beforeEach(async () => { 32 | const fixture = await loadFixture(pairFixture) 33 | factory = fixture.factory 34 | token0 = fixture.token0 35 | token1 = fixture.token1 36 | pair = fixture.pair 37 | }) 38 | 39 | it('mint', async () => { 40 | const token0Amount = expandTo18Decimals(1) 41 | const token1Amount = expandTo18Decimals(4) 42 | // always transfer from defaultLiquidityProviderWallet because of `createFixtureLoader` overrideWallets setting 43 | await token0.transfer(pair.address, token0Amount) 44 | await token1.transfer(pair.address, token1Amount) 45 | 46 | const expectedLiquidityTokenAmount = expandTo18Decimals(2) 47 | await expect(pair.mint(defaultLiquidityProviderWallet.address, overrides)) 48 | .to.emit(pair, 'Transfer') 49 | .withArgs(AddressZero, AddressZero, MINIMUM_LIQUIDITY) 50 | .to.emit(pair, 'Transfer') 51 | .withArgs( 52 | AddressZero, 53 | defaultLiquidityProviderWallet.address, 54 | expectedLiquidityTokenAmount.sub(MINIMUM_LIQUIDITY) 55 | ) 56 | .to.emit(pair, 'Sync') 57 | .withArgs(token0Amount, token1Amount) 58 | .to.emit(pair, 'Mint') 59 | .withArgs(defaultLiquidityProviderWallet.address, token0Amount, token1Amount) 60 | 61 | expect(await pair.totalSupply()).to.eq(expectedLiquidityTokenAmount) 62 | expect(await pair.balanceOf(defaultLiquidityProviderWallet.address)).to.eq( 63 | expectedLiquidityTokenAmount.sub(MINIMUM_LIQUIDITY) 64 | ) 65 | expect(await token0.balanceOf(pair.address)).to.eq(token0Amount) 66 | expect(await token1.balanceOf(pair.address)).to.eq(token1Amount) 67 | // reserve amount equals this round supplied liquidity amount because only supplied liquidity once 68 | const reserves = await pair.getReserves() 69 | expect(reserves[0]).to.eq(token0Amount) 70 | expect(reserves[1]).to.eq(token1Amount) 71 | 72 | // provide liquidity one more time with the same amounts on each side of the pair 73 | await token0.transfer(pair.address, token0Amount) 74 | await token1.transfer(pair.address, token1Amount) 75 | await pair.mint(defaultLiquidityProviderWallet.address, overrides) 76 | const reservesAfterProvidingLiquidityTwice = await pair.getReserves() 77 | expect(reservesAfterProvidingLiquidityTwice[0]).to.eq(token0Amount.add(token0Amount)) 78 | expect(reservesAfterProvidingLiquidityTwice[1]).to.eq(token1Amount.add(token1Amount)) 79 | }) 80 | 81 | async function addLiquidity(token0Amount: BigNumber, token1Amount: BigNumber, liquidityProviderAddress: string) { 82 | await token0.transfer(pair.address, token0Amount) 83 | await token1.transfer(pair.address, token1Amount) 84 | await pair.mint(liquidityProviderAddress, overrides) 85 | } 86 | 87 | const swapTestCases: BigNumber[][] = [ 88 | [1, 5, 10, '1662497915624478906'], 89 | [1, 10, 5, '453305446940074565'], 90 | 91 | [2, 5, 10, '2851015155847869602'], 92 | [2, 10, 5, '831248957812239453'], 93 | 94 | [1, 10, 10, '906610893880149131'], 95 | [1, 100, 100, '987158034397061298'], 96 | [1, 1000, 1000, '996006981039903216'] 97 | ].map(a => a.map(n => (typeof n === 'string' ? bigNumberify(n) : expandTo18Decimals(n)))) 98 | 99 | swapTestCases.forEach((swapTestCase, i) => { 100 | it(`getInputPrice:${i}: should revert when constant product formula condition is not met`, async () => { 101 | const [swapAmountOfToken0, token0LiquidityAmount, token1LiquidityAmount, swapAmountOfToken1] = swapTestCase 102 | await addLiquidity(token0LiquidityAmount, token1LiquidityAmount, defaultLiquidityProviderWallet.address) 103 | await token0.transfer(pair.address, swapAmountOfToken0) 104 | await expect( 105 | pair.swap(0, swapAmountOfToken1.add(1), defaultLiquidityTakerWallet.address, '0x', overrides) 106 | ).to.be.revertedWith('CroDefiSwap: Constant product formula condition not met!') 107 | await pair.swap(0, swapAmountOfToken1, defaultLiquidityTakerWallet.address, '0x', overrides) 108 | }) 109 | }) 110 | 111 | // TODO this test is bound to change when fee ratio is configurable 112 | const optimisticTestCases: BigNumber[][] = [ 113 | ['997000000000000000', 5, 10, 1], // given amountIn, amountOut = floor(amountIn * .997) 114 | ['997000000000000000', 10, 5, 1], 115 | ['997000000000000000', 5, 5, 1], 116 | [1, 5, 5, '1003009027081243732'] // given amountOut, amountIn = ceiling(amountOut / .997) 117 | ].map(a => a.map(n => (typeof n === 'string' ? bigNumberify(n) : expandTo18Decimals(n)))) 118 | 119 | optimisticTestCases.forEach((optimisticTestCase, i) => { 120 | it(`optimistic:${i}: should revert when constant product formula condition is not met`, async () => { 121 | const [swapAmountOfToken0, token0LiquidityAmount, token1LiquidityAmount, inputAmount] = optimisticTestCase 122 | await addLiquidity(token0LiquidityAmount, token1LiquidityAmount, defaultLiquidityProviderWallet.address) 123 | await token0.transfer(pair.address, inputAmount) 124 | await expect( 125 | pair.swap(swapAmountOfToken0.add(1), 0, defaultLiquidityTakerWallet.address, '0x', overrides) 126 | ).to.be.revertedWith('CroDefiSwap: Constant product formula condition not met!') 127 | await pair.swap(swapAmountOfToken0, 0, defaultLiquidityTakerWallet.address, '0x', overrides) 128 | }) 129 | }) 130 | 131 | it('swap:token0 into pool, token1 out of pool to liquidity taker', async () => { 132 | const token0AdditionalLiquidityAmount = expandTo18Decimals(5) 133 | const token1AdditionalLiquidityAmount = expandTo18Decimals(10) 134 | await addLiquidity( 135 | token0AdditionalLiquidityAmount, 136 | token1AdditionalLiquidityAmount, 137 | defaultLiquidityProviderWallet.address 138 | ) 139 | 140 | const swapInAmountOfToken0 = expandTo18Decimals(1) 141 | const expectedSwapOutAmountOfToken1 = bigNumberify('1662497915624478906') 142 | await token0.transfer(pair.address, swapInAmountOfToken0) 143 | 144 | await expect(pair.swap(0, expectedSwapOutAmountOfToken1, defaultLiquidityTakerWallet.address, '0x', overrides)) 145 | .to.emit(token1, 'Transfer') 146 | .withArgs(pair.address, defaultLiquidityTakerWallet.address, expectedSwapOutAmountOfToken1) 147 | .to.emit(pair, 'Sync') 148 | .withArgs( 149 | token0AdditionalLiquidityAmount.add(swapInAmountOfToken0), 150 | token1AdditionalLiquidityAmount.sub(expectedSwapOutAmountOfToken1) 151 | ) 152 | .to.emit(pair, 'Swap') 153 | .withArgs( 154 | defaultLiquidityProviderWallet.address, 155 | swapInAmountOfToken0, 156 | 0, 157 | 0, 158 | expectedSwapOutAmountOfToken1, 159 | defaultLiquidityTakerWallet.address 160 | ) 161 | 162 | const reserves = await pair.getReserves() 163 | expect(reserves[0]).to.eq( 164 | token0AdditionalLiquidityAmount.add(swapInAmountOfToken0), 165 | 'token 0 liquidity reserve should increase' 166 | ) 167 | expect(reserves[1]).to.eq( 168 | token1AdditionalLiquidityAmount.sub(expectedSwapOutAmountOfToken1), 169 | 'token 1 liquidity reserve should decrease' 170 | ) 171 | 172 | const totalSupplyToken0 = await token0.totalSupply() 173 | const totalSupplyToken1 = await token1.totalSupply() 174 | 175 | expect(await token0.balanceOf(pair.address)).to.eq( 176 | token0AdditionalLiquidityAmount.add(swapInAmountOfToken0), 177 | 'token 0 balance of this pair contract should increase after this specific swap' 178 | ) 179 | expect(await token0.balanceOf(defaultLiquidityProviderWallet.address)).to.eq( 180 | totalSupplyToken0.sub(token0AdditionalLiquidityAmount).sub(swapInAmountOfToken0), 181 | "liquidity provider's token 0 balance should decrease after this specific swap" 182 | ) 183 | // FIXME why liquidity taker's token 0 balance is not affected? :( , why token 0 is not coming from liquidity taker??? 184 | 185 | expect(await token1.balanceOf(pair.address)).to.eq( 186 | token1AdditionalLiquidityAmount.sub(expectedSwapOutAmountOfToken1), 187 | 'token 1 balance of this pair contract should decrease after this specific swap' 188 | ) 189 | expect(await token1.balanceOf(defaultLiquidityTakerWallet.address)).to.eq( 190 | expectedSwapOutAmountOfToken1, 191 | `'liquidity taker\'s token 1 balance should increase from 0 to expectedSwapOutAmountOfToken1 (${expectedSwapOutAmountOfToken1}) after this specific swap'` 192 | ) 193 | 194 | expect(await token1.balanceOf(defaultLiquidityProviderWallet.address)).to.eq( 195 | totalSupplyToken1.sub(token1AdditionalLiquidityAmount), 196 | "liquidity provider's token 1 balance should not change before or after swap event initiated by another liquidity taker" 197 | ) 198 | }) 199 | 200 | it('swap:token1', async () => { 201 | const token0Amount = expandTo18Decimals(5) 202 | const token1Amount = expandTo18Decimals(10) 203 | await addLiquidity(token0Amount, token1Amount, defaultLiquidityProviderWallet.address) 204 | 205 | const swapAmount = expandTo18Decimals(1) 206 | const expectedOutputAmount = bigNumberify('453305446940074565') 207 | await token1.transfer(pair.address, swapAmount) 208 | await expect(pair.swap(expectedOutputAmount, 0, defaultLiquidityProviderWallet.address, '0x', overrides)) 209 | .to.emit(token0, 'Transfer') 210 | .withArgs(pair.address, defaultLiquidityProviderWallet.address, expectedOutputAmount) 211 | .to.emit(pair, 'Sync') 212 | .withArgs(token0Amount.sub(expectedOutputAmount), token1Amount.add(swapAmount)) 213 | .to.emit(pair, 'Swap') 214 | .withArgs( 215 | defaultLiquidityProviderWallet.address, 216 | 0, 217 | swapAmount, 218 | expectedOutputAmount, 219 | 0, 220 | defaultLiquidityProviderWallet.address 221 | ) 222 | 223 | const reserves = await pair.getReserves() 224 | expect(reserves[0]).to.eq(token0Amount.sub(expectedOutputAmount)) 225 | expect(reserves[1]).to.eq(token1Amount.add(swapAmount)) 226 | expect(await token0.balanceOf(pair.address)).to.eq(token0Amount.sub(expectedOutputAmount)) 227 | expect(await token1.balanceOf(pair.address)).to.eq(token1Amount.add(swapAmount)) 228 | const totalSupplyToken0 = await token0.totalSupply() 229 | const totalSupplyToken1 = await token1.totalSupply() 230 | expect(await token0.balanceOf(defaultLiquidityProviderWallet.address)).to.eq( 231 | totalSupplyToken0.sub(token0Amount).add(expectedOutputAmount) 232 | ) 233 | expect(await token1.balanceOf(defaultLiquidityProviderWallet.address)).to.eq( 234 | totalSupplyToken1.sub(token1Amount).sub(swapAmount) 235 | ) 236 | }) 237 | 238 | it('swap:gas', async () => { 239 | const token0Amount = expandTo18Decimals(5) 240 | const token1Amount = expandTo18Decimals(10) 241 | await addLiquidity(token0Amount, token1Amount, defaultLiquidityProviderWallet.address) 242 | 243 | // ensure that setting price{0,1}CumulativeLast for the first time doesn't affect our gas math 244 | await mineBlock(provider, (await provider.getBlock('latest')).timestamp + 1) 245 | await pair.sync(overrides) 246 | 247 | const swapAmount = expandTo18Decimals(1) 248 | const expectedOutputAmount = bigNumberify('453305446940074565') 249 | await token1.transfer(pair.address, swapAmount) 250 | await mineBlock(provider, (await provider.getBlock('latest')).timestamp + 1) 251 | const tx = await pair.swap(expectedOutputAmount, 0, defaultLiquidityProviderWallet.address, '0x', overrides) 252 | const receipt = await tx.wait() 253 | expect(receipt.gasUsed).to.eq(76984) 254 | }) 255 | 256 | it('burn', async () => { 257 | const token0Amount = expandTo18Decimals(3) 258 | const token1Amount = expandTo18Decimals(3) 259 | await addLiquidity(token0Amount, token1Amount, defaultLiquidityProviderWallet.address) 260 | 261 | const expectedLiquidity = expandTo18Decimals(3) 262 | const MINIMUM_LIQUIDITY = 1000 263 | await pair.transfer(pair.address, expectedLiquidity.sub(MINIMUM_LIQUIDITY)) 264 | await expect(pair.burn(defaultLiquidityProviderWallet.address, overrides)) 265 | .to.emit(pair, 'Transfer') 266 | .withArgs(pair.address, AddressZero, expectedLiquidity.sub(MINIMUM_LIQUIDITY)) 267 | .to.emit(token0, 'Transfer') 268 | .withArgs(pair.address, defaultLiquidityProviderWallet.address, token0Amount.sub(MINIMUM_LIQUIDITY)) 269 | .to.emit(token1, 'Transfer') 270 | .withArgs(pair.address, defaultLiquidityProviderWallet.address, token1Amount.sub(MINIMUM_LIQUIDITY)) 271 | .to.emit(pair, 'Sync') 272 | .withArgs(MINIMUM_LIQUIDITY, MINIMUM_LIQUIDITY) 273 | .to.emit(pair, 'Burn') 274 | .withArgs( 275 | defaultLiquidityProviderWallet.address, // not that in real set up, msg.sender can be swap factory 276 | token0Amount.sub(MINIMUM_LIQUIDITY), 277 | token1Amount.sub(MINIMUM_LIQUIDITY), 278 | defaultLiquidityProviderWallet.address 279 | ) 280 | 281 | expect(await pair.balanceOf(defaultLiquidityProviderWallet.address)).to.eq(0) 282 | expect(await pair.totalSupply()).to.eq(MINIMUM_LIQUIDITY) 283 | expect(await token0.balanceOf(pair.address)).to.eq(MINIMUM_LIQUIDITY) 284 | expect(await token1.balanceOf(pair.address)).to.eq(MINIMUM_LIQUIDITY) 285 | const totalSupplyToken0 = await token0.totalSupply() 286 | const totalSupplyToken1 = await token1.totalSupply() 287 | expect(await token0.balanceOf(defaultLiquidityProviderWallet.address)).to.eq( 288 | totalSupplyToken0.sub(MINIMUM_LIQUIDITY) 289 | ) 290 | expect(await token1.balanceOf(defaultLiquidityProviderWallet.address)).to.eq( 291 | totalSupplyToken1.sub(MINIMUM_LIQUIDITY) 292 | ) 293 | }) 294 | 295 | it('price{0,1}CumulativeLast', async () => { 296 | const liquidityProviderAddress = defaultLiquidityProviderWallet.address 297 | 298 | const token0Amount = expandTo18Decimals(3) 299 | const token1Amount = expandTo18Decimals(3) 300 | await addLiquidity(token0Amount, token1Amount, liquidityProviderAddress) 301 | 302 | const blockTimestamp = (await pair.getReserves())[2] 303 | await mineBlock(provider, blockTimestamp + 1) 304 | await pair.sync(overrides) 305 | 306 | const initialPrice = encodePrice(token0Amount, token1Amount) 307 | expect(await pair.price0CumulativeLast()).to.eq(initialPrice[0]) 308 | expect(await pair.price1CumulativeLast()).to.eq(initialPrice[1]) 309 | expect((await pair.getReserves())[2]).to.eq(blockTimestamp + 1) 310 | 311 | const swapAmount = expandTo18Decimals(3) 312 | await token0.transfer(pair.address, swapAmount) 313 | await mineBlock(provider, blockTimestamp + 10) 314 | // swap to a new price eagerly instead of syncing 315 | await pair.swap(0, expandTo18Decimals(1), liquidityProviderAddress, '0x', overrides) // make the price nice 316 | 317 | expect(await pair.price0CumulativeLast()).to.eq(initialPrice[0].mul(10)) 318 | expect(await pair.price1CumulativeLast()).to.eq(initialPrice[1].mul(10)) 319 | expect((await pair.getReserves())[2]).to.eq(blockTimestamp + 10) 320 | 321 | await mineBlock(provider, blockTimestamp + 20) 322 | await pair.sync(overrides) 323 | 324 | const newPrice = encodePrice(expandTo18Decimals(6), expandTo18Decimals(2)) 325 | expect(await pair.price0CumulativeLast()).to.eq(initialPrice[0].mul(10).add(newPrice[0].mul(10))) 326 | expect(await pair.price1CumulativeLast()).to.eq(initialPrice[1].mul(10).add(newPrice[1].mul(10))) 327 | expect((await pair.getReserves())[2]).to.eq(blockTimestamp + 20) 328 | }) 329 | 330 | it('When feeTo:off, all fee should go to liquidity providers.', async () => { 331 | const swapAmountIntoPoolInToken1 = expandTo18Decimals(1) 332 | await token1.transfer(defaultLiquidityTakerWallet.address, swapAmountIntoPoolInToken1.mul(2)) // add some buffer to avoid underflow 333 | 334 | const token0TotalBalanceBeforeAnyActions = await getTokenTotalBalance(token0) 335 | const token1TotalBalanceBeforeAnyActions = await getTokenTotalBalance(token1) 336 | 337 | const token0BalanceOfProviderBeforeAnyActions = await token0.balanceOf(defaultLiquidityProviderWallet.address) 338 | 339 | await printBalances('before providing liquidity') 340 | 341 | const token0Amount = expandTo18Decimals(1000) 342 | const token1Amount = expandTo18Decimals(1000) 343 | // 1 token0 = INITIAL_RATE_TOKEN0_TO_TOKEN1 token1 NOTE THAT THIS RATE ASSUMPTION GUARDS THE FOLLOWING TESTING LOGIC 344 | const INITIAL_RATE_TOKEN0_TO_TOKEN1 = bigNumberify(1) 345 | await addLiquidity( 346 | token0Amount, 347 | INITIAL_RATE_TOKEN0_TO_TOKEN1.mul(token1Amount), 348 | defaultLiquidityProviderWallet.address 349 | ) 350 | 351 | await printBalances('after addLiquidity & before transfer swapAmount') 352 | 353 | await token1.connect(defaultLiquidityTakerWallet).approve(pair.address, swapAmountIntoPoolInToken1) 354 | 355 | console.log( 356 | `await token1.allowance(taker.address, pair.address): ${await token1.allowance( 357 | defaultLiquidityTakerWallet.address, 358 | pair.address 359 | )}` 360 | ) 361 | await token1.connect(defaultLiquidityTakerWallet).transfer(pair.address, swapAmountIntoPoolInToken1) 362 | 363 | await printBalances('after transfer swapAmount & before swap token0 out to taker') 364 | 365 | const expectedOutputAmountOfToken0 = bigNumberify('996006981039903216') // need to hardcode the expected amount because precision is not very good... 366 | const expectedOutputAmountOfToken1 = bigNumberify('0') 367 | await pair.swap( 368 | expectedOutputAmountOfToken0, 369 | expectedOutputAmountOfToken1, 370 | defaultLiquidityTakerWallet.address, 371 | '0x', 372 | overrides 373 | ) 374 | 375 | expect(await token0.balanceOf(defaultLiquidityTakerWallet.address)).to.eq( 376 | expectedOutputAmountOfToken0, 377 | 'taker balance of token 0 should increment by expectedOutputAmountOfToken0 after successful swap' 378 | ) 379 | 380 | await printBalances( 381 | 'after swap token0 out & before the provider transfers all LP tokens s/he can back to pair contract' 382 | ) 383 | 384 | const expectedLiquidityTokenAmount = expandTo18Decimals(1000) 385 | await pair.transfer(pair.address, expectedLiquidityTokenAmount.sub(MINIMUM_LIQUIDITY)) 386 | 387 | await printBalances('after transfer out liquidity tokens & before burn') 388 | 389 | await pair.burn(defaultLiquidityProviderWallet.address, overrides) 390 | 391 | await printBalances('after burning LP tokens') 392 | 393 | const token0TotalBalanceAfterAllActions = await getTokenTotalBalance(token0) 394 | const token1TotalBalanceAfterAllActions = await getTokenTotalBalance(token1) 395 | 396 | console.log(`token0TotalBalanceAfterAllActions: ${token0TotalBalanceAfterAllActions}`) 397 | console.log(`token1TotalBalanceAfterAllActions: ${token1TotalBalanceAfterAllActions}`) 398 | expect(token0TotalBalanceBeforeAnyActions).to.deep.eq( 399 | token0TotalBalanceAfterAllActions, 400 | 'token 0 total balance before and after should be the same' 401 | ) 402 | expect(token1TotalBalanceBeforeAnyActions).to.deep.eq( 403 | token1TotalBalanceAfterAllActions, 404 | 'token 1 total balance before and after should be the same' 405 | ) 406 | 407 | expect(await pair.totalSupply()).to.eq(MINIMUM_LIQUIDITY) 408 | 409 | /** 410 | * liquidity taker is trying to swap $expandTo18Decimals(1) units of token1 for token 0, 411 | * priced (according to pool reserves) roughly at $INITIAL_RATE_TOKEN0_TO_TOKEN1, 412 | * but taker get less instead after fees, 413 | * and the fee in the form of extra token 0 goes to liquidity provider 414 | * 415 | before providing liquidity: await token0.balanceOf(provider.address): 10000000000000000000000 416 | before providing liquidity: await token1.balanceOf(provider.address): 9998000000000000000000 417 | before providing liquidity: await token0.balanceOf(pair.address): 0 418 | before providing liquidity: await token1.balanceOf(pair.address): 0 419 | before providing liquidity: await token0.balanceOf(taker.address): 0 420 | before providing liquidity: await token1.balanceOf(taker.address): 2000000000000000000 421 | 422 | after swap & burn: await token0.balanceOf(provider.address): 9999003993018960095784 423 | after swap & burn: await token1.balanceOf(provider.address): 9998999999999999998999 424 | after swap & burn: await token0.balanceOf(pair.address): 1000 425 | after swap & burn: await token1.balanceOf(pair.address): 1001 426 | after swap & burn: await token0.balanceOf(taker.address): 996006981039903216 427 | after swap & burn: await token1.balanceOf(taker.address): 1000000000000000000 428 | */ 429 | const token0BalanceChangeOfProvider = (await token0.balanceOf(defaultLiquidityProviderWallet.address)).sub( 430 | token0BalanceOfProviderBeforeAnyActions 431 | ) 432 | const token0DeductibleFromProviderWithoutFees = swapAmountIntoPoolInToken1.div(INITIAL_RATE_TOKEN0_TO_TOKEN1) 433 | const feeEarnedInToken0 = token0BalanceChangeOfProvider.add(token0DeductibleFromProviderWithoutFees) 434 | const FEE_RATE_IN_BPS = bigNumberify(30) 435 | console.log( 436 | `feeEarnedInToken0: ${feeEarnedInToken0} token0DeductibleFromProviderWithoutFees: ${token0DeductibleFromProviderWithoutFees}` 437 | ) 438 | expect(feeEarnedInToken0).to.gte( 439 | token0DeductibleFromProviderWithoutFees.mul(FEE_RATE_IN_BPS).div(10000), 440 | `liquidity provider should get >= ${FEE_RATE_IN_BPS} bps in fees when pulling out liquidity` 441 | ) 442 | }) 443 | 444 | // TODO make fees and number metrics configurable 445 | it('When feeTo:on, 5 bps of fees should go to feeToAddress, the rest 25 bps still go to liquidity providers', async () => { 446 | await expect(factory.setFeeTo(defaultFeeToWallet.address)) 447 | .to.emit(factory, 'FeeToUpdated') 448 | .withArgs(defaultFeeToWallet.address, AddressZero) 449 | 450 | const swapAmountIntoPoolInToken1 = expandTo18Decimals(1) 451 | await token1.transfer(defaultLiquidityTakerWallet.address, swapAmountIntoPoolInToken1.mul(2)) // add some buffer to avoid underflow 452 | 453 | const token0TotalBalanceBeforeAnyActions = await getTokenTotalBalance(token0) 454 | const token1TotalBalanceBeforeAnyActions = await getTokenTotalBalance(token1) 455 | 456 | await printBalances('before providing liquidity') 457 | 458 | const token0Amount = expandTo18Decimals(1000) 459 | const token1Amount = expandTo18Decimals(1000) 460 | 461 | // 1 token0 = INITIAL_RATE_TOKEN0_TO_TOKEN1 token1 NOTE THAT THIS RATE ASSUMPTION GUARDS THE FOLLOWING TESTING LOGIC 462 | const INITIAL_RATE_TOKEN0_TO_TOKEN1 = bigNumberify(1) 463 | 464 | await addLiquidity( 465 | token0Amount, 466 | INITIAL_RATE_TOKEN0_TO_TOKEN1.mul(token1Amount), 467 | defaultLiquidityProviderWallet.address 468 | ) 469 | 470 | await printBalances('after addLiquidity & before transfer swapAmount') 471 | 472 | const expectedOutputAmountOfToken0 = bigNumberify('996006981039903216') 473 | const expectedOutputAmountOfToken1 = bigNumberify('0') 474 | 475 | await token1.connect(defaultLiquidityTakerWallet).transfer(pair.address, swapAmountIntoPoolInToken1) 476 | 477 | await printBalances('after transfer swapAmount & before swap token0 out to taker') 478 | 479 | await pair.swap( 480 | expectedOutputAmountOfToken0, 481 | expectedOutputAmountOfToken1, 482 | defaultLiquidityTakerWallet.address, 483 | '0x', 484 | overrides 485 | ) 486 | 487 | await printBalances( 488 | 'after swap token0 out & before the provider transfers all LP tokens s/he can back to pair contract' 489 | ) 490 | 491 | const expectedLiquidityTokenAmount = expandTo18Decimals(1000) 492 | await pair.transfer(pair.address, expectedLiquidityTokenAmount.sub(MINIMUM_LIQUIDITY)) 493 | 494 | await printBalances('after transfer out liquidity tokens & before burn') 495 | 496 | await pair.burn(defaultLiquidityProviderWallet.address, overrides) 497 | 498 | await printBalances('after liquidity provider burning LP tokens') 499 | 500 | const expectedFeeToLiquidityTokenAmount = '249750499251388' // roughly 2.5 bps of the expectedOutputAmount 501 | expect(await pair.totalSupply()).to.eq(MINIMUM_LIQUIDITY.add(expectedFeeToLiquidityTokenAmount)) 502 | expect(await pair.balanceOf(defaultFeeToWallet.address)).to.eq(expectedFeeToLiquidityTokenAmount) 503 | 504 | // using 1000 here instead of the symbolic MINIMUM_LIQUIDITY because the amounts only happen to be equal... 505 | // ...because the initial liquidity amounts were equal 506 | expect(await token0.balanceOf(pair.address)).to.eq(bigNumberify(1000).add('249501683697445')) 507 | expect(await token1.balanceOf(pair.address)).to.eq(bigNumberify(1000).add('250000187312969')) 508 | 509 | await pair.connect(defaultFeeToWallet).transfer(pair.address, expectedFeeToLiquidityTokenAmount) 510 | await pair.connect(defaultFeeToWallet).burn(defaultFeeToWallet.address, overrides) 511 | 512 | await printBalances('after feeTo wallet burning LP tokens') 513 | 514 | const token0TotalBalanceAfterAllActions = await getTokenTotalBalance(token0) 515 | const token1TotalBalanceAfterAllActions = await getTokenTotalBalance(token1) 516 | 517 | expect(token0TotalBalanceBeforeAnyActions).to.deep.eq( 518 | token0TotalBalanceAfterAllActions, 519 | 'token 0 total balance before and after should be the same' 520 | ) 521 | expect(token1TotalBalanceBeforeAnyActions).to.deep.eq( 522 | token1TotalBalanceAfterAllActions, 523 | 'token 1 total balance before and after should be the same' 524 | ) 525 | 526 | expect(await token0.balanceOf(defaultFeeToWallet.address)).to.gt( 527 | 0, 528 | 'feeTo address should have collected some fees in token 0 after burning its allocated liquidity token' 529 | ) 530 | expect(await token1.balanceOf(defaultFeeToWallet.address)).to.gt( 531 | 0, 532 | 'feeTo address should have collected some fees in token 1 after burning its allocated liquidity token' 533 | ) 534 | }) 535 | 536 | const printBalances = async (scenarioDescription: string) => { 537 | console.log( 538 | `${scenarioDescription}: await token0.balanceOf(provider.address): ${await token0.balanceOf( 539 | defaultLiquidityProviderWallet.address 540 | )}` 541 | ) 542 | console.log(`${scenarioDescription}: await token0.balanceOf(pair.address): ${await token0.balanceOf(pair.address)}`) 543 | console.log( 544 | `${scenarioDescription}: await token0.balanceOf(taker.address): ${await token0.balanceOf( 545 | defaultLiquidityTakerWallet.address 546 | )}` 547 | ) 548 | console.log( 549 | `${scenarioDescription}: await token0.balanceOf(feeTo.address): ${await token0.balanceOf( 550 | defaultFeeToWallet.address 551 | )}` 552 | ) 553 | 554 | console.log( 555 | `${scenarioDescription}: await token1.balanceOf(provider.address): ${await token1.balanceOf( 556 | defaultLiquidityProviderWallet.address 557 | )}` 558 | ) 559 | console.log(`${scenarioDescription}: await token1.balanceOf(pair.address): ${await token1.balanceOf(pair.address)}`) 560 | 561 | console.log( 562 | `${scenarioDescription}: await token1.balanceOf(taker.address): ${await token1.balanceOf( 563 | defaultLiquidityTakerWallet.address 564 | )}` 565 | ) 566 | 567 | console.log( 568 | `${scenarioDescription}: await token1.balanceOf(feeTo.address): ${await token1.balanceOf( 569 | defaultFeeToWallet.address 570 | )}` 571 | ) 572 | 573 | console.log( 574 | `${scenarioDescription}: await pair.balanceOf(provider.address): ${await pair.balanceOf( 575 | defaultLiquidityProviderWallet.address 576 | )}` 577 | ) 578 | console.log( 579 | `${scenarioDescription}: await pair.balanceOf(taker.address): ${await pair.balanceOf( 580 | defaultLiquidityTakerWallet.address 581 | )}` 582 | ) 583 | console.log( 584 | `${scenarioDescription}: await pair.balanceOf(feeTo.address): ${await pair.balanceOf(defaultFeeToWallet.address)}` 585 | ) 586 | } 587 | async function getTokenTotalBalance(whichToken: Contract) { 588 | return (await whichToken.balanceOf(defaultLiquidityProviderWallet.address)) 589 | .add(await whichToken.balanceOf(pair.address)) 590 | .add(await whichToken.balanceOf(defaultLiquidityTakerWallet.address)) 591 | .add(await whichToken.balanceOf(defaultFeeToWallet.address)) 592 | } 593 | }) 594 | -------------------------------------------------------------------------------- /test/shared/fixtures.ts: -------------------------------------------------------------------------------- 1 | import { Contract, Wallet } from 'ethers' 2 | import { Web3Provider } from 'ethers/providers' 3 | import { deployContract } from 'ethereum-waffle' 4 | 5 | import { expandTo18Decimals } from './utilities' 6 | 7 | import ERC20 from '../../build/ERC20.json' 8 | import CroDefiSwapFactory from '../../build/CroDefiSwapFactory.json' 9 | import CroDefiSwapPair from '../../build/CroDefiSwapPair.json' 10 | 11 | interface FactoryFixture { 12 | factory: Contract 13 | } 14 | 15 | const overrides = { 16 | gasLimit: 9999999 17 | } 18 | 19 | export async function factoryFixture(_: Web3Provider, [wallet]: Wallet[]): Promise { 20 | const factory = await deployContract(wallet, CroDefiSwapFactory, [wallet.address, 30, 5], overrides) 21 | return { factory } 22 | } 23 | 24 | interface PairFixture extends FactoryFixture { 25 | token0: Contract 26 | token1: Contract 27 | pair: Contract 28 | } 29 | 30 | export async function pairFixture(provider: Web3Provider, [wallet]: Wallet[]): Promise { 31 | const { factory } = await factoryFixture(provider, [wallet]) 32 | 33 | const tokenA = await deployContract(wallet, ERC20, [expandTo18Decimals(10000)], overrides) 34 | const tokenB = await deployContract(wallet, ERC20, [expandTo18Decimals(10000)], overrides) 35 | 36 | await factory.createPair(tokenA.address, tokenB.address, overrides) 37 | const pairAddress = await factory.getPair(tokenA.address, tokenB.address) 38 | const pair = new Contract(pairAddress, JSON.stringify(CroDefiSwapPair.abi), provider).connect(wallet) 39 | 40 | const token0Address = (await pair.token0()).address 41 | const token0 = tokenA.address === token0Address ? tokenA : tokenB 42 | const token1 = tokenA.address === token0Address ? tokenB : tokenA 43 | 44 | return { factory, token0, token1, pair } 45 | } 46 | -------------------------------------------------------------------------------- /test/shared/utilities.ts: -------------------------------------------------------------------------------- 1 | import { Contract } from 'ethers' 2 | import { Web3Provider } from 'ethers/providers' 3 | import { 4 | BigNumber, 5 | bigNumberify, 6 | getAddress, 7 | keccak256, 8 | defaultAbiCoder, 9 | toUtf8Bytes, 10 | solidityPack 11 | } from 'ethers/utils' 12 | 13 | const PERMIT_TYPEHASH = keccak256( 14 | toUtf8Bytes('Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)') 15 | ) 16 | 17 | export function expandTo18Decimals(n: number): BigNumber { 18 | return bigNumberify(n).mul(bigNumberify(10).pow(18)) 19 | } 20 | 21 | function getDomainSeparator(name: string, tokenAddress: string) { 22 | return keccak256( 23 | defaultAbiCoder.encode( 24 | ['bytes32', 'bytes32', 'bytes32', 'uint256', 'address'], 25 | [ 26 | keccak256(toUtf8Bytes('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)')), 27 | keccak256(toUtf8Bytes(name)), 28 | keccak256(toUtf8Bytes('1')), 29 | 1, 30 | tokenAddress 31 | ] 32 | ) 33 | ) 34 | } 35 | 36 | export function getCreate2Address( 37 | factoryAddress: string, 38 | [tokenA, tokenB]: [string, string], 39 | bytecode: string 40 | ): string { 41 | const [token0, token1] = tokenA < tokenB ? [tokenA, tokenB] : [tokenB, tokenA] 42 | const create2Inputs = [ 43 | '0xff', 44 | factoryAddress, 45 | keccak256(solidityPack(['address', 'address'], [token0, token1])), 46 | keccak256(bytecode) 47 | ] 48 | const sanitizedInputs = `0x${create2Inputs.map(i => i.slice(2)).join('')}` 49 | return getAddress(`0x${keccak256(sanitizedInputs).slice(-40)}`) 50 | } 51 | 52 | export async function getApprovalDigest( 53 | token: Contract, 54 | approve: { 55 | owner: string 56 | spender: string 57 | value: BigNumber 58 | }, 59 | nonce: BigNumber, 60 | deadline: BigNumber 61 | ): Promise { 62 | const name = await token.name() 63 | const DOMAIN_SEPARATOR = getDomainSeparator(name, token.address) 64 | return keccak256( 65 | solidityPack( 66 | ['bytes1', 'bytes1', 'bytes32', 'bytes32'], 67 | [ 68 | '0x19', 69 | '0x01', 70 | DOMAIN_SEPARATOR, 71 | keccak256( 72 | defaultAbiCoder.encode( 73 | ['bytes32', 'address', 'address', 'uint256', 'uint256', 'uint256'], 74 | [PERMIT_TYPEHASH, approve.owner, approve.spender, approve.value, nonce, deadline] 75 | ) 76 | ) 77 | ] 78 | ) 79 | ) 80 | } 81 | 82 | export async function mineBlock(provider: Web3Provider, timestamp: number): Promise { 83 | await new Promise(async (resolve, reject) => { 84 | ;(provider._web3Provider.sendAsync as any)( 85 | { jsonrpc: '2.0', method: 'evm_mine', params: [timestamp] }, 86 | (error: any, result: any): void => { 87 | if (error) { 88 | reject(error) 89 | } else { 90 | resolve(result) 91 | } 92 | } 93 | ) 94 | }) 95 | } 96 | 97 | export function encodePrice(reserve0: BigNumber, reserve1: BigNumber) { 98 | return [reserve1.mul(bigNumberify(2).pow(112)).div(reserve0), reserve0.mul(bigNumberify(2).pow(112)).div(reserve1)] 99 | } 100 | -------------------------------------------------------------------------------- /truffle-config.js: -------------------------------------------------------------------------------- 1 | const HDWalletProvider = require("@truffle/hdwallet-provider"); 2 | 3 | const dotenv = require('dotenv'); 4 | dotenv.config(); 5 | 6 | const infuraProvider = (network) => { 7 | return new HDWalletProvider( 8 | process.env.MNEMONIC, 9 | `https://${network}.infura.io/v3/${process.env.INFURA_API_KEY}` 10 | ) 11 | } 12 | 13 | module.exports = { 14 | // Uncommenting the defaults below 15 | // provides for an easier quick-start with Ganache. 16 | // You can also follow this format for other networks; 17 | // see 18 | // for more details on how to specify configuration options! 19 | // 20 | networks: { 21 | development: { 22 | host: "127.0.0.1", 23 | port: 8545, 24 | network_id: "*" 25 | }, 26 | ropsten: { 27 | provider: infuraProvider("ropsten"), 28 | network_id: "3", 29 | gas: 6000000, 30 | gasPrice: 5000000000, // in wei 31 | }, 32 | mainnet: { 33 | provider: infuraProvider("mainnet"), 34 | network_id: "1", 35 | gas: 6000000, 36 | gasPrice: 125000000000, // in wei 37 | } 38 | }, 39 | // 40 | compilers: { 41 | solc: { 42 | version: "0.5.16", 43 | settings: { 44 | optimizer: { 45 | enabled: true, 46 | runs: 999999 47 | }, 48 | evmVersion: "istanbul" 49 | } 50 | } 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "resolveJsonModule": true 8 | } 9 | } 10 | --------------------------------------------------------------------------------