├── .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 |
--------------------------------------------------------------------------------