├── .circleci └── config.yml ├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .gitignore ├── .solcover.js ├── .solhint.json ├── Audit.md ├── LICENSE ├── README.md ├── Trail of Bits Full Audit.pdf ├── contracts ├── BColor.sol ├── BConst.sol ├── BFactory.sol ├── BMath.sol ├── BNum.sol ├── BPool.sol ├── BToken.sol ├── Migrations.sol └── test │ ├── TMath.sol │ ├── TToken.sol │ └── echidna │ ├── TBPoolJoinExitPool.sol │ ├── TBPoolJoinExitPoolNoFee.sol │ └── TBPoolJoinPool.sol ├── echidna ├── BMathInternal.sol ├── CryticInterface.sol ├── MyToken.sol ├── TBPoolBalance.sol ├── TBPoolBalance.yaml ├── TBPoolBind.sol ├── TBPoolBindPrivileged.yaml ├── TBPoolBindUnprivileged.yaml ├── TBPoolController.sol ├── TBPoolControllerPrivileged.yaml ├── TBPoolControllerUnprivileged.yaml ├── TBPoolExitSwap.sol ├── TBPoolJoinExit.sol ├── TBPoolJoinExit.yaml ├── TBPoolLimits.sol ├── TBPoolLimits.yaml ├── TBPoolNoRevert.sol ├── TBPoolNoRevert.yaml ├── TBTokenERC20.sol └── TBTokenERC20.yaml ├── echidna_general_config.yaml ├── lib └── calc_comparisons.js ├── manticore ├── TBPoolJoinExit.py ├── TBPoolJoinExitNoFee.py ├── TBPoolJoinPool.py └── contracts │ ├── BNum.sol │ ├── TBPoolJoinExitPool.sol │ ├── TBPoolJoinExitPoolNoFee.sol │ └── TBPoolJoinPool.sol ├── migrations ├── 1_initial_migration.js └── 2_deploy_factories.js ├── package.json ├── test ├── factory.js ├── math_extreme_weights.js ├── math_with_fees.js ├── num.js ├── pool.js └── pool_max_tokens.js ├── truffle-config.js └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | checkout_and_install: 4 | docker: 5 | - image: circleci/node:11 6 | working_directory: ~/balancer 7 | steps: 8 | - checkout 9 | - restore_cache: 10 | keys: 11 | - dependency-cache-{{ checksum "package.json" }} 12 | - run: 13 | name: Install Dependencies 14 | command: yarn install --quiet 15 | - save_cache: 16 | key: dependency-cache-{{ checksum "package.json" }} 17 | paths: 18 | - node_modules 19 | - save_cache: 20 | key: balancer-{{ .Environment.CIRCLE_SHA1 }} 21 | paths: 22 | - ~/balancer 23 | lint: 24 | docker: 25 | - image: circleci/node:11 26 | working_directory: ~/balancer 27 | steps: 28 | - restore_cache: 29 | key: balancer-{{ .Environment.CIRCLE_SHA1 }} 30 | - run: 31 | name: Lint contracts 32 | command: yarn lint:contracts 33 | - run: 34 | name: Lint tests 35 | command: yarn lint 36 | build: 37 | docker: 38 | - image: circleci/node:11 39 | - image: ethereum/solc:0.5.11 40 | working_directory: ~/balancer 41 | steps: 42 | - restore_cache: 43 | key: balancer-{{ .Environment.CIRCLE_SHA1 }} 44 | - run: 45 | name: Compile contracts 46 | command: yarn compile 47 | - save_cache: 48 | key: balancer-contracts-build-{{ .Environment.CIRCLE_SHA1 }} 49 | paths: 50 | - ~/balancer 51 | test: 52 | docker: 53 | - image: circleci/node:11 54 | - image: trufflesuite/ganache-cli 55 | command: ganache-cli -d -l 4294967295 --allowUnlimitedContractSize 56 | working_directory: ~/balancer 57 | steps: 58 | - restore_cache: 59 | key: balancer-contracts-build-{{ .Environment.CIRCLE_SHA1 }} 60 | - run: 61 | name: Run tests 62 | command: yarn test 63 | 64 | coverage: 65 | docker: 66 | - image: circleci/node:11 67 | working_directory: ~/balancer 68 | steps: 69 | - setup_remote_docker: 70 | docker_layer_caching: true 71 | - run: 72 | name: Fetch solc version 73 | command: docker pull ethereum/solc:0.5.11 74 | - restore_cache: 75 | key: balancer-contracts-build-{{ .Environment.CIRCLE_SHA1 }} 76 | - run: 77 | name: Coverage 78 | command: yarn coverage && cat coverage/lcov.info | ./node_modules/.bin/coveralls 79 | 80 | slither: 81 | docker: 82 | - image: trailofbits/eth-security-toolbox 83 | working_directory: ~/balancer 84 | steps: 85 | - checkout 86 | - run: 87 | name: Compile 88 | command: truffle compile 89 | - run: 90 | name: Slither 91 | command: slither . --filter-paths test --exclude=naming-convention,unused-state,solc-version,constable-states,external-function,reentrancy-events 92 | 93 | workflows: 94 | version: 2 95 | build_and_test: 96 | jobs: 97 | - checkout_and_install 98 | - lint: 99 | requires: 100 | - checkout_and_install 101 | - build: 102 | requires: 103 | - checkout_and_install 104 | - test: 105 | requires: 106 | - build 107 | - coverage: 108 | requires: 109 | - build 110 | - slither: 111 | requires: 112 | - build 113 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/* 2 | coverage/* 3 | migrations/* -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "airbnb" 4 | ], 5 | "rules": { 6 | "indent": ["error", 4], 7 | "max-len": ["error", { "code": 120 }], 8 | "no-console": "off" 9 | }, 10 | "globals" : { 11 | "artifacts": false, 12 | "contract": false, 13 | "assert": false, 14 | "it": false, 15 | "before": false, 16 | "describe": false, 17 | "web3": false 18 | } 19 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | .idea/* 4 | coverage.json 5 | coverage/ 6 | yarn-error.log 7 | crytic-export 8 | mcore_* -------------------------------------------------------------------------------- /.solcover.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | port: 8555, 3 | skipFiles: [ 4 | 'Migrations.sol', 5 | 'test' 6 | ], 7 | testrpcOptions: "-p 8555 -d" 8 | }; -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "rules": { 4 | "mark-callable-contracts": "off", 5 | "event-name-camelcase": "off", 6 | "const-name-snakecase": "off", 7 | "max-line-length": ["warn", 120], 8 | "indent": ["error", 4] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Audit.md: -------------------------------------------------------------------------------- 1 | - [Installation](#Installation) 2 | - [Testing with Echidna](#testing-properties-with-echidna) 3 | - [Code verification with Manticore](#Code-verification-with-Manticore) 4 | 5 | # Installation 6 | 7 | **Slither** 8 | ``` 9 | pip3 install slither-analyzer 10 | ``` 11 | 12 | **Manticore** 13 | ``` 14 | pip3 install manticore 15 | ``` 16 | 17 | **Echidna** 18 | See [Echidna Installation](https://github.com/crytic/building-secure-contracts/tree/master/program-analysis/echidna#installation). 19 | 20 | 21 | ``` 22 | docker run -it -v "$PWD":/home/training trailofbits/eth-security-toolbox 23 | ``` 24 | 25 | ``` 26 | solc-select 0.5.12 27 | cd /home/training 28 | ``` 29 | 30 | 31 | # Testing properties with Echidna 32 | 33 | `slither-flat` will export the contract and translate external function to public, to faciliate writting properties: 34 | ``` 35 | slither-flat . --convert-external 36 | ``` 37 | 38 | The flattened contracts are in `crytic-export/flattening`. The Echidna properties are in `echidna/`. 39 | 40 | ## Properties 41 | 42 | Echidna properties can be broadly divided in two categories: general properties of the contracts that states what user can and cannot do and 43 | specific properties based on unit tests. 44 | 45 | To test a property, run `echidna-test echidna/CONTRACT_file.sol CONTRACT_name --config echidna/CONTRACT_name.yaml`. 46 | 47 | ## General Properties 48 | 49 | | Description | Name | Contract | Finding | Status | 50 | | :--- | :---: | :---: | :---: | :---: | 51 | | An attacker cannot steal assets from a public pool. | [`attacker_token_balance`](echidna/TBPoolBalance.sol#L22-L25) | [`TBPoolBalance`](echidna/TBPoolBalance.sol) |FAILED ([#193](https://github.com/balancer-labs/balancer-core/issues/193))| **Fixed** | 52 | | An attacker cannot force the pool balance to be out-of-sync. | [`pool_record_balance`](echidna/TBPoolBalance.sol#L27-L33) | [`TBPoolBalance`](echidna/TBPoolBalance.sol)|PASSED| | 53 | | An attacker cannot generate free pool tokens with `joinPool` (1, 2). | [`joinPool`](contracts/test/echidna/TBPoolJoinPool.sol#L7-L31) | [`TBPoolJoinPool`](contracts/test/echidna/TBPoolBalance.sol)|FAILED ([#204](https://github.com/balancer-labs/balancer-core/issues/204))| **Mitigated** | 54 | | Calling `joinPool-exitPool` does not lead to free pool tokens (no fee) (1, 2). | [`joinPool`](contracts/test/echidna/TBPoolJoinExitPoolNoFee.sol#L34-L59) | [`TBPoolJoinExitNoFee`](contracts/test/echidna/TBPoolJoinExitPoolNoFee.sol)|FAILED ([#205](https://github.com/balancer-labs/balancer-core/issues/205))| **Mitigated** | 55 | | Calling `joinPool-exitPool` does not lead to free pool tokens (with fee) (1, 2). | [`joinPool`](contracts/test/echidna/TBPoolJoinExitPool.sol#L37-L62) | [`TBPoolJoinExit`](contracts/test/echidna/TBPoolJoinExitPool.sol)|FAILED ([#205](https://github.com/balancer-labs/balancer-core/issues/205))| **Mitigated** | 56 | | Calling `exitswapExternAmountOut` does not lead to free asset (1). | [`exitswapExternAmountOut`](echidna/TBPoolExitSwap.sol#L8-L21) | [`TBPoolExitSwap`](contracts/test/echidna/TBPoolExitSwap.sol)|FAILED ([#203](https://github.com/balancer-labs/balancer-core/issues/203))| **Mitigated** | 57 | 58 | 59 | (1) These properties target a specific piece of code. 60 | 61 | (2) These properties don't need slither-flat, and are integrated into `contracts/test/echidna/`. To test them run `echidna-test . CONTRACT_name --config ./echidna_general_config.yaml`. 62 | 63 | ## Unit-test-based Properties 64 | 65 | | Description | Name | Contract | Finding | Status | 66 | | :--- | :---: | :---: | :---: | :---: | 67 | | If the controller calls `setController`, then the `getController()` should return the new controller. | [`controller_should_change`](echidna/TBPoolController.sol#L6-L13) | [`TBPoolController`](echidna/TBPoolController.sol)|PASSED| | 68 | | The controller cannot be changed to a null address (`0x0`). | [`controller_cannot_be_null`](echidna/TBPoolController.sol#L15-L23) | [`TBPoolController`](echidna/TBPoolController.sol)|FAILED ([#198](https://github.com/balancer-labs/balancer-core/issues/198))| **WONT FIX** | 69 | | The controller cannot be changed by other users. | [`no_other_user_can_change_the_controller`](echidna/TBPoolController.sol#L28-L31) | [`TBPoolController`](echidna/TBPoolController.sol)|PASSED| | 70 | | The sum of normalized weight should be 1 if there are tokens binded. | [`valid_weights`](echidna/TBPoolLimits.sol#L35-L52) | [`TBPoolLimits`](echidna/TBPoolLimits.sol) |FAILED ([#208](https://github.com/balancer-labs/balancer-core/issues/208)| **Mitigated** | 71 | | The balances of all the tokens are greater or equal than `MIN_BALANCE`. | [`min_token_balance`](echidna/TBPoolLimits.sol#L65-L74) | [`TBPoolLimits`](echidna/TBPoolLimits.sol) |FAILED ([#210](https://github.com/balancer-labs/balancer-core/issues/210)) | **WONT FIX**| 72 | | The weight of all the tokens are less or equal than `MAX_WEIGHT`. | [`max_weight`](echidna/TBPoolLimits.sol#L76-L85) | [`TBPoolLimits`](echidna/TBPoolLimits.sol) |PASSED| | 73 | | The weight of all the tokens are greater or equal than `MIN_WEIGHT`. | [`min_weight`](echidna/TBPoolLimits.sol#L87-L96) | [`TBPoolLimits`](echidna/TBPoolLimits.sol) |PASSED| | 74 | | The swap fee is less or equal tan `MAX_FEE`. | [`min_swap_free`](echidna/TBPoolLimits.sol#L99-L102) | [`TBPoolLimits`](echidna/TBPoolLimits.sol) |PASSED| | 75 | | The swap fee is greater or equal than `MIN_FEE`. | [`max_swap_free`](echidna/TBPoolLimits.sol#L104-L107) | [`TBPoolLimits`](echidna/TBPoolLimits.sol) |PASSED| | 76 | | An user can only swap in less than 50% of the current balance of tokenIn for a given pool. | [`max_swapExactAmountIn`](echidna/TBPoolLimits.sol#L134-L156) | [`TBPoolLimits`](echidna/TBPoolLimits.sol) |FAILED ([#212](https://github.com/balancer-labs/balancer-core/issues/212))| **Fixed** | 77 | | An user can only swap out less than 33.33% of the current balance of tokenOut for a given pool. | [`max_swapExactAmountOut`](echidna/TBPoolLimits.sol#L109-L132) | [`TBPoolLimits`](echidna/TBPoolLimits.sol) |FAILED ([#212](https://github.com/balancer-labs/balancer-core/issues/212))| **Fixed** | 78 | | If a token is bounded, the `getSpotPrice` should never revert. | [`getSpotPrice_no_revert`](echidna/TBPoolNoRevert.sol#L34-L44) | [`TBPoolNoRevert`](echidna/TBPoolNoRevert.sol) |PASSED| | 79 | | If a token is bounded, the `getSpotPriceSansFee` should never revert. | [`getSpotPriceSansFee_no_revert`](echidna/TBPoolNoRevert.sol#L46-L56) | [`TBPoolNoRevert`](echidna/TBPoolNoRevert.sol) |PASSED| | 80 | | Calling `swapExactAmountIn` with a small value of the same token should never revert. | [`swapExactAmountIn_no_revert`](echidna/TBPoolNoRevert.sol#L58-L77) | [`TBPoolNoRevert`](echidna/TBPoolNoRevert.sol) |PASSED| | 81 | | Calling `swapExactAmountOut` with a small value of the same token should never revert. | [`swapExactAmountOut_no_revert`](echidna/TBPoolNoRevert.sol#L79-L99) | [`TBPoolNoRevert`](echidna/TBPoolNoRevert.sol) |PASSED| | 82 | | If a user joins pool and exits it with the same amount, the balances should keep constant. | [`joinPool_exitPool_balance_consistency`](echidna/TBPoolJoinExit.sol#L48-L97) | [`TBPoolJoinExit`](echidna/TBPoolJoinExit.sol) |PASSED| | 83 | | If a user joins pool and exits it with a larger amount, `exitPool` should revert. | [`impossible_joinPool_exitPool`](echidna/TBPoolJoinExit.sol#L99-L112) | [`TBPoolJoinExit`](echidna/TBPoolJoinExit.sol) |PASSED| | 84 | | It is not possible to bind more than `MAX_BOUND_TOKENS`. | [`getNumTokens_less_or_equal_MAX_BOUND_TOKENS`](echidna/TBPoolBind.sol#L40-L43) | [`TBPoolBind`](echidna/TBPoolBind.sol) |PASSED| | 85 | | It is not possible to bind more than once the same token. | [`bind_twice`](echidna/TBPoolBind.sol#L45-L54) | [`TBPoolBind`](echidna/TBPoolBind.sol) |PASSED| | 86 | | It is not possible to unbind more than once the same token. | [`unbind_twice`](echidna/TBPoolBind.sol#L56-L66) | [`TBPoolBind`](echidna/TBPoolBind.sol) |PASSED| | 87 | | It is always possible to unbind a token. | [`all_tokens_are_unbindable`](echidna/TBPoolBind.sol#L68-L81) | [`TBPoolBind`](echidna/TBPoolBind.sol) |PASSED| | 88 | | All tokens are rebindable with valid parameters. | [`all_tokens_are_rebindable_with_valid_parameters`](echidna/TBPoolBind.sol#L83-L95) | [`TBPoolBind`](echidna/TBPoolBind.sol) |PASSED| | 89 | | It is not possible to rebind an unbinded token. | [`rebind_unbinded`](echidna/TBPoolBind.sol#L97-L107) | [`TBPoolBind`](echidna/TBPoolBind.sol) |PASSED| | 90 | | Only the controller can bind. | [`when_bind`](echidna/TBPoolBind.sol#L150-L154) and [`only_controller_can_bind`](echidna/TBPoolBind.sol#L145-L148) | [`TBPoolBind`](echidna/TBPoolBind.sol) |PASSED| | 91 | | If a user that is not the controller, tries to bind, rebind or unbind, the operation will revert. | [`when_bind`](echidna/TBPoolBind.sol#L150-L154), [`when_rebind`](echidna/TBPoolBind.sol#L150-L154) and [`when_unbind`](echidna/TBPoolBind.sol#L163-L168) | [`TBPoolBind`](echidna/TBPoolBind.sol) |PASSED| | 92 | | Transfer tokens to the null address (`0x0`) causes a revert | [`transfer_to_zero`](echidna/TBTokenERC20.sol#L75-L79) and [`transferFrom_to_zero`](echidna/TBTokenERC20.sol#L85-L89) | [`TBTokenERC20`](echidna/TBTokenERC20.sol) |FAILED ([#197](https://github.com/balancer-labs/balancer-core/issues/197))| **WONT FIX** | 93 | | The null address (`0x0`) owns no tokens | [`zero_always_empty`](echidna/TBTokenERC20.sol#L34-L36) | [`TBTokenERC20`](echidna/TBTokenERC20.sol) |FAILED| **WONT FIX** | 94 | | Transfer a valid amout of tokens to non-null address reduces the current balance | [`transferFrom_to_other`](echidna/TBTokenERC20.sol#L108-L113) and [`transfer_to_other`](echidna/TBTokenERC20.sol#L131-L142) | [`TBTokenERC20`](echidna/TBTokenERC20.sol) |PASSED| | 95 | | Transfer an invalid amout of tokens to non-null address reverts or returns false | [`transfer_to_user`](echidna/TBTokenERC20.sol#L149-L155) | [`TBTokenERC20`](echidna/TBTokenERC20.sol) |PASSED| | 96 | | Self transfer a valid amout of tokens keeps the current balance constant | [`self_transferFrom`](echidna/TBTokenERC20.sol#L96-L101) and [`self_transfer`](echidna/TBTokenERC20.sol#L120-L124) | [`TBTokenERC20`](echidna/TBTokenERC20.sol) |PASSED| | 97 | | Approving overwrites the previous allowance value | [`approve_overwrites`](echidna/TBTokenERC20.sol#L42-L49) | [`TBTokenERC20`](echidna/TBTokenERC20.sol) |PASSED| | 98 | | The `totalSupply` is a constant | [`totalSupply_constant`](echidna/TBTokenERC20.sol#L166-L168) | [`TBTokenERC20`](echidna/TBTokenERC20.sol) |PASSED| | 99 | | The balances are consistent with the `totalSupply` | [`totalSupply_balances_consistency`](echidna/TBTokenERC20.sol#L63-L65) and [`balance_less_than_totalSupply`](echidna/TBTokenERC20.sol#L55-L57) | [`TBTokenERC20`](echidna/TBTokenERC20.sol) |PASSED| | 100 | 101 | # Code verification with Manticore 102 | 103 | The following properties have equivalent Echidna property, but Manticore allows to either prove the absence of bugs, or look for an upper bound. 104 | 105 | To execute the script, run `python3 ./manticore/script_name.py`. 106 | 107 | | Description | Script | Contract | Status | 108 | | :--- | :---: | :---: | :---: | 109 | | An attacker cannot generate free pool tokens with `joinPool`. | [`TBPoolJoinPool.py`](manticore/TBPoolJoinPool.py)| [`TBPoolJoinPool`](manticore/contracts/TBPoolJoinPool.sol) | **FAILED** ([#204](https://github.com/balancer-labs/balancer-core/issues/204)) | 110 | | Calling `joinPool-exitPool` does not lead to free pool tokens (no fee). | [`TBPoolJoinExitNoFee.py`](manticore/TBPoolJoinExitNoFee.py) | [`TBPoolJoinExitPoolNoFee`](manticore/contracts/TBPoolJoinExitPoolNoFee.sol) |**FAILED** ([#205](https://github.com/balancer-labs/balancer-core/issues/205)) | 111 | | Calling `joinPool-exitPool` does not lead to free pool tokens (with fee).| [`TBPoolJoinExit.py`](manticore/TBPoolJoinExit.py) | [`TBPoolJoinExit`](manticore/contracts/TBPoolJoinExitPool.sol) |**FAILED** ([#205](https://github.com/balancer-labs/balancer-core/issues/205))| 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | balancer pebbles logo 3 |

4 | 5 |

6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |

16 | 17 |

balancer

18 | 19 | **Balancer** is an automated **portfolio manager**, **liquidity provider**, and **price sensor**. 20 | 21 | Balancer turns the concept of an index fund on its head: instead of a paying fees 22 | to portfolio managers to rebalance your portfolio, you collect fees from traders, who rebalance 23 | your portfolio by following arbitrage opportunities. 24 | 25 | Balancer is based on an N-dimensional invariant surface which is a generalization of the constant product formula described by Vitalik Buterin and proven viable by the popular Uniswap dapp. 26 | 27 | ## 🍂 Bronze Release 🍂 28 | 29 | The *🍂Bronze Release🍂* is the first of 3 planned releases of the Balancer Protocol. Bronze emphasizes code clarity for audit and verification, and does not go to great lengths to optimize for gas. 30 | 31 | The *❄️Silver Release❄️* will bring many gas optimizations and architecture changes that will reduce transaction overhead and enable more flexibility for managed pools. 32 | 33 | The *☀️Golden Release☀️* will introduce a curious new liquidity mechanism to the market. 34 | 35 | ## Documentation 36 | 37 | The full documentation can be found at [https://docs.balancer.finance](https://docs.balancer.finance) 38 | 39 | 40 | ## Development 41 | 42 | Most users will want to consume the ABI definitions for BFactory and BPool. 43 | 44 | This project follows the standard Truffle project structure. 45 | 46 | ``` 47 | yarn compile # build artifacts to `build/contracts` 48 | yarn testrpc # run ganache 49 | yarn test # run the tests 50 | ``` 51 | 52 | Tests can be run verbosely to view approximation diffs: 53 | 54 | ``` 55 | yarn test:verbose 56 | ``` 57 | 58 | ``` 59 | Contract: BPool 60 | With fees 61 | pAi 62 | expected: 10.891089108910892) 63 | actual : 10.891089106783580001) 64 | relDif : 1.9532588879656032e-10) 65 | Pool Balance 66 | expected: 98010000000000030000) 67 | actual : 98010000001320543977) 68 | relDif : 1.3473294888276702e-11) 69 | Dirt Balance 70 | expected: 3921200210105053000) 71 | actual : 3921200210099248361) 72 | relDif : 1.480428360949332e-12) 73 | Rock Balance 74 | expected: 11763600630315160000) 75 | actual : 11763600630334527239) 76 | relDif : 1.6464292361378058e-12) 77 | ✓ exitswap_ExternAmountOut (537ms) 78 | ``` 79 | 80 | Complete API docs are available at [https://docs.balancer.finance/smart-contracts/api](https://docs.balancer.finance/smart-contracts/api) 81 | 82 | 83 |

84 | -------------------------------------------------------------------------------- /Trail of Bits Full Audit.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/balancer/balancer-core/f4ed5d65362a8d6cec21662fb6eae233b0babc1f/Trail of Bits Full Audit.pdf -------------------------------------------------------------------------------- /contracts/BColor.sol: -------------------------------------------------------------------------------- 1 | // This program is free software: you can redistribute it and/or modify 2 | // it under the terms of the GNU General Public License as published by 3 | // the Free Software Foundation, either version 3 of the License, or 4 | // (at your option) any later version. 5 | 6 | // This program is distributed in the hope that it will be useful, 7 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | // GNU General Public License for more details. 10 | 11 | // You should have received a copy of the GNU General Public License 12 | // along with this program. If not, see . 13 | 14 | pragma solidity 0.5.12; 15 | 16 | contract BColor { 17 | function getColor() 18 | external view 19 | returns (bytes32); 20 | } 21 | 22 | contract BBronze is BColor { 23 | function getColor() 24 | external view 25 | returns (bytes32) { 26 | return bytes32("BRONZE"); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /contracts/BConst.sol: -------------------------------------------------------------------------------- 1 | // This program is free software: you can redistribute it and/or modify 2 | // it under the terms of the GNU General Public License as published by 3 | // the Free Software Foundation, either version 3 of the License, or 4 | // (at your option) any later version. 5 | 6 | // This program is distributed in the hope that it will be useful, 7 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | // GNU General Public License for more details. 10 | 11 | // You should have received a copy of the GNU General Public License 12 | // along with this program. If not, see . 13 | 14 | pragma solidity 0.5.12; 15 | 16 | import "./BColor.sol"; 17 | 18 | contract BConst is BBronze { 19 | uint public constant BONE = 10**18; 20 | 21 | uint public constant MIN_BOUND_TOKENS = 2; 22 | uint public constant MAX_BOUND_TOKENS = 8; 23 | 24 | uint public constant MIN_FEE = BONE / 10**6; 25 | uint public constant MAX_FEE = BONE / 10; 26 | uint public constant EXIT_FEE = 0; 27 | 28 | uint public constant MIN_WEIGHT = BONE; 29 | uint public constant MAX_WEIGHT = BONE * 50; 30 | uint public constant MAX_TOTAL_WEIGHT = BONE * 50; 31 | uint public constant MIN_BALANCE = BONE / 10**12; 32 | 33 | uint public constant INIT_POOL_SUPPLY = BONE * 100; 34 | 35 | uint public constant MIN_BPOW_BASE = 1 wei; 36 | uint public constant MAX_BPOW_BASE = (2 * BONE) - 1 wei; 37 | uint public constant BPOW_PRECISION = BONE / 10**10; 38 | 39 | uint public constant MAX_IN_RATIO = BONE / 2; 40 | uint public constant MAX_OUT_RATIO = (BONE / 3) + 1 wei; 41 | } 42 | -------------------------------------------------------------------------------- /contracts/BFactory.sol: -------------------------------------------------------------------------------- 1 | // This program is free software: you can redistribute it and/or modify 2 | // it under the terms of the GNU General Public License as published by 3 | // the Free Software Foundation, either version 3 of the License, or 4 | // (at your option) any later version. 5 | 6 | // This program is disstributed in the hope that it will be useful, 7 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | // GNU General Public License for more details. 10 | 11 | // You should have received a copy of the GNU General Public License 12 | // along with this program. If not, see . 13 | 14 | pragma solidity 0.5.12; 15 | 16 | // Builds new BPools, logging their addresses and providing `isBPool(address) -> (bool)` 17 | 18 | import "./BPool.sol"; 19 | 20 | contract BFactory is BBronze { 21 | event LOG_NEW_POOL( 22 | address indexed caller, 23 | address indexed pool 24 | ); 25 | 26 | event LOG_BLABS( 27 | address indexed caller, 28 | address indexed blabs 29 | ); 30 | 31 | mapping(address=>bool) private _isBPool; 32 | 33 | function isBPool(address b) 34 | external view returns (bool) 35 | { 36 | return _isBPool[b]; 37 | } 38 | 39 | function newBPool() 40 | external 41 | returns (BPool) 42 | { 43 | BPool bpool = new BPool(); 44 | _isBPool[address(bpool)] = true; 45 | emit LOG_NEW_POOL(msg.sender, address(bpool)); 46 | bpool.setController(msg.sender); 47 | return bpool; 48 | } 49 | 50 | address private _blabs; 51 | 52 | constructor() public { 53 | _blabs = msg.sender; 54 | } 55 | 56 | function getBLabs() 57 | external view 58 | returns (address) 59 | { 60 | return _blabs; 61 | } 62 | 63 | function setBLabs(address b) 64 | external 65 | { 66 | require(msg.sender == _blabs, "ERR_NOT_BLABS"); 67 | emit LOG_BLABS(msg.sender, b); 68 | _blabs = b; 69 | } 70 | 71 | function collect(BPool pool) 72 | external 73 | { 74 | require(msg.sender == _blabs, "ERR_NOT_BLABS"); 75 | uint collected = IERC20(pool).balanceOf(address(this)); 76 | bool xfer = pool.transfer(_blabs, collected); 77 | require(xfer, "ERR_ERC20_FAILED"); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /contracts/BMath.sol: -------------------------------------------------------------------------------- 1 | // This program is free software: you can redistribute it and/or modify 2 | // it under the terms of the GNU General Public License as published by 3 | // the Free Software Foundation, either version 3 of the License, or 4 | // (at your option) any later version. 5 | 6 | // This program is distributed in the hope that it will be useful, 7 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | // GNU General Public License for more details. 10 | 11 | // You should have received a copy of the GNU General Public License 12 | // along with this program. If not, see . 13 | 14 | pragma solidity 0.5.12; 15 | 16 | import "./BNum.sol"; 17 | 18 | contract BMath is BBronze, BConst, BNum { 19 | /********************************************************************************************** 20 | // calcSpotPrice // 21 | // sP = spotPrice // 22 | // bI = tokenBalanceIn ( bI / wI ) 1 // 23 | // bO = tokenBalanceOut sP = ----------- * ---------- // 24 | // wI = tokenWeightIn ( bO / wO ) ( 1 - sF ) // 25 | // wO = tokenWeightOut // 26 | // sF = swapFee // 27 | **********************************************************************************************/ 28 | function calcSpotPrice( 29 | uint tokenBalanceIn, 30 | uint tokenWeightIn, 31 | uint tokenBalanceOut, 32 | uint tokenWeightOut, 33 | uint swapFee 34 | ) 35 | public pure 36 | returns (uint spotPrice) 37 | { 38 | uint numer = bdiv(tokenBalanceIn, tokenWeightIn); 39 | uint denom = bdiv(tokenBalanceOut, tokenWeightOut); 40 | uint ratio = bdiv(numer, denom); 41 | uint scale = bdiv(BONE, bsub(BONE, swapFee)); 42 | return (spotPrice = bmul(ratio, scale)); 43 | } 44 | 45 | /********************************************************************************************** 46 | // calcOutGivenIn // 47 | // aO = tokenAmountOut // 48 | // bO = tokenBalanceOut // 49 | // bI = tokenBalanceIn / / bI \ (wI / wO) \ // 50 | // aI = tokenAmountIn aO = bO * | 1 - | -------------------------- | ^ | // 51 | // wI = tokenWeightIn \ \ ( bI + ( aI * ( 1 - sF )) / / // 52 | // wO = tokenWeightOut // 53 | // sF = swapFee // 54 | **********************************************************************************************/ 55 | function calcOutGivenIn( 56 | uint tokenBalanceIn, 57 | uint tokenWeightIn, 58 | uint tokenBalanceOut, 59 | uint tokenWeightOut, 60 | uint tokenAmountIn, 61 | uint swapFee 62 | ) 63 | public pure 64 | returns (uint tokenAmountOut) 65 | { 66 | uint weightRatio = bdiv(tokenWeightIn, tokenWeightOut); 67 | uint adjustedIn = bsub(BONE, swapFee); 68 | adjustedIn = bmul(tokenAmountIn, adjustedIn); 69 | uint y = bdiv(tokenBalanceIn, badd(tokenBalanceIn, adjustedIn)); 70 | uint foo = bpow(y, weightRatio); 71 | uint bar = bsub(BONE, foo); 72 | tokenAmountOut = bmul(tokenBalanceOut, bar); 73 | return tokenAmountOut; 74 | } 75 | 76 | /********************************************************************************************** 77 | // calcInGivenOut // 78 | // aI = tokenAmountIn // 79 | // bO = tokenBalanceOut / / bO \ (wO / wI) \ // 80 | // bI = tokenBalanceIn bI * | | ------------ | ^ - 1 | // 81 | // aO = tokenAmountOut aI = \ \ ( bO - aO ) / / // 82 | // wI = tokenWeightIn -------------------------------------------- // 83 | // wO = tokenWeightOut ( 1 - sF ) // 84 | // sF = swapFee // 85 | **********************************************************************************************/ 86 | function calcInGivenOut( 87 | uint tokenBalanceIn, 88 | uint tokenWeightIn, 89 | uint tokenBalanceOut, 90 | uint tokenWeightOut, 91 | uint tokenAmountOut, 92 | uint swapFee 93 | ) 94 | public pure 95 | returns (uint tokenAmountIn) 96 | { 97 | uint weightRatio = bdiv(tokenWeightOut, tokenWeightIn); 98 | uint diff = bsub(tokenBalanceOut, tokenAmountOut); 99 | uint y = bdiv(tokenBalanceOut, diff); 100 | uint foo = bpow(y, weightRatio); 101 | foo = bsub(foo, BONE); 102 | tokenAmountIn = bsub(BONE, swapFee); 103 | tokenAmountIn = bdiv(bmul(tokenBalanceIn, foo), tokenAmountIn); 104 | return tokenAmountIn; 105 | } 106 | 107 | /********************************************************************************************** 108 | // calcPoolOutGivenSingleIn // 109 | // pAo = poolAmountOut / \ // 110 | // tAi = tokenAmountIn /// / // wI \ \\ \ wI \ // 111 | // wI = tokenWeightIn //| tAi *| 1 - || 1 - -- | * sF || + tBi \ -- \ // 112 | // tW = totalWeight pAo=|| \ \ \\ tW / // | ^ tW | * pS - pS // 113 | // tBi = tokenBalanceIn \\ ------------------------------------- / / // 114 | // pS = poolSupply \\ tBi / / // 115 | // sF = swapFee \ / // 116 | **********************************************************************************************/ 117 | function calcPoolOutGivenSingleIn( 118 | uint tokenBalanceIn, 119 | uint tokenWeightIn, 120 | uint poolSupply, 121 | uint totalWeight, 122 | uint tokenAmountIn, 123 | uint swapFee 124 | ) 125 | public pure 126 | returns (uint poolAmountOut) 127 | { 128 | // Charge the trading fee for the proportion of tokenAi 129 | /// which is implicitly traded to the other pool tokens. 130 | // That proportion is (1- weightTokenIn) 131 | // tokenAiAfterFee = tAi * (1 - (1-weightTi) * poolFee); 132 | uint normalizedWeight = bdiv(tokenWeightIn, totalWeight); 133 | uint zaz = bmul(bsub(BONE, normalizedWeight), swapFee); 134 | uint tokenAmountInAfterFee = bmul(tokenAmountIn, bsub(BONE, zaz)); 135 | 136 | uint newTokenBalanceIn = badd(tokenBalanceIn, tokenAmountInAfterFee); 137 | uint tokenInRatio = bdiv(newTokenBalanceIn, tokenBalanceIn); 138 | 139 | // uint newPoolSupply = (ratioTi ^ weightTi) * poolSupply; 140 | uint poolRatio = bpow(tokenInRatio, normalizedWeight); 141 | uint newPoolSupply = bmul(poolRatio, poolSupply); 142 | poolAmountOut = bsub(newPoolSupply, poolSupply); 143 | return poolAmountOut; 144 | } 145 | 146 | /********************************************************************************************** 147 | // calcSingleInGivenPoolOut // 148 | // tAi = tokenAmountIn //(pS + pAo)\ / 1 \\ // 149 | // pS = poolSupply || --------- | ^ | --------- || * bI - bI // 150 | // pAo = poolAmountOut \\ pS / \(wI / tW)// // 151 | // bI = balanceIn tAi = -------------------------------------------- // 152 | // wI = weightIn / wI \ // 153 | // tW = totalWeight | 1 - ---- | * sF // 154 | // sF = swapFee \ tW / // 155 | **********************************************************************************************/ 156 | function calcSingleInGivenPoolOut( 157 | uint tokenBalanceIn, 158 | uint tokenWeightIn, 159 | uint poolSupply, 160 | uint totalWeight, 161 | uint poolAmountOut, 162 | uint swapFee 163 | ) 164 | public pure 165 | returns (uint tokenAmountIn) 166 | { 167 | uint normalizedWeight = bdiv(tokenWeightIn, totalWeight); 168 | uint newPoolSupply = badd(poolSupply, poolAmountOut); 169 | uint poolRatio = bdiv(newPoolSupply, poolSupply); 170 | 171 | //uint newBalTi = poolRatio^(1/weightTi) * balTi; 172 | uint boo = bdiv(BONE, normalizedWeight); 173 | uint tokenInRatio = bpow(poolRatio, boo); 174 | uint newTokenBalanceIn = bmul(tokenInRatio, tokenBalanceIn); 175 | uint tokenAmountInAfterFee = bsub(newTokenBalanceIn, tokenBalanceIn); 176 | // Do reverse order of fees charged in joinswap_ExternAmountIn, this way 177 | // ``` pAo == joinswap_ExternAmountIn(Ti, joinswap_PoolAmountOut(pAo, Ti)) ``` 178 | //uint tAi = tAiAfterFee / (1 - (1-weightTi) * swapFee) ; 179 | uint zar = bmul(bsub(BONE, normalizedWeight), swapFee); 180 | tokenAmountIn = bdiv(tokenAmountInAfterFee, bsub(BONE, zar)); 181 | return tokenAmountIn; 182 | } 183 | 184 | /********************************************************************************************** 185 | // calcSingleOutGivenPoolIn // 186 | // tAo = tokenAmountOut / / \\ // 187 | // bO = tokenBalanceOut / // pS - (pAi * (1 - eF)) \ / 1 \ \\ // 188 | // pAi = poolAmountIn | bO - || ----------------------- | ^ | --------- | * b0 || // 189 | // ps = poolSupply \ \\ pS / \(wO / tW)/ // // 190 | // wI = tokenWeightIn tAo = \ \ // // 191 | // tW = totalWeight / / wO \ \ // 192 | // sF = swapFee * | 1 - | 1 - ---- | * sF | // 193 | // eF = exitFee \ \ tW / / // 194 | **********************************************************************************************/ 195 | function calcSingleOutGivenPoolIn( 196 | uint tokenBalanceOut, 197 | uint tokenWeightOut, 198 | uint poolSupply, 199 | uint totalWeight, 200 | uint poolAmountIn, 201 | uint swapFee 202 | ) 203 | public pure 204 | returns (uint tokenAmountOut) 205 | { 206 | uint normalizedWeight = bdiv(tokenWeightOut, totalWeight); 207 | // charge exit fee on the pool token side 208 | // pAiAfterExitFee = pAi*(1-exitFee) 209 | uint poolAmountInAfterExitFee = bmul(poolAmountIn, bsub(BONE, EXIT_FEE)); 210 | uint newPoolSupply = bsub(poolSupply, poolAmountInAfterExitFee); 211 | uint poolRatio = bdiv(newPoolSupply, poolSupply); 212 | 213 | // newBalTo = poolRatio^(1/weightTo) * balTo; 214 | uint tokenOutRatio = bpow(poolRatio, bdiv(BONE, normalizedWeight)); 215 | uint newTokenBalanceOut = bmul(tokenOutRatio, tokenBalanceOut); 216 | 217 | uint tokenAmountOutBeforeSwapFee = bsub(tokenBalanceOut, newTokenBalanceOut); 218 | 219 | // charge swap fee on the output token side 220 | //uint tAo = tAoBeforeSwapFee * (1 - (1-weightTo) * swapFee) 221 | uint zaz = bmul(bsub(BONE, normalizedWeight), swapFee); 222 | tokenAmountOut = bmul(tokenAmountOutBeforeSwapFee, bsub(BONE, zaz)); 223 | return tokenAmountOut; 224 | } 225 | 226 | /********************************************************************************************** 227 | // calcPoolInGivenSingleOut // 228 | // pAi = poolAmountIn // / tAo \\ / wO \ \ // 229 | // bO = tokenBalanceOut // | bO - -------------------------- |\ | ---- | \ // 230 | // tAo = tokenAmountOut pS - || \ 1 - ((1 - (tO / tW)) * sF)/ | ^ \ tW / * pS | // 231 | // ps = poolSupply \\ -----------------------------------/ / // 232 | // wO = tokenWeightOut pAi = \\ bO / / // 233 | // tW = totalWeight ------------------------------------------------------------- // 234 | // sF = swapFee ( 1 - eF ) // 235 | // eF = exitFee // 236 | **********************************************************************************************/ 237 | function calcPoolInGivenSingleOut( 238 | uint tokenBalanceOut, 239 | uint tokenWeightOut, 240 | uint poolSupply, 241 | uint totalWeight, 242 | uint tokenAmountOut, 243 | uint swapFee 244 | ) 245 | public pure 246 | returns (uint poolAmountIn) 247 | { 248 | 249 | // charge swap fee on the output token side 250 | uint normalizedWeight = bdiv(tokenWeightOut, totalWeight); 251 | //uint tAoBeforeSwapFee = tAo / (1 - (1-weightTo) * swapFee) ; 252 | uint zoo = bsub(BONE, normalizedWeight); 253 | uint zar = bmul(zoo, swapFee); 254 | uint tokenAmountOutBeforeSwapFee = bdiv(tokenAmountOut, bsub(BONE, zar)); 255 | 256 | uint newTokenBalanceOut = bsub(tokenBalanceOut, tokenAmountOutBeforeSwapFee); 257 | uint tokenOutRatio = bdiv(newTokenBalanceOut, tokenBalanceOut); 258 | 259 | //uint newPoolSupply = (ratioTo ^ weightTo) * poolSupply; 260 | uint poolRatio = bpow(tokenOutRatio, normalizedWeight); 261 | uint newPoolSupply = bmul(poolRatio, poolSupply); 262 | uint poolAmountInAfterExitFee = bsub(poolSupply, newPoolSupply); 263 | 264 | // charge exit fee on the pool token side 265 | // pAi = pAiAfterExitFee/(1-exitFee) 266 | poolAmountIn = bdiv(poolAmountInAfterExitFee, bsub(BONE, EXIT_FEE)); 267 | return poolAmountIn; 268 | } 269 | 270 | 271 | } 272 | -------------------------------------------------------------------------------- /contracts/BNum.sol: -------------------------------------------------------------------------------- 1 | // This program is free software: you can redistribute it and/or modify 2 | // it under the terms of the GNU General Public License as published by 3 | // the Free Software Foundation, either version 3 of the License, or 4 | // (at your option) any later version. 5 | 6 | // This program is distributed in the hope that it will be useful, 7 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | // GNU General Public License for more details. 10 | 11 | // You should have received a copy of the GNU General Public License 12 | // along with this program. If not, see . 13 | 14 | pragma solidity 0.5.12; 15 | 16 | import "./BConst.sol"; 17 | 18 | contract BNum is BConst { 19 | 20 | function btoi(uint a) 21 | internal pure 22 | returns (uint) 23 | { 24 | return a / BONE; 25 | } 26 | 27 | function bfloor(uint a) 28 | internal pure 29 | returns (uint) 30 | { 31 | return btoi(a) * BONE; 32 | } 33 | 34 | function badd(uint a, uint b) 35 | internal pure 36 | returns (uint) 37 | { 38 | uint c = a + b; 39 | require(c >= a, "ERR_ADD_OVERFLOW"); 40 | return c; 41 | } 42 | 43 | function bsub(uint a, uint b) 44 | internal pure 45 | returns (uint) 46 | { 47 | (uint c, bool flag) = bsubSign(a, b); 48 | require(!flag, "ERR_SUB_UNDERFLOW"); 49 | return c; 50 | } 51 | 52 | function bsubSign(uint a, uint b) 53 | internal pure 54 | returns (uint, bool) 55 | { 56 | if (a >= b) { 57 | return (a - b, false); 58 | } else { 59 | return (b - a, true); 60 | } 61 | } 62 | 63 | function bmul(uint a, uint b) 64 | internal pure 65 | returns (uint) 66 | { 67 | uint c0 = a * b; 68 | require(a == 0 || c0 / a == b, "ERR_MUL_OVERFLOW"); 69 | uint c1 = c0 + (BONE / 2); 70 | require(c1 >= c0, "ERR_MUL_OVERFLOW"); 71 | uint c2 = c1 / BONE; 72 | return c2; 73 | } 74 | 75 | function bdiv(uint a, uint b) 76 | internal pure 77 | returns (uint) 78 | { 79 | require(b != 0, "ERR_DIV_ZERO"); 80 | uint c0 = a * BONE; 81 | require(a == 0 || c0 / a == BONE, "ERR_DIV_INTERNAL"); // bmul overflow 82 | uint c1 = c0 + (b / 2); 83 | require(c1 >= c0, "ERR_DIV_INTERNAL"); // badd require 84 | uint c2 = c1 / b; 85 | return c2; 86 | } 87 | 88 | // DSMath.wpow 89 | function bpowi(uint a, uint n) 90 | internal pure 91 | returns (uint) 92 | { 93 | uint z = n % 2 != 0 ? a : BONE; 94 | 95 | for (n /= 2; n != 0; n /= 2) { 96 | a = bmul(a, a); 97 | 98 | if (n % 2 != 0) { 99 | z = bmul(z, a); 100 | } 101 | } 102 | return z; 103 | } 104 | 105 | // Compute b^(e.w) by splitting it into (b^e)*(b^0.w). 106 | // Use `bpowi` for `b^e` and `bpowK` for k iterations 107 | // of approximation of b^0.w 108 | function bpow(uint base, uint exp) 109 | internal pure 110 | returns (uint) 111 | { 112 | require(base >= MIN_BPOW_BASE, "ERR_BPOW_BASE_TOO_LOW"); 113 | require(base <= MAX_BPOW_BASE, "ERR_BPOW_BASE_TOO_HIGH"); 114 | 115 | uint whole = bfloor(exp); 116 | uint remain = bsub(exp, whole); 117 | 118 | uint wholePow = bpowi(base, btoi(whole)); 119 | 120 | if (remain == 0) { 121 | return wholePow; 122 | } 123 | 124 | uint partialResult = bpowApprox(base, remain, BPOW_PRECISION); 125 | return bmul(wholePow, partialResult); 126 | } 127 | 128 | function bpowApprox(uint base, uint exp, uint precision) 129 | internal pure 130 | returns (uint) 131 | { 132 | // term 0: 133 | uint a = exp; 134 | (uint x, bool xneg) = bsubSign(base, BONE); 135 | uint term = BONE; 136 | uint sum = term; 137 | bool negative = false; 138 | 139 | 140 | // term(k) = numer / denom 141 | // = (product(a - i - 1, i=1-->k) * x^k) / (k!) 142 | // each iteration, multiply previous term by (a-(k-1)) * x / k 143 | // continue until term is less than precision 144 | for (uint i = 1; term >= precision; i++) { 145 | uint bigK = i * BONE; 146 | (uint c, bool cneg) = bsubSign(a, bsub(bigK, BONE)); 147 | term = bmul(term, bmul(c, x)); 148 | term = bdiv(term, bigK); 149 | if (term == 0) break; 150 | 151 | if (xneg) negative = !negative; 152 | if (cneg) negative = !negative; 153 | if (negative) { 154 | sum = bsub(sum, term); 155 | } else { 156 | sum = badd(sum, term); 157 | } 158 | } 159 | 160 | return sum; 161 | } 162 | 163 | } 164 | -------------------------------------------------------------------------------- /contracts/BToken.sol: -------------------------------------------------------------------------------- 1 | // This program is free software: you can redistribute it and/or modify 2 | // it under the terms of the GNU General Public License as published by 3 | // the Free Software Foundation, either version 3 of the License, or 4 | // (at your option) any later version. 5 | 6 | // This program is distributed in the hope that it will be useful, 7 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | // GNU General Public License for more details. 10 | 11 | // You should have received a copy of the GNU General Public License 12 | // along with this program. If not, see . 13 | 14 | pragma solidity 0.5.12; 15 | 16 | import "./BNum.sol"; 17 | 18 | // Highly opinionated token implementation 19 | 20 | interface IERC20 { 21 | event Approval(address indexed src, address indexed dst, uint amt); 22 | event Transfer(address indexed src, address indexed dst, uint amt); 23 | 24 | function totalSupply() external view returns (uint); 25 | function balanceOf(address whom) external view returns (uint); 26 | function allowance(address src, address dst) external view returns (uint); 27 | 28 | function approve(address dst, uint amt) external returns (bool); 29 | function transfer(address dst, uint amt) external returns (bool); 30 | function transferFrom( 31 | address src, address dst, uint amt 32 | ) external returns (bool); 33 | } 34 | 35 | contract BTokenBase is BNum { 36 | 37 | mapping(address => uint) internal _balance; 38 | mapping(address => mapping(address=>uint)) internal _allowance; 39 | uint internal _totalSupply; 40 | 41 | event Approval(address indexed src, address indexed dst, uint amt); 42 | event Transfer(address indexed src, address indexed dst, uint amt); 43 | 44 | function _mint(uint amt) internal { 45 | _balance[address(this)] = badd(_balance[address(this)], amt); 46 | _totalSupply = badd(_totalSupply, amt); 47 | emit Transfer(address(0), address(this), amt); 48 | } 49 | 50 | function _burn(uint amt) internal { 51 | require(_balance[address(this)] >= amt, "ERR_INSUFFICIENT_BAL"); 52 | _balance[address(this)] = bsub(_balance[address(this)], amt); 53 | _totalSupply = bsub(_totalSupply, amt); 54 | emit Transfer(address(this), address(0), amt); 55 | } 56 | 57 | function _move(address src, address dst, uint amt) internal { 58 | require(_balance[src] >= amt, "ERR_INSUFFICIENT_BAL"); 59 | _balance[src] = bsub(_balance[src], amt); 60 | _balance[dst] = badd(_balance[dst], amt); 61 | emit Transfer(src, dst, amt); 62 | } 63 | 64 | function _push(address to, uint amt) internal { 65 | _move(address(this), to, amt); 66 | } 67 | 68 | function _pull(address from, uint amt) internal { 69 | _move(from, address(this), amt); 70 | } 71 | } 72 | 73 | contract BToken is BTokenBase, IERC20 { 74 | 75 | string private _name = "Balancer Pool Token"; 76 | string private _symbol = "BPT"; 77 | uint8 private _decimals = 18; 78 | 79 | function name() public view returns (string memory) { 80 | return _name; 81 | } 82 | 83 | function symbol() public view returns (string memory) { 84 | return _symbol; 85 | } 86 | 87 | function decimals() public view returns(uint8) { 88 | return _decimals; 89 | } 90 | 91 | function allowance(address src, address dst) external view returns (uint) { 92 | return _allowance[src][dst]; 93 | } 94 | 95 | function balanceOf(address whom) external view returns (uint) { 96 | return _balance[whom]; 97 | } 98 | 99 | function totalSupply() public view returns (uint) { 100 | return _totalSupply; 101 | } 102 | 103 | function approve(address dst, uint amt) external returns (bool) { 104 | _allowance[msg.sender][dst] = amt; 105 | emit Approval(msg.sender, dst, amt); 106 | return true; 107 | } 108 | 109 | function increaseApproval(address dst, uint amt) external returns (bool) { 110 | _allowance[msg.sender][dst] = badd(_allowance[msg.sender][dst], amt); 111 | emit Approval(msg.sender, dst, _allowance[msg.sender][dst]); 112 | return true; 113 | } 114 | 115 | function decreaseApproval(address dst, uint amt) external returns (bool) { 116 | uint oldValue = _allowance[msg.sender][dst]; 117 | if (amt > oldValue) { 118 | _allowance[msg.sender][dst] = 0; 119 | } else { 120 | _allowance[msg.sender][dst] = bsub(oldValue, amt); 121 | } 122 | emit Approval(msg.sender, dst, _allowance[msg.sender][dst]); 123 | return true; 124 | } 125 | 126 | function transfer(address dst, uint amt) external returns (bool) { 127 | _move(msg.sender, dst, amt); 128 | return true; 129 | } 130 | 131 | function transferFrom(address src, address dst, uint amt) external returns (bool) { 132 | require(msg.sender == src || amt <= _allowance[src][msg.sender], "ERR_BTOKEN_BAD_CALLER"); 133 | _move(src, dst, amt); 134 | if (msg.sender != src && _allowance[src][msg.sender] != uint256(-1)) { 135 | _allowance[src][msg.sender] = bsub(_allowance[src][msg.sender], amt); 136 | emit Approval(msg.sender, dst, _allowance[src][msg.sender]); 137 | } 138 | return true; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.5.12; 2 | 3 | contract Migrations { 4 | address public owner; 5 | uint public lastCompletedMigration; 6 | 7 | constructor() public { 8 | owner = msg.sender; 9 | } 10 | 11 | modifier restricted() { 12 | if (msg.sender == owner) _; 13 | } 14 | 15 | function setCompleted(uint completed) external restricted { 16 | lastCompletedMigration = completed; 17 | } 18 | 19 | function upgrade(address new_address) external restricted { 20 | Migrations upgraded = Migrations(new_address); 21 | upgraded.setCompleted(lastCompletedMigration); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /contracts/test/TMath.sol: -------------------------------------------------------------------------------- 1 | // This program is free software: you can redistribute it and/or modify 2 | // it under the terms of the GNU General Public License as published by 3 | // the Free Software Foundation, either version 3 of the License, or 4 | // (at your option) any later version. 5 | 6 | // This program is distributed in the hope that it will be useful, 7 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | // GNU General Public License for more details. 10 | 11 | // You should have received a copy of the GNU General Public License 12 | // along with this program. If not, see . 13 | 14 | pragma solidity 0.5.12; 15 | 16 | import "../BMath.sol"; 17 | import "../BNum.sol"; 18 | 19 | // Contract to wrap internal functions for testing 20 | 21 | contract TMath is BMath { 22 | 23 | function calc_btoi(uint a) external pure returns (uint) { 24 | return btoi(a); 25 | } 26 | 27 | function calc_bfloor(uint a) external pure returns (uint) { 28 | return bfloor(a); 29 | } 30 | 31 | function calc_badd(uint a, uint b) external pure returns (uint) { 32 | return badd(a, b); 33 | } 34 | 35 | function calc_bsub(uint a, uint b) external pure returns (uint) { 36 | return bsub(a, b); 37 | } 38 | 39 | function calc_bsubSign(uint a, uint b) external pure returns (uint, bool) { 40 | return bsubSign(a, b); 41 | } 42 | 43 | function calc_bmul(uint a, uint b) external pure returns (uint) { 44 | return bmul(a, b); 45 | } 46 | 47 | function calc_bdiv(uint a, uint b) external pure returns (uint) { 48 | return bdiv(a, b); 49 | } 50 | 51 | function calc_bpowi(uint a, uint n) external pure returns (uint) { 52 | return bpowi(a, n); 53 | } 54 | 55 | function calc_bpow(uint base, uint exp) external pure returns (uint) { 56 | return bpow(base, exp); 57 | } 58 | 59 | function calc_bpowApprox(uint base, uint exp, uint precision) external pure returns (uint) { 60 | return bpowApprox(base, exp, precision); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /contracts/test/TToken.sol: -------------------------------------------------------------------------------- 1 | // This program is free software: you can redistribute it and/or modify 2 | // it under the terms of the GNU General Public License as published by 3 | // the Free Software Foundation, either version 3 of the License, or 4 | // (at your option) any later version. 5 | 6 | // This program is distributed in the hope that it will be useful, 7 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | // GNU General Public License for more details. 10 | 11 | // You should have received a copy of the GNU General Public License 12 | // along with this program. If not, see . 13 | 14 | pragma solidity 0.5.12; 15 | 16 | // Test Token 17 | 18 | contract TToken { 19 | 20 | string private _name; 21 | string private _symbol; 22 | uint8 private _decimals; 23 | 24 | address private _owner; 25 | 26 | uint internal _totalSupply; 27 | 28 | mapping(address => uint) private _balance; 29 | mapping(address => mapping(address=>uint)) private _allowance; 30 | 31 | modifier _onlyOwner_() { 32 | require(msg.sender == _owner, "ERR_NOT_OWNER"); 33 | _; 34 | } 35 | 36 | event Approval(address indexed src, address indexed dst, uint amt); 37 | event Transfer(address indexed src, address indexed dst, uint amt); 38 | 39 | // Math 40 | function add(uint a, uint b) internal pure returns (uint c) { 41 | require((c = a + b) >= a); 42 | } 43 | function sub(uint a, uint b) internal pure returns (uint c) { 44 | require((c = a - b) <= a); 45 | } 46 | 47 | constructor( 48 | string memory name, 49 | string memory symbol, 50 | uint8 decimals 51 | ) public { 52 | _name = name; 53 | _symbol = symbol; 54 | _decimals = decimals; 55 | _owner = msg.sender; 56 | } 57 | 58 | function name() public view returns (string memory) { 59 | return _name; 60 | } 61 | 62 | function symbol() public view returns (string memory) { 63 | return _symbol; 64 | } 65 | 66 | function decimals() public view returns(uint8) { 67 | return _decimals; 68 | } 69 | 70 | function _move(address src, address dst, uint amt) internal { 71 | require(_balance[src] >= amt, "ERR_INSUFFICIENT_BAL"); 72 | _balance[src] = sub(_balance[src], amt); 73 | _balance[dst] = add(_balance[dst], amt); 74 | emit Transfer(src, dst, amt); 75 | } 76 | 77 | function _push(address to, uint amt) internal { 78 | _move(address(this), to, amt); 79 | } 80 | 81 | function _pull(address from, uint amt) internal { 82 | _move(from, address(this), amt); 83 | } 84 | 85 | function _mint(address dst, uint amt) internal { 86 | _balance[dst] = add(_balance[dst], amt); 87 | _totalSupply = add(_totalSupply, amt); 88 | emit Transfer(address(0), dst, amt); 89 | } 90 | 91 | function allowance(address src, address dst) external view returns (uint) { 92 | return _allowance[src][dst]; 93 | } 94 | 95 | function balanceOf(address whom) external view returns (uint) { 96 | return _balance[whom]; 97 | } 98 | 99 | function totalSupply() public view returns (uint) { 100 | return _totalSupply; 101 | } 102 | 103 | function approve(address dst, uint amt) external returns (bool) { 104 | _allowance[msg.sender][dst] = amt; 105 | emit Approval(msg.sender, dst, amt); 106 | return true; 107 | } 108 | 109 | function mint(address dst, uint256 amt) public _onlyOwner_ returns (bool) { 110 | _mint(dst, amt); 111 | return true; 112 | } 113 | 114 | function burn(uint amt) public returns (bool) { 115 | require(_balance[address(this)] >= amt, "ERR_INSUFFICIENT_BAL"); 116 | _balance[address(this)] = sub(_balance[address(this)], amt); 117 | _totalSupply = sub(_totalSupply, amt); 118 | emit Transfer(address(this), address(0), amt); 119 | return true; 120 | } 121 | 122 | function transfer(address dst, uint amt) external returns (bool) { 123 | _move(msg.sender, dst, amt); 124 | return true; 125 | } 126 | 127 | function transferFrom(address src, address dst, uint amt) external returns (bool) { 128 | require(msg.sender == src || amt <= _allowance[src][msg.sender], "ERR_BTOKEN_BAD_CALLER"); 129 | _move(src, dst, amt); 130 | if (msg.sender != src && _allowance[src][msg.sender] != uint256(-1)) { 131 | _allowance[src][msg.sender] = sub(_allowance[src][msg.sender], amt); 132 | emit Approval(msg.sender, dst, _allowance[src][msg.sender]); 133 | } 134 | return true; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /contracts/test/echidna/TBPoolJoinExitPool.sol: -------------------------------------------------------------------------------- 1 | import "../../BNum.sol"; 2 | 3 | pragma solidity 0.5.12; 4 | 5 | // This test is similar to TBPoolJoin but with an exit fee 6 | contract TBPoolJoinExit is BNum { 7 | 8 | bool public echidna_no_bug_found = true; 9 | 10 | // joinPool models the BPool.joinPool behavior for one token 11 | function joinPool(uint poolAmountOut, uint poolTotal, uint _records_t_balance) 12 | internal pure returns(uint) 13 | { 14 | uint ratio = bdiv(poolAmountOut, poolTotal); 15 | require(ratio != 0, "ERR_MATH_APPROX"); 16 | 17 | uint bal = _records_t_balance; 18 | uint tokenAmountIn = bmul(ratio, bal); 19 | 20 | return tokenAmountIn; 21 | } 22 | 23 | // exitPool models the BPool.exitPool behavior for one token 24 | function exitPool(uint poolAmountIn, uint poolTotal, uint _records_t_balance) 25 | internal pure returns(uint) 26 | { 27 | uint exitFee = bmul(poolAmountIn, EXIT_FEE); 28 | uint pAiAfterExitFee = bsub(poolAmountIn, exitFee); 29 | uint ratio = bdiv(pAiAfterExitFee, poolTotal); 30 | require(ratio != 0, "ERR_MATH_APPROX"); 31 | 32 | uint bal = _records_t_balance; 33 | uint tokenAmountOut = bmul(ratio, bal); 34 | 35 | return tokenAmountOut; 36 | } 37 | 38 | 39 | // This function model an attacker calling joinPool - exitPool and taking advantage of potential rounding 40 | // issues to generate free pool token 41 | function joinAndExitPool(uint poolAmountOut, uint poolAmountIn, uint poolTotal, uint _records_t_balance) public { 42 | uint tokenAmountIn = joinPool(poolAmountOut, poolTotal, _records_t_balance); 43 | 44 | // We constraint poolTotal and _records_t_balance 45 | // To have "realistic" values 46 | require(poolTotal <= 100 ether); 47 | require(poolTotal >= 1 ether); 48 | require(_records_t_balance <= 10 ether); 49 | require(_records_t_balance >= 10**6); 50 | 51 | poolTotal = badd(poolTotal, poolAmountOut); 52 | _records_t_balance = badd(_records_t_balance, tokenAmountIn); 53 | 54 | require(tokenAmountIn > 0); // prevent triggering the free token generation from joinPool 55 | 56 | require(poolTotal >= poolAmountIn); 57 | uint tokenAmountOut = exitPool(poolAmountIn, poolTotal, _records_t_balance); 58 | require(_records_t_balance >= tokenAmountOut); 59 | 60 | // We try to generate free pool share 61 | require(poolAmountOut > poolAmountIn); 62 | require(tokenAmountOut == tokenAmountIn); 63 | echidna_no_bug_found = false; 64 | } 65 | 66 | } -------------------------------------------------------------------------------- /contracts/test/echidna/TBPoolJoinExitPoolNoFee.sol: -------------------------------------------------------------------------------- 1 | import "../../BNum.sol"; 2 | 3 | pragma solidity 0.5.12; 4 | 5 | // This test is similar to TBPoolJoinExit but with no exit fee 6 | contract TBPoolJoinExitNoFee is BNum { 7 | 8 | bool public echidna_no_bug_found = true; 9 | 10 | // joinPool models the BPool.joinPool behavior for one token 11 | function joinPool(uint poolAmountOut, uint poolTotal, uint _records_t_balance) 12 | internal pure returns(uint) 13 | { 14 | uint ratio = bdiv(poolAmountOut, poolTotal); 15 | require(ratio != 0, "ERR_MATH_APPROX"); 16 | 17 | uint bal = _records_t_balance; 18 | uint tokenAmountIn = bmul(ratio, bal); 19 | 20 | return tokenAmountIn; 21 | } 22 | 23 | // exitPool models the BPool.exitPool behavior for one token where no fee is applied 24 | function exitPoolNoFee(uint poolAmountIn, uint poolTotal, uint _records_t_balance) 25 | internal pure returns(uint) 26 | { 27 | uint ratio = bdiv(poolAmountIn, poolTotal); 28 | require(ratio != 0, "ERR_MATH_APPROX"); 29 | 30 | uint bal = _records_t_balance; 31 | uint tokenAmountOut = bmul(ratio, bal); 32 | 33 | return tokenAmountOut; 34 | } 35 | 36 | // This function model an attacker calling joinPool - exitPool and taking advantage of potential rounding 37 | // issues to generate free pool token 38 | function joinAndExitNoFeePool(uint poolAmountOut, uint poolAmountIn, uint poolTotal, uint _records_t_balance) 39 | public 40 | { 41 | uint tokenAmountIn = joinPool(poolAmountOut, poolTotal, _records_t_balance); 42 | 43 | // We constraint poolTotal and _records_t_balance 44 | // To have "realistic" values 45 | require(poolTotal <= 100 ether); 46 | require(poolTotal >= 1 ether); 47 | require(_records_t_balance <= 10 ether); 48 | require(_records_t_balance >= 10**6); 49 | 50 | poolTotal = badd(poolTotal, poolAmountOut); 51 | _records_t_balance = badd(_records_t_balance, tokenAmountIn); 52 | 53 | require(tokenAmountIn > 0); // prevent triggering the free token generation from joinPool 54 | 55 | require(poolTotal >= poolAmountIn); 56 | uint tokenAmountOut = exitPoolNoFee(poolAmountIn, poolTotal, _records_t_balance); 57 | require(_records_t_balance >= tokenAmountOut); 58 | 59 | // We try to generate free pool share 60 | require(poolAmountOut > poolAmountIn); 61 | require(tokenAmountOut == tokenAmountIn); 62 | echidna_no_bug_found = false; 63 | } 64 | 65 | 66 | } -------------------------------------------------------------------------------- /contracts/test/echidna/TBPoolJoinPool.sol: -------------------------------------------------------------------------------- 1 | import "../../BNum.sol"; 2 | 3 | pragma solidity 0.5.12; 4 | 5 | contract TBPoolJoinPool is BNum { 6 | 7 | bool public echidna_no_bug_found = true; 8 | 9 | // joinPool models the BPool.joinPool behavior for one token 10 | // A bug is found if poolAmountOut is greater than 0 11 | // And tokenAmountIn is 0 12 | function joinPool(uint poolAmountOut, uint poolTotal, uint _records_t_balance) 13 | public returns(uint) 14 | { 15 | // We constraint poolTotal and _records_t_balance 16 | // To have "realistic" values 17 | require(poolTotal <= 100 ether); 18 | require(poolTotal >= 1 ether); 19 | require(_records_t_balance <= 10 ether); 20 | require(_records_t_balance >= 10**6); 21 | 22 | uint ratio = bdiv(poolAmountOut, poolTotal); 23 | require(ratio != 0, "ERR_MATH_APPROX"); 24 | 25 | uint bal = _records_t_balance; 26 | uint tokenAmountIn = bmul(ratio, bal); 27 | 28 | require(poolAmountOut > 0); 29 | require(tokenAmountIn == 0); 30 | 31 | echidna_no_bug_found = false; 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /echidna/BMathInternal.sol: -------------------------------------------------------------------------------- 1 | /* 2 | This file is a flatenen version of BMath where all the public functions have been marked internal. 3 | This helps Echidna to focus on some specific properties. 4 | */ 5 | 6 | 7 | 8 | pragma solidity 0.5.12; 9 | contract BColor { 10 | function getColor() 11 | internal view 12 | returns (bytes32); 13 | } 14 | contract BBronze is BColor { 15 | function getColor() 16 | internal view 17 | returns (bytes32) { 18 | return bytes32("BRONZE"); 19 | } 20 | } 21 | contract BConst is BBronze { 22 | uint internal constant BONE = 10**18; 23 | 24 | uint internal constant MAX_BOUND_TOKENS = 8; 25 | uint internal constant BPOW_PRECISION = BONE / 10**10; 26 | 27 | uint internal constant MIN_FEE = BONE / 10**6; 28 | uint internal constant MAX_FEE = BONE / 10; 29 | uint internal constant EXIT_FEE = BONE / 10000; 30 | 31 | uint internal constant MIN_WEIGHT = BONE; 32 | uint internal constant MAX_WEIGHT = BONE * 50; 33 | uint internal constant MAX_TOTAL_WEIGHT = BONE * 50; 34 | uint internal constant MIN_BALANCE = BONE / 10**12; 35 | uint internal constant MAX_BALANCE = BONE * 10**12; 36 | 37 | uint internal constant MIN_POOL_SUPPLY = BONE; 38 | 39 | uint internal constant MIN_BPOW_BASE = 1 wei; 40 | uint internal constant MAX_BPOW_BASE = (2 * BONE) - 1 wei; 41 | 42 | uint internal constant MAX_IN_RATIO = BONE / 2; 43 | uint internal constant MAX_OUT_RATIO = (BONE / 3) + 1 wei; 44 | 45 | } 46 | contract BNum is BConst { 47 | 48 | function btoi(uint a) 49 | internal pure 50 | returns (uint) 51 | { 52 | return a / BONE; 53 | } 54 | 55 | function bfloor(uint a) 56 | internal pure 57 | returns (uint) 58 | { 59 | return btoi(a) * BONE; 60 | } 61 | 62 | function badd(uint a, uint b) 63 | internal pure 64 | returns (uint) 65 | { 66 | uint c = a + b; 67 | require(c >= a, "ERR_ADD_OVERFLOW"); 68 | return c; 69 | } 70 | 71 | function bsub(uint a, uint b) 72 | internal pure 73 | returns (uint) 74 | { 75 | (uint c, bool flag) = bsubSign(a, b); 76 | require(!flag, "ERR_SUB_UNDERFLOW"); 77 | return c; 78 | } 79 | 80 | function bsubSign(uint a, uint b) 81 | internal pure 82 | returns (uint, bool) 83 | { 84 | if (a >= b) { 85 | return (a - b, false); 86 | } else { 87 | return (b - a, true); 88 | } 89 | } 90 | 91 | function bmul(uint a, uint b) 92 | internal pure 93 | returns (uint) 94 | { 95 | uint c0 = a * b; 96 | require(a == 0 || c0 / a == b, "ERR_MUL_OVERFLOW"); 97 | uint c1 = c0 + (BONE / 2); 98 | require(c1 >= c0, "ERR_MUL_OVERFLOW"); 99 | uint c2 = c1 / BONE; 100 | return c2; 101 | } 102 | 103 | function bdiv(uint a, uint b) 104 | internal pure 105 | returns (uint) 106 | { 107 | require(b != 0, "ERR_DIV_ZERO"); 108 | uint c0 = a * BONE; 109 | require(a == 0 || c0 / a == BONE, "ERR_DIV_INTERNAL"); // bmul overflow 110 | uint c1 = c0 + (b / 2); 111 | require(c1 >= c0, "ERR_DIV_INTERNAL"); // badd require 112 | uint c2 = c1 / b; 113 | return c2; 114 | } 115 | 116 | // DSMath.wpow 117 | function bpowi(uint a, uint n) 118 | internal pure 119 | returns (uint) 120 | { 121 | uint z = n % 2 != 0 ? a : BONE; 122 | 123 | for (n /= 2; n != 0; n /= 2) { 124 | a = bmul(a, a); 125 | 126 | if (n % 2 != 0) { 127 | z = bmul(z, a); 128 | } 129 | } 130 | return z; 131 | } 132 | 133 | // Compute b^(e.w) by splitting it into (b^e)*(b^0.w). 134 | // Use `bpowi` for `b^e` and `bpowK` for k iterations 135 | // of approximation of b^0.w 136 | function bpow(uint base, uint exp) 137 | internal pure 138 | returns (uint) 139 | { 140 | require(base >= MIN_BPOW_BASE, "ERR_BPOW_BASE_TOO_LOW"); 141 | require(base <= MAX_BPOW_BASE, "ERR_BPOW_BASE_TOO_HIGH"); 142 | 143 | uint whole = bfloor(exp); 144 | uint remain = bsub(exp, whole); 145 | 146 | uint wholePow = bpowi(base, btoi(whole)); 147 | 148 | if (remain == 0) { 149 | return wholePow; 150 | } 151 | 152 | uint partialResult = bpowApprox(base, remain, BPOW_PRECISION); 153 | return bmul(wholePow, partialResult); 154 | } 155 | 156 | function bpowApprox(uint base, uint exp, uint precision) 157 | internal pure 158 | returns (uint) 159 | { 160 | // term 0: 161 | uint a = exp; 162 | (uint x, bool xneg) = bsubSign(base, BONE); 163 | uint term = BONE; 164 | uint sum = term; 165 | bool negative = false; 166 | 167 | 168 | // term(k) = numer / denom 169 | // = (product(a - i - 1, i=1-->k) * x^k) / (k!) 170 | // each iteration, multiply previous term by (a-(k-1)) * x / k 171 | // continue until term is less than precision 172 | for (uint i = 1; term >= precision; i++) { 173 | uint bigK = i * BONE; 174 | (uint c, bool cneg) = bsubSign(a, bsub(bigK, BONE)); 175 | term = bmul(term, bmul(c, x)); 176 | term = bdiv(term, bigK); 177 | if (term == 0) break; 178 | 179 | if (xneg) negative = !negative; 180 | if (cneg) negative = !negative; 181 | if (negative) { 182 | sum = bsub(sum, term); 183 | } else { 184 | sum = badd(sum, term); 185 | } 186 | } 187 | 188 | return sum; 189 | } 190 | 191 | } 192 | contract BMath is BBronze, BConst, BNum { 193 | /********************************************************************************************** 194 | // calcSpotPrice // 195 | // sP = spotPrice // 196 | // bI = tokenBalanceIn ( bI / wI ) 1 // 197 | // bO = tokenBalanceOut sP = ----------- * ---------- // 198 | // wI = tokenWeightIn ( bO / wO ) ( 1 - sF ) // 199 | // wO = tokenWeightOut // 200 | // sF = swapFee // 201 | **********************************************************************************************/ 202 | function calcSpotPrice( 203 | uint tokenBalanceIn, 204 | uint tokenWeightIn, 205 | uint tokenBalanceOut, 206 | uint tokenWeightOut, 207 | uint swapFee 208 | ) 209 | internal pure 210 | returns (uint spotPrice) 211 | { 212 | uint numer = bdiv(tokenBalanceIn, tokenWeightIn); 213 | uint denom = bdiv(tokenBalanceOut, tokenWeightOut); 214 | uint ratio = bdiv(numer, denom); 215 | uint scale = bdiv(BONE, bsub(BONE, swapFee)); 216 | return (spotPrice = bmul(ratio, scale)); 217 | } 218 | 219 | /********************************************************************************************** 220 | // calcOutGivenIn // 221 | // aO = tokenAmountOut // 222 | // bO = tokenBalanceOut // 223 | // bI = tokenBalanceIn / / bI \ (wI / wO) \ // 224 | // aI = tokenAmountIn aO = bO * | 1 - | -------------------------- | ^ | // 225 | // wI = tokenWeightIn \ \ ( bI + ( aI * ( 1 - sF )) / / // 226 | // wO = tokenWeightOut // 227 | // sF = swapFee // 228 | **********************************************************************************************/ 229 | function calcOutGivenIn( 230 | uint tokenBalanceIn, 231 | uint tokenWeightIn, 232 | uint tokenBalanceOut, 233 | uint tokenWeightOut, 234 | uint tokenAmountIn, 235 | uint swapFee 236 | ) 237 | internal pure 238 | returns (uint tokenAmountOut) 239 | { 240 | uint weightRatio = bdiv(tokenWeightIn, tokenWeightOut); 241 | uint adjustedIn = bsub(BONE, swapFee); 242 | adjustedIn = bmul(tokenAmountIn, adjustedIn); 243 | uint y = bdiv(tokenBalanceIn, badd(tokenBalanceIn, adjustedIn)); 244 | uint foo = bpow(y, weightRatio); 245 | uint bar = bsub(BONE, foo); 246 | tokenAmountOut = bmul(tokenBalanceOut, bar); 247 | return tokenAmountOut; 248 | } 249 | 250 | /********************************************************************************************** 251 | // calcInGivenOut // 252 | // aI = tokenAmountIn // 253 | // bO = tokenBalanceOut / / bO \ (wO / wI) \ // 254 | // bI = tokenBalanceIn bI * | | ------------ | ^ - 1 | // 255 | // aO = tokenAmountOut aI = \ \ ( bO - aO ) / / // 256 | // wI = tokenWeightIn -------------------------------------------- // 257 | // wO = tokenWeightOut ( 1 - sF ) // 258 | // sF = swapFee // 259 | **********************************************************************************************/ 260 | function calcInGivenOut( 261 | uint tokenBalanceIn, 262 | uint tokenWeightIn, 263 | uint tokenBalanceOut, 264 | uint tokenWeightOut, 265 | uint tokenAmountOut, 266 | uint swapFee 267 | ) 268 | internal pure 269 | returns (uint tokenAmountIn) 270 | { 271 | uint weightRatio = bdiv(tokenWeightOut, tokenWeightIn); 272 | uint diff = bsub(tokenBalanceOut, tokenAmountOut); 273 | uint y = bdiv(tokenBalanceOut, diff); 274 | uint foo = bpow(y, weightRatio); 275 | foo = bsub(foo, BONE); 276 | tokenAmountIn = bsub(BONE, swapFee); 277 | tokenAmountIn = bdiv(bmul(tokenBalanceIn, foo), tokenAmountIn); 278 | return tokenAmountIn; 279 | } 280 | 281 | /********************************************************************************************** 282 | // calcPoolOutGivenSingleIn // 283 | // pAo = poolAmountOut / \ // 284 | // tAi = tokenAmountIn /// / // wI \ \\ \ wI \ // 285 | // wI = tokenWeightIn //| tAi *| 1 - || 1 - -- | * sF || + tBi \ -- \ // 286 | // tW = totalWeight pAo=|| \ \ \\ tW / // | ^ tW | * pS - pS // 287 | // tBi = tokenBalanceIn \\ ------------------------------------- / / // 288 | // pS = poolSupply \\ tBi / / // 289 | // sF = swapFee \ / // 290 | **********************************************************************************************/ 291 | function calcPoolOutGivenSingleIn( 292 | uint tokenBalanceIn, 293 | uint tokenWeightIn, 294 | uint poolSupply, 295 | uint totalWeight, 296 | uint tokenAmountIn, 297 | uint swapFee 298 | ) 299 | internal pure 300 | returns (uint poolAmountOut) 301 | { 302 | // Charge the trading fee for the proportion of tokenAi 303 | /// which is implicitly traded to the other pool tokens. 304 | // That proportion is (1- weightTokenIn) 305 | // tokenAiAfterFee = tAi * (1 - (1-weightTi) * poolFee); 306 | uint normalizedWeight = bdiv(tokenWeightIn, totalWeight); 307 | uint zaz = bmul(bsub(BONE, normalizedWeight), swapFee); 308 | uint tokenAmountInAfterFee = bmul(tokenAmountIn, bsub(BONE, zaz)); 309 | 310 | uint newTokenBalanceIn = badd(tokenBalanceIn, tokenAmountInAfterFee); 311 | uint tokenInRatio = bdiv(newTokenBalanceIn, tokenBalanceIn); 312 | 313 | // uint newPoolSupply = (ratioTi ^ weightTi) * poolSupply; 314 | uint poolRatio = bpow(tokenInRatio, normalizedWeight); 315 | uint newPoolSupply = bmul(poolRatio, poolSupply); 316 | poolAmountOut = bsub(newPoolSupply, poolSupply); 317 | return poolAmountOut; 318 | } 319 | 320 | /********************************************************************************************** 321 | // calcSingleInGivenPoolOut // 322 | // tAi = tokenAmountIn //(pS + pAo)\ / 1 \\ // 323 | // pS = poolSupply || --------- | ^ | --------- || * bI - bI // 324 | // pAo = poolAmountOut \\ pS / \(wI / tW)// // 325 | // bI = balanceIn tAi = -------------------------------------------- // 326 | // wI = weightIn / wI \ // 327 | // tW = totalWeight | 1 - ---- | * sF // 328 | // sF = swapFee \ tW / // 329 | **********************************************************************************************/ 330 | function calcSingleInGivenPoolOut( 331 | uint tokenBalanceIn, 332 | uint tokenWeightIn, 333 | uint poolSupply, 334 | uint totalWeight, 335 | uint poolAmountOut, 336 | uint swapFee 337 | ) 338 | internal pure 339 | returns (uint tokenAmountIn) 340 | { 341 | uint normalizedWeight = bdiv(tokenWeightIn, totalWeight); 342 | uint newPoolSupply = badd(poolSupply, poolAmountOut); 343 | uint poolRatio = bdiv(newPoolSupply, poolSupply); 344 | 345 | //uint newBalTi = poolRatio^(1/weightTi) * balTi; 346 | uint boo = bdiv(BONE, normalizedWeight); 347 | uint tokenInRatio = bpow(poolRatio, boo); 348 | uint newTokenBalanceIn = bmul(tokenInRatio, tokenBalanceIn); 349 | uint tokenAmountInAfterFee = bsub(newTokenBalanceIn, tokenBalanceIn); 350 | // Do reverse order of fees charged in joinswap_ExternAmountIn, this way 351 | // ``` pAo == joinswap_ExternAmountIn(Ti, joinswap_PoolAmountOut(pAo, Ti)) ``` 352 | //uint tAi = tAiAfterFee / (1 - (1-weightTi) * swapFee) ; 353 | uint zar = bmul(bsub(BONE, normalizedWeight), swapFee); 354 | tokenAmountIn = bdiv(tokenAmountInAfterFee, bsub(BONE, zar)); 355 | return tokenAmountIn; 356 | } 357 | 358 | /********************************************************************************************** 359 | // calcSingleOutGivenPoolIn // 360 | // tAo = tokenAmountOut / / \\ // 361 | // bO = tokenBalanceOut / // pS - (pAi * (1 - eF)) \ / 1 \ \\ // 362 | // pAi = poolAmountIn | bO - || ----------------------- | ^ | --------- | * b0 || // 363 | // ps = poolSupply \ \\ pS / \(wO / tW)/ // // 364 | // wI = tokenWeightIn tAo = \ \ // // 365 | // tW = totalWeight / / wO \ \ // 366 | // sF = swapFee * | 1 - | 1 - ---- | * sF | // 367 | // eF = exitFee \ \ tW / / // 368 | **********************************************************************************************/ 369 | function calcSingleOutGivenPoolIn( 370 | uint tokenBalanceOut, 371 | uint tokenWeightOut, 372 | uint poolSupply, 373 | uint totalWeight, 374 | uint poolAmountIn, 375 | uint swapFee 376 | ) 377 | internal pure 378 | returns (uint tokenAmountOut) 379 | { 380 | uint normalizedWeight = bdiv(tokenWeightOut, totalWeight); 381 | // charge exit fee on the pool token side 382 | // pAiAfterExitFee = pAi*(1-exitFee) 383 | uint poolAmountInAfterExitFee = bmul(poolAmountIn, bsub(BONE, EXIT_FEE)); 384 | uint newPoolSupply = bsub(poolSupply, poolAmountInAfterExitFee); 385 | uint poolRatio = bdiv(newPoolSupply, poolSupply); 386 | 387 | // newBalTo = poolRatio^(1/weightTo) * balTo; 388 | uint tokenOutRatio = bpow(poolRatio, bdiv(BONE, normalizedWeight)); 389 | uint newTokenBalanceOut = bmul(tokenOutRatio, tokenBalanceOut); 390 | 391 | uint tokenAmountOutBeforeSwapFee = bsub(tokenBalanceOut, newTokenBalanceOut); 392 | 393 | // charge swap fee on the output token side 394 | //uint tAo = tAoBeforeSwapFee * (1 - (1-weightTo) * swapFee) 395 | uint zaz = bmul(bsub(BONE, normalizedWeight), swapFee); 396 | tokenAmountOut = bmul(tokenAmountOutBeforeSwapFee, bsub(BONE, zaz)); 397 | return tokenAmountOut; 398 | } 399 | 400 | /********************************************************************************************** 401 | // calcPoolInGivenSingleOut // 402 | // pAi = poolAmountIn // / tAo \\ / wO \ \ // 403 | // bO = tokenBalanceOut // | bO - -------------------------- |\ | ---- | \ // 404 | // tAo = tokenAmountOut pS - || \ 1 - ((1 - (tO / tW)) * sF)/ | ^ \ tW / * pS | // 405 | // ps = poolSupply \\ -----------------------------------/ / // 406 | // wO = tokenWeightOut pAi = \\ bO / / // 407 | // tW = totalWeight ------------------------------------------------------------- // 408 | // sF = swapFee ( 1 - eF ) // 409 | // eF = exitFee // 410 | **********************************************************************************************/ 411 | function calcPoolInGivenSingleOut( 412 | uint tokenBalanceOut, 413 | uint tokenWeightOut, 414 | uint poolSupply, 415 | uint totalWeight, 416 | uint tokenAmountOut, 417 | uint swapFee 418 | ) 419 | internal pure 420 | returns (uint poolAmountIn) 421 | { 422 | 423 | // charge swap fee on the output token side 424 | uint normalizedWeight = bdiv(tokenWeightOut, totalWeight); 425 | //uint tAoBeforeSwapFee = tAo / (1 - (1-weightTo) * swapFee) ; 426 | uint zoo = bsub(BONE, normalizedWeight); 427 | uint zar = bmul(zoo, swapFee); 428 | uint tokenAmountOutBeforeSwapFee = bdiv(tokenAmountOut, bsub(BONE, zar)); 429 | 430 | uint newTokenBalanceOut = bsub(tokenBalanceOut, tokenAmountOutBeforeSwapFee); 431 | uint tokenOutRatio = bdiv(newTokenBalanceOut, tokenBalanceOut); 432 | 433 | //uint newPoolSupply = (ratioTo ^ weightTo) * poolSupply; 434 | uint poolRatio = bpow(tokenOutRatio, normalizedWeight); 435 | uint newPoolSupply = bmul(poolRatio, poolSupply); 436 | uint poolAmountInAfterExitFee = bsub(poolSupply, newPoolSupply); 437 | 438 | // charge exit fee on the pool token side 439 | // pAi = pAiAfterExitFee/(1-exitFee) 440 | poolAmountIn = bdiv(poolAmountInAfterExitFee, bsub(BONE, EXIT_FEE)); 441 | return poolAmountIn; 442 | } 443 | 444 | 445 | } -------------------------------------------------------------------------------- /echidna/CryticInterface.sol: -------------------------------------------------------------------------------- 1 | contract CryticInterface { 2 | address internal crytic_owner = address(0x41414141); 3 | address internal crytic_user = address(0x42424242); 4 | address internal crytic_attacker = address(0x43434343); 5 | } 6 | -------------------------------------------------------------------------------- /echidna/MyToken.sol: -------------------------------------------------------------------------------- 1 | import "../crytic-export/flattening/BPool.sol"; 2 | import "./CryticInterface.sol"; 3 | 4 | contract MyToken is BToken, CryticInterface{ 5 | 6 | constructor(uint balance, address allowed) public { 7 | // balance is the new totalSupply 8 | _totalSupply = balance; 9 | // each user receives 1/3 of the balance and sets 10 | // the allowance of the allowed address. 11 | uint initialTotalSupply = balance; 12 | _balance[crytic_owner] = initialTotalSupply/3; 13 | _allowance[crytic_owner][allowed] = balance; 14 | _balance[crytic_user] = initialTotalSupply/3; 15 | _allowance[crytic_user][allowed] = balance; 16 | _balance[crytic_attacker] = initialTotalSupply/3; 17 | _allowance[crytic_attacker][allowed] = balance; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /echidna/TBPoolBalance.sol: -------------------------------------------------------------------------------- 1 | import "../crytic-export/flattening/BPool.sol"; 2 | import "./MyToken.sol"; 3 | import "./CryticInterface.sol"; 4 | 5 | contract TBPoolBalance is BPool, CryticInterface { 6 | 7 | MyToken public token; 8 | uint internal initial_token_balance = uint(-1); 9 | 10 | constructor() public{ 11 | // Create a new token with initial_token_balance as total supply. 12 | // After the token is created, each user defined in CryticInterface 13 | // (crytic_owner, crytic_user and crytic_attacker) receives 1/3 of 14 | // the initial balance 15 | token = new MyToken(initial_token_balance, address(this)); 16 | // Bind the token with the minimal balance/weights 17 | bind(address(token), MIN_BALANCE, MIN_WEIGHT); 18 | // Enable public swap 19 | setPublicSwap(true); 20 | } 21 | 22 | function echidna_attacker_token_balance() public returns(bool){ 23 | // An attacker cannot obtain more tokens than its initial balance 24 | return token.balanceOf(crytic_attacker) == initial_token_balance/3; //initial balance of crytic_attacker 25 | } 26 | 27 | function echidna_pool_record_balance() public returns (bool) { 28 | // If the token was unbinded, avoid revert and return true 29 | if (this.getNumTokens() == 0) 30 | return true; 31 | // The token balance should not be out-of-sync 32 | return (token.balanceOf(address(this)) >= this.getBalance(address(token))); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /echidna/TBPoolBalance.yaml: -------------------------------------------------------------------------------- 1 | seqLen: 50 2 | testLimit: 100000 3 | prefix: "echidna_" 4 | deployer: "0x41414141" 5 | sender: ["0x41414141", "0x42424242","0x43434343"] 6 | psender: "0x41414141" 7 | dashboard: true 8 | -------------------------------------------------------------------------------- /echidna/TBPoolBind.sol: -------------------------------------------------------------------------------- 1 | import "../crytic-export/flattening/BPool.sol"; 2 | import "./MyToken.sol"; 3 | import "./CryticInterface.sol"; 4 | 5 | contract TBPoolBindPrivileged is CryticInterface, BPool { 6 | 7 | constructor() public { 8 | // Create a new token with initial_token_balance as total supply. 9 | // After the token is created, each user defined in CryticInterface 10 | // (crytic_owner, crytic_user and crytic_attacker) receives 1/3 of 11 | // the initial balance 12 | MyToken t; 13 | t = new MyToken(initial_token_balance, address(this)); 14 | bind(address(t), MIN_BALANCE, MIN_WEIGHT); 15 | } 16 | 17 | // initial token balances is the max amount for uint256 18 | uint internal initial_token_balance = uint(-1); 19 | // these two variables are used to save valid balances and denorm parameters 20 | uint internal valid_balance_to_bind = MIN_BALANCE; 21 | uint internal valid_denorm_to_bind = MIN_WEIGHT; 22 | 23 | 24 | // this function allows to create as many tokens as needed 25 | function create_and_bind(uint balance, uint denorm) public returns (address) { 26 | // Create a new token with initial_token_balance as total supply. 27 | // After the token is created, each user defined in CryticInterface 28 | // (crytic_owner, crytic_user and crytic_attacker) receives 1/3 of 29 | // the initial balance 30 | MyToken bt = new MyToken(initial_token_balance, address(this)); 31 | bt.approve(address(this), initial_token_balance); 32 | // Bind the token with the provided parameters 33 | bind(address(bt), balance, denorm); 34 | // Save the balance and denorm values used. These are used in the rebind checks 35 | valid_balance_to_bind = balance; 36 | valid_denorm_to_bind = denorm; 37 | return address(bt); 38 | } 39 | 40 | function echidna_getNumTokens_less_or_equal_MAX_BOUND_TOKENS() public returns (bool) { 41 | // it is not possible to bind more than `MAX_BOUND_TOKENS` 42 | return this.getNumTokens() <= MAX_BOUND_TOKENS; 43 | } 44 | 45 | function echidna_revert_bind_twice() public returns (bool) { 46 | if (this.getCurrentTokens().length > 0 && this.getController() == crytic_owner && !this.isFinalized()) { 47 | // binding the first token should be enough, if we have this property to always revert 48 | bind(this.getCurrentTokens()[0], valid_balance_to_bind, valid_denorm_to_bind); 49 | // This return will make this property to fail 50 | return true; 51 | } 52 | // If there are no tokens or if the controller was changed or if the pool was finalized, just revert. 53 | revert(); 54 | } 55 | 56 | function echidna_revert_unbind_twice() public returns (bool) { 57 | if (this.getCurrentTokens().length > 0 && this.getController() == crytic_owner && !this.isFinalized()) { 58 | address[] memory current_tokens = this.getCurrentTokens(); 59 | // unbinding the first token twice should be enough, if we want this property to always revert 60 | unbind(current_tokens[0]); 61 | unbind(current_tokens[0]); 62 | return true; 63 | } 64 | // if there are no tokens or if the controller was changed or if the pool was finalized, just revert 65 | revert(); 66 | } 67 | 68 | function echidna_all_tokens_are_unbindable() public returns (bool) { 69 | if (this.getController() == crytic_owner && !this.isFinalized()) { 70 | address[] memory current_tokens = this.getCurrentTokens(); 71 | // unbind all the tokens, one by one 72 | for (uint i = 0; i < current_tokens.length; i++) { 73 | unbind(current_tokens[i]); 74 | } 75 | // at the end, the list of current tokens should be empty 76 | return (this.getCurrentTokens().length == 0); 77 | } 78 | 79 | // if the controller was changed or if the pool was finalized, just return true 80 | return true; 81 | } 82 | 83 | function echidna_all_tokens_are_rebindable_with_valid_parameters() public returns (bool) { 84 | if (this.getController() == crytic_owner && !this.isFinalized()) { 85 | address[] memory current_tokens = this.getCurrentTokens(); 86 | for (uint i = 0; i < current_tokens.length; i++) { 87 | // rebind all the tokens, one by one, using valid parameters 88 | rebind(current_tokens[i], valid_balance_to_bind, valid_denorm_to_bind); 89 | } 90 | // at the end, the list of current tokens should have not change in size 91 | return current_tokens.length == this.getCurrentTokens().length; 92 | } 93 | // if the controller was changed or if the pool was finalized, just return true 94 | return true; 95 | } 96 | 97 | function echidna_revert_rebind_unbinded() public returns (bool) { 98 | if (this.getCurrentTokens().length > 0 && this.getController() == crytic_owner && !this.isFinalized()) { 99 | address[] memory current_tokens = this.getCurrentTokens(); 100 | // unbinding and rebinding the first token should be enough, if we want this property to always revert 101 | unbind(current_tokens[0]); 102 | rebind(current_tokens[0], valid_balance_to_bind, valid_denorm_to_bind); 103 | return true; 104 | } 105 | // if the controller was changed or if the pool was finalized, just return true 106 | revert(); 107 | } 108 | } 109 | 110 | contract TBPoolBindUnprivileged is CryticInterface, BPool { 111 | 112 | MyToken t1; 113 | MyToken t2; 114 | // initial token balances is the max amount for uint256 115 | uint internal initial_token_balance = uint(-1); 116 | 117 | constructor() public { 118 | // two tokens with minimal balances and weights are created by the controller 119 | t1 = new MyToken(initial_token_balance, address(this)); 120 | bind(address(t1), MIN_BALANCE, MIN_WEIGHT); 121 | t2 = new MyToken(initial_token_balance, address(this)); 122 | bind(address(t2), MIN_BALANCE, MIN_WEIGHT); 123 | } 124 | 125 | // these two variables are used to save valid balances and denorm parameters 126 | uint internal valid_balance_to_bind = MIN_BALANCE; 127 | uint internal valid_denorm_to_bind = MIN_WEIGHT; 128 | 129 | // this function allows to create as many tokens as needed 130 | function create_and_bind(uint balance, uint denorm) public returns (address) { 131 | // Create a new token with initial_token_balance as total supply. 132 | // After the token is created, each user defined in CryticInterface 133 | // (crytic_owner, crytic_user and crytic_attacker) receives 1/3 of 134 | // the initial balance 135 | MyToken bt = new MyToken(initial_token_balance, address(this)); 136 | bt.approve(address(this), initial_token_balance); 137 | // Bind the token with the provided parameters 138 | bind(address(bt), balance, denorm); 139 | // Save the balance and denorm values used. These are used in the rebind checks 140 | valid_balance_to_bind = balance; 141 | valid_denorm_to_bind = denorm; 142 | return address(bt); 143 | } 144 | 145 | function echidna_only_controller_can_bind() public returns (bool) { 146 | // the number of tokens cannot be changed 147 | return this.getNumTokens() == 2; 148 | } 149 | 150 | function echidna_revert_when_bind() public returns (bool) { 151 | // calling bind will revert 152 | create_and_bind(valid_balance_to_bind, valid_denorm_to_bind); 153 | return true; 154 | } 155 | 156 | function echidna_revert_when_rebind() public returns (bool) { 157 | // calling rebind on binded tokens will revert 158 | rebind(address(t1), valid_balance_to_bind, valid_denorm_to_bind); 159 | rebind(address(t2), valid_balance_to_bind, valid_denorm_to_bind); 160 | return true; 161 | } 162 | 163 | function echidna_revert_when_unbind() public returns (bool) { 164 | // calling unbind on binded tokens will revert 165 | unbind(address(t1)); 166 | unbind(address(t2)); 167 | return true; 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /echidna/TBPoolBindPrivileged.yaml: -------------------------------------------------------------------------------- 1 | seqLen: 50 2 | testLimit: 100000 3 | prefix: "echidna_" 4 | deployer: "0x41414141" 5 | sender: ["0x41414141", "0x42424242", "0x43434343"] 6 | psender: "0x41414141" 7 | dashboard: true 8 | corpusDir: "corpus" 9 | mutation: true 10 | -------------------------------------------------------------------------------- /echidna/TBPoolBindUnprivileged.yaml: -------------------------------------------------------------------------------- 1 | seqLen: 50 2 | testLimit: 20000 3 | prefix: "echidna_" 4 | deployer: "0x41414141" 5 | sender: ["0x42424242", "0x43434343"] 6 | psender: "0x42424242" 7 | dashboard: true 8 | -------------------------------------------------------------------------------- /echidna/TBPoolController.sol: -------------------------------------------------------------------------------- 1 | import "../crytic-export/flattening/BPool.sol"; 2 | import "./CryticInterface.sol"; 3 | 4 | contract TBPoolControllerPrivileged is CryticInterface, BPool { 5 | 6 | function echidna_controller_should_change() public returns (bool) { 7 | if (this.getController() == crytic_owner) { 8 | setController(crytic_user); 9 | return (this.getController() == crytic_user); 10 | } 11 | // if the controller was changed, this should return true 12 | return true; 13 | } 14 | 15 | function echidna_revert_controller_cannot_be_null() public returns (bool) { 16 | if (this.getController() == crytic_owner) { 17 | // setting the controller to 0x0 should fail 18 | setController(address(0x0)); 19 | return true; 20 | } 21 | // if the controller was changed, this should revert anyway 22 | revert(); 23 | } 24 | } 25 | 26 | contract TBPoolControllerUnprivileged is CryticInterface, BPool { 27 | 28 | function echidna_no_other_user_can_change_the_controller() public returns (bool) { 29 | // the controller cannot be changed by other users 30 | return this.getController() == crytic_owner; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /echidna/TBPoolControllerPrivileged.yaml: -------------------------------------------------------------------------------- 1 | seqLen: 50 2 | testLimit: 20000 3 | prefix: "echidna_" 4 | deployer: "0x41414141" 5 | sender: ["0x41414141", "0x42424242", "0x43434343"] 6 | psender: "0x41414141" 7 | dashboard: true 8 | -------------------------------------------------------------------------------- /echidna/TBPoolControllerUnprivileged.yaml: -------------------------------------------------------------------------------- 1 | seqLen: 50 2 | testLimit: 20000 3 | prefix: "echidna_" 4 | deployer: "0x41414141" 5 | sender: ["0x42424242", "0x43434343"] 6 | psender: "0x41414141" 7 | dashboard: true 8 | -------------------------------------------------------------------------------- /echidna/TBPoolExitSwap.sol: -------------------------------------------------------------------------------- 1 | import "./BMathInternal.sol"; 2 | 3 | // This contract used a modified version of BMath where all the public/external functions are internal to speed up Echidna exploration 4 | contract TestSwapOut is BMath { 5 | 6 | bool public echidna_no_bug = true; 7 | 8 | // A bug is found if tokenAmountOut can be greater than 0 while calcPoolInGivenSingleOut returns 0 9 | function exitswapExternAmountOut(uint balanceOut, uint poolTotal, uint tokenAmountOut) public { 10 | // We constraint poolTotal and _records_t_balance 11 | // To have "realistic" values 12 | require(poolTotal <= 100 ether); 13 | require(poolTotal >= 1 ether); 14 | 15 | require(balanceOut <= 10 ether); 16 | require(balanceOut >= 10**6); 17 | 18 | require(tokenAmountOut > 0); 19 | require(calcPoolInGivenSingleOut(balanceOut, MIN_WEIGHT, poolTotal, MIN_WEIGHT*2, tokenAmountOut, MIN_FEE)==0); 20 | echidna_no_bug = false; 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /echidna/TBPoolJoinExit.sol: -------------------------------------------------------------------------------- 1 | import "../crytic-export/flattening/BPool.sol"; 2 | import "./MyToken.sol"; 3 | import "./CryticInterface.sol"; 4 | 5 | contract TBPoolJoinExit is CryticInterface, BPool { 6 | 7 | uint MAX_BALANCE = BONE * 10**12; 8 | 9 | constructor() public { 10 | MyToken t; 11 | t = new MyToken(uint(-1), address(this)); 12 | bind(address(t), MAX_BALANCE, MAX_WEIGHT); 13 | } 14 | 15 | // initial token balances is the max amount for uint256 16 | uint internal initial_token_balance = uint(-1); 17 | 18 | // this function allows to create as many tokens as needed 19 | function create_and_bind(uint balance, uint denorm) public returns (address) { 20 | // Create a new token with initial_token_balance as total supply. 21 | // After the token is created, each user defined in CryticInterface 22 | // (crytic_owner, crytic_user and crytic_attacker) receives 1/3 of 23 | // the initial balance 24 | MyToken bt = new MyToken(initial_token_balance, address(this)); 25 | bt.approve(address(this), initial_token_balance); 26 | // Bind the token with the provided parameters 27 | bind(address(bt), balance, denorm); 28 | return address(bt); 29 | } 30 | 31 | uint[] internal maxAmountsIn = [uint(-1), uint(-1), uint(-1), uint(-1), uint(-1), uint(-1)]; 32 | uint[] internal minAmountsOut = [0, 0, 0, 0, 0, 0, 0, 0]; 33 | uint[8] internal balances = [0, 0, 0, 0, 0, 0, 0, 0]; 34 | 35 | uint internal amount = EXIT_FEE; 36 | uint internal amount1 = EXIT_FEE; 37 | uint internal amount2 = EXIT_FEE; 38 | 39 | // sets an amount between EXIT_FEE and EXIT_FEE + 2**64 40 | function set_input(uint _amount) public { 41 | amount = EXIT_FEE + _amount % 2**64; 42 | } 43 | 44 | // sets two amounts between EXIT_FEE and EXIT_FEE + 2**64 45 | function set_two_inputs(uint _amount1, uint _amount2) public { 46 | amount1 = EXIT_FEE + _amount1 % 2**64; 47 | amount2 = EXIT_FEE + _amount2 % 2**64; 48 | } 49 | 50 | function echidna_joinPool_exitPool_balance_consistency() public returns (bool) { 51 | 52 | // if the pool was not finalize, return true (it is unclear how to finalize it) 53 | if (!this.isFinalized()) 54 | return true; 55 | 56 | // check this precondition for joinPool 57 | if (bdiv(amount, this.totalSupply()) == 0) 58 | return true; 59 | 60 | // save all the token balances in `balances` before calling joinPool / exitPool 61 | address[] memory current_tokens = this.getCurrentTokens(); 62 | for (uint i = 0; i < current_tokens.length; i++) 63 | balances[i] = (IERC20(current_tokens[i]).balanceOf(address(msg.sender))); 64 | 65 | // save the amount of share tokens 66 | uint old_balance = this.balanceOf(crytic_owner); 67 | 68 | // call joinPool, with some some reasonable amount 69 | joinPool(amount, maxAmountsIn); 70 | // check that the amount of shares decreased 71 | if (this.balanceOf(crytic_owner) - amount != old_balance) 72 | return false; 73 | 74 | // check the precondition for exitPool 75 | uint exit_fee = bmul(amount, EXIT_FEE); 76 | uint pAiAfterExitFee = bsub(amount, exit_fee); 77 | if(bdiv(pAiAfterExitFee, this.totalSupply()) == 0) 78 | return true; 79 | 80 | // call exitPool with some reasonable amount 81 | exitPool(amount, minAmountsOut); 82 | uint new_balance = this.balanceOf(crytic_owner); 83 | 84 | // check that the amount of shares decreased, taking in consideration that 85 | // _factory is crytic_owner, so it will receive the exit_fees 86 | if (old_balance != new_balance - exit_fee) 87 | return false; 88 | 89 | // verify that the final token balance are consistent. It is possible 90 | // to have rounding issues, but it should not allow to obtain more tokens than 91 | // the ones a user owned 92 | for (uint i = 0; i < current_tokens.length; i++) { 93 | uint current_balance = IERC20(current_tokens[i]).balanceOf(address(msg.sender)); 94 | if (balances[i] < current_balance) 95 | return false; 96 | } 97 | 98 | return true; 99 | } 100 | 101 | function echidna_revert_impossible_joinPool_exitPool() public returns (bool) { 102 | 103 | // the amount to join should be smaller to the amount to exit 104 | if (amount1 >= amount2) 105 | revert(); 106 | 107 | // burn all the shares transfering them to 0x0 108 | transfer(address(0x0), this.balanceOf(msg.sender)); 109 | // join a pool with a reasonable amount. 110 | joinPool(amount1, maxAmountsIn); 111 | // exit a pool with a larger amount 112 | exitPool(amount2, minAmountsOut); 113 | return true; 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /echidna/TBPoolJoinExit.yaml: -------------------------------------------------------------------------------- 1 | seqLen: 50 2 | testLimit: 1000000 3 | prefix: "echidna_" 4 | deployer: "0x41414141" 5 | sender: ["0x41414141", "0x42424242", "0x43434343"] 6 | psender: "0x41414141" 7 | dashboard: true 8 | corpusDir: "corpus" 9 | mutation: true 10 | -------------------------------------------------------------------------------- /echidna/TBPoolLimits.sol: -------------------------------------------------------------------------------- 1 | import "../crytic-export/flattening/BPool.sol"; 2 | import "./MyToken.sol"; 3 | import "./CryticInterface.sol"; 4 | 5 | contract TBPoolLimits is CryticInterface, BPool { 6 | 7 | uint MAX_BALANCE = BONE * 10**12; 8 | 9 | constructor() public { 10 | MyToken t; 11 | t = new MyToken(uint(-1), address(this)); 12 | bind(address(t), MIN_BALANCE, MIN_WEIGHT); 13 | } 14 | 15 | // initial token balances is the max amount for uint256 16 | uint internal initial_token_balance = uint(-1); 17 | // these two variables are used to save valid balances and denorm parameters 18 | uint internal valid_balance_to_bind = MIN_BALANCE; 19 | uint internal valid_denorm_to_bind = MIN_WEIGHT; 20 | 21 | // this function allows to create as many tokens as needed 22 | function create_and_bind(uint balance, uint denorm) public returns (address) { 23 | // Create a new token with initial_token_balance as total supply. 24 | // After the token is created, each user defined in CryticInterface 25 | // (crytic_owner, crytic_user and crytic_attacker) receives 1/3 of 26 | // the initial balance 27 | MyToken bt = new MyToken(initial_token_balance, address(this)); 28 | bt.approve(address(this), initial_token_balance); 29 | // Bind the token with the provided parameters 30 | bind(address(bt), balance, denorm); 31 | // Save the balance and denorm values used. These are used in the rebind checks 32 | valid_balance_to_bind = balance; 33 | valid_denorm_to_bind = denorm; 34 | return address(bt); 35 | } 36 | 37 | function echidna_valid_weights() public returns (bool) { 38 | address[] memory current_tokens = this.getCurrentTokens(); 39 | // store the normalized weight in this variable 40 | uint nw = 0; 41 | for (uint i = 0; i < current_tokens.length; i++) { 42 | // accumulate the total normalized weights, checking for overflows 43 | nw = badd(nw,this.getNormalizedWeight(current_tokens[i])); 44 | } 45 | // convert the sum of normalized weights into an integer 46 | nw = btoi(nw); 47 | 48 | // if there are no tokens, check that the normalized weight is zero 49 | if (current_tokens.length == 0) 50 | return (nw == 0); 51 | 52 | // if there are tokens, the normalized weight should be 1 53 | return (nw == 1); 54 | } 55 | 56 | function echidna_min_token_balance() public returns (bool) { 57 | address[] memory current_tokens = this.getCurrentTokens(); 58 | for (uint i = 0; i < current_tokens.length; i++) { 59 | // verify that the balance of each token is more than `MIN_BALACE` 60 | if (this.getBalance(address(current_tokens[i])) < MIN_BALANCE) 61 | return false; 62 | } 63 | // if there are no tokens, return true 64 | return true; 65 | } 66 | 67 | function echidna_max_weight() public returns (bool) { 68 | address[] memory current_tokens = this.getCurrentTokens(); 69 | for (uint i = 0; i < current_tokens.length; i++) { 70 | // verify that the weight of each token is less than `MAX_WEIGHT` 71 | if (this.getDenormalizedWeight(address(current_tokens[i])) > MAX_WEIGHT) 72 | return false; 73 | } 74 | // if there are no tokens, return true 75 | return true; 76 | } 77 | 78 | function echidna_min_weight() public returns (bool) { 79 | address[] memory current_tokens = this.getCurrentTokens(); 80 | for (uint i = 0; i < current_tokens.length; i++) { 81 | // verify that the weight of each token is more than `MIN_WEIGHT` 82 | if (this.getDenormalizedWeight(address(current_tokens[i])) < MIN_WEIGHT) 83 | return false; 84 | } 85 | // if there are no tokens, return true 86 | return true; 87 | } 88 | 89 | 90 | function echidna_min_swap_free() public returns (bool) { 91 | // verify that the swap fee is greater or equal than `MIN_FEE` 92 | return this.getSwapFee() >= MIN_FEE; 93 | } 94 | 95 | function echidna_max_swap_free() public returns (bool) { 96 | // verify that the swap fee is less or equal than `MAX_FEE` 97 | return this.getSwapFee() <= MAX_FEE; 98 | } 99 | 100 | function echidna_revert_max_swapExactAmountOut() public returns (bool) { 101 | // if the controller was changed, revert 102 | if (this.getController() != crytic_owner) 103 | revert(); 104 | 105 | // if the pool is not finalized, make sure public swap is enabled 106 | if (!this.isFinalized()) 107 | setPublicSwap(true); 108 | 109 | address[] memory current_tokens = this.getCurrentTokens(); 110 | // if there is not token, revert 111 | if (current_tokens.length == 0) 112 | revert(); 113 | 114 | uint large_balance = this.getBalance(current_tokens[0])/3 + 2; 115 | 116 | // check that the balance is large enough 117 | if (IERC20(current_tokens[0]).balanceOf(crytic_owner) < large_balance) 118 | revert(); 119 | 120 | // call swapExactAmountOut with more than 1/3 of the balance should revert 121 | swapExactAmountOut(address(current_tokens[0]), uint(-1), address(current_tokens[0]), large_balance, uint(-1)); 122 | return true; 123 | } 124 | 125 | function echidna_revert_max_swapExactAmountIn() public returns (bool) { 126 | // if the controller was changed, revert 127 | if (this.getController() != crytic_owner) 128 | revert(); 129 | 130 | // if the pool is not finalized, make sure public swap is enabled 131 | if (!this.isFinalized()) 132 | setPublicSwap(true); 133 | 134 | address[] memory current_tokens = this.getCurrentTokens(); 135 | // if there is not token, revert 136 | if (current_tokens.length == 0) 137 | revert(); 138 | 139 | uint large_balance = this.getBalance(current_tokens[0])/2 + 1; 140 | 141 | if (IERC20(current_tokens[0]).balanceOf(crytic_owner) < large_balance) 142 | revert(); 143 | 144 | swapExactAmountIn(address(current_tokens[0]), large_balance, address(current_tokens[0]), 0, uint(-1)); 145 | 146 | return true; 147 | } 148 | 149 | } 150 | -------------------------------------------------------------------------------- /echidna/TBPoolLimits.yaml: -------------------------------------------------------------------------------- 1 | seqLen: 50 2 | testLimit: 100000 3 | prefix: "echidna_" 4 | deployer: "0x41414141" 5 | sender: ["0x41414141", "0x42424242", "0x43434343"] 6 | psender: "0x41414141" 7 | dashboard: true 8 | corpusDir: "corpus" 9 | mutation: true 10 | -------------------------------------------------------------------------------- /echidna/TBPoolNoRevert.sol: -------------------------------------------------------------------------------- 1 | import "../crytic-export/flattening/BPool.sol"; 2 | import "./MyToken.sol"; 3 | import "./CryticInterface.sol"; 4 | 5 | contract TBPoolNoRevert is CryticInterface, BPool { 6 | 7 | constructor() public { // out-of-gas? 8 | // Create a new token with initial_token_balance as total supply. 9 | // After the token is created, each user defined in CryticInterface 10 | // (crytic_owner, crytic_user and crytic_attacker) receives 1/3 of 11 | // the initial balance 12 | MyToken t; 13 | t = new MyToken(initial_token_balance, address(this)); 14 | bind(address(t), MIN_BALANCE, MIN_WEIGHT); 15 | } 16 | 17 | // initial token balances is the max amount for uint256 18 | uint internal initial_token_balance = uint(-1); 19 | 20 | // this function allows to create as many tokens as needed 21 | function create_and_bind(uint balance, uint denorm) public returns (address) { 22 | // Create a new token with initial_token_balance as total supply. 23 | // After the token is created, each user defined in CryticInterface 24 | // (crytic_owner, crytic_user and crytic_attacker) receives 1/3 of 25 | // the initial balance 26 | MyToken bt = new MyToken(initial_token_balance, address(this)); 27 | bt.approve(address(this), initial_token_balance); 28 | // Bind the token with the provided parameters 29 | bind(address(bt), balance, denorm); 30 | // Save the balance and denorm values used. These are used in the rebind checks 31 | return address(bt); 32 | } 33 | 34 | function echidna_getSpotPrice_no_revert() public returns (bool) { 35 | address[] memory current_tokens = this.getCurrentTokens(); 36 | for (uint i = 0; i < current_tokens.length; i++) { 37 | for (uint j = 0; j < current_tokens.length; j++) { 38 | // getSpotPrice should not revert for any pair of tokens 39 | this.getSpotPrice(address(current_tokens[i]), address(current_tokens[j])); 40 | } 41 | } 42 | 43 | return true; 44 | } 45 | 46 | function echidna_getSpotPriceSansFee_no_revert() public returns (bool) { 47 | address[] memory current_tokens = this.getCurrentTokens(); 48 | for (uint i = 0; i < current_tokens.length; i++) { 49 | for (uint j = 0; j < current_tokens.length; j++) { 50 | // getSpotPriceSansFee should not revert for any pair of tokens 51 | this.getSpotPriceSansFee(address(current_tokens[i]), address(current_tokens[j])); 52 | } 53 | } 54 | 55 | return true; 56 | } 57 | 58 | function echidna_swapExactAmountIn_no_revert() public returns (bool) { 59 | // if the controller was changed, return true 60 | if (this.getController() != crytic_owner) 61 | return true; 62 | 63 | // if the pool was not finalized, enable the public swap 64 | if (!this.isFinalized()) 65 | setPublicSwap(true); 66 | 67 | address[] memory current_tokens = this.getCurrentTokens(); 68 | for (uint i = 0; i < current_tokens.length; i++) { 69 | // a small balance is 1% of the total balance available 70 | uint small_balance = this.getBalance(current_tokens[i])/100; 71 | // if the user has a small balance, it should be able to swap it 72 | if (IERC20(current_tokens[i]).balanceOf(crytic_owner) > small_balance) 73 | swapExactAmountIn(address(current_tokens[i]), small_balance, address(current_tokens[i]), 0, uint(-1)); 74 | } 75 | 76 | return true; 77 | } 78 | 79 | function echidna_swapExactAmountOut_no_revert() public returns (bool) { 80 | 81 | // if the controller was changed, return true 82 | if (this.getController() != crytic_owner) 83 | return true; 84 | 85 | // if the pool was not finalized, enable the public swap 86 | if (!this.isFinalized()) 87 | setPublicSwap(true); 88 | 89 | address[] memory current_tokens = this.getCurrentTokens(); 90 | for (uint i = 0; i < current_tokens.length; i++) { 91 | // a small balance is 1% of the total balance available 92 | uint small_balance = this.getBalance(current_tokens[i])/100; 93 | // if the user has a small balance, it should be able to swap it 94 | if (IERC20(current_tokens[i]).balanceOf(crytic_owner) > small_balance) 95 | swapExactAmountOut(address(current_tokens[i]), uint(-1), address(current_tokens[i]), small_balance, uint(-1)); 96 | } 97 | 98 | return true; 99 | } 100 | 101 | } 102 | 103 | -------------------------------------------------------------------------------- /echidna/TBPoolNoRevert.yaml: -------------------------------------------------------------------------------- 1 | seqLen: 50 2 | testLimit: 100000 3 | prefix: "echidna_" 4 | deployer: "0x41414141" 5 | sender: ["0x41414141", "0x42424242", "0x43434343"] 6 | psender: "0x41414141" 7 | dashboard: true 8 | corpusDir: "corpus" 9 | mutation: true 10 | -------------------------------------------------------------------------------- /echidna/TBTokenERC20.sol: -------------------------------------------------------------------------------- 1 | import "../crytic-export/flattening/BPool.sol"; 2 | 3 | contract CryticInterface{ 4 | address internal crytic_owner = address(0x41414141); 5 | address internal crytic_user = address(0x42424242); 6 | address internal crytic_attacker = address(0x43434343); 7 | 8 | uint internal initialTotalSupply = uint(-1); 9 | uint internal initialBalance_owner; 10 | uint internal initialBalance_user; 11 | uint internal initialBalance_attacker; 12 | 13 | uint initialAllowance_user_attacker; 14 | uint initialAllowance_attacker_user; 15 | uint initialAllowance_attacker_attacker; 16 | } 17 | 18 | contract TBTokenERC20 is CryticInterface, BToken { 19 | 20 | constructor() public { 21 | _totalSupply = initialTotalSupply; 22 | _balance[crytic_owner] = 0; 23 | _balance[crytic_user] = initialTotalSupply/2; 24 | initialBalance_user = initialTotalSupply/2; 25 | _balance[crytic_attacker] = initialTotalSupply/2; 26 | initialBalance_attacker = initialTotalSupply/2; 27 | } 28 | 29 | 30 | /* 31 | Type: Code quality 32 | Return: Success 33 | */ 34 | function echidna_zero_always_empty() public returns(bool){ 35 | return this.balanceOf(address(0x0)) == 0; 36 | } 37 | 38 | /* 39 | Type: Code Quality 40 | Return: 41 | */ 42 | function echidna_approve_overwrites() public returns (bool) { 43 | bool approve_return; 44 | approve_return = approve(crytic_user, 10); 45 | require(approve_return); 46 | approve_return = approve(crytic_user, 20); 47 | require(approve_return); 48 | return this.allowance(msg.sender, crytic_user) == 20; 49 | } 50 | 51 | /* 52 | Type: Undetermined severity 53 | Return: Success 54 | */ 55 | function echidna_balance_less_than_totalSupply() public returns(bool){ 56 | return this.balanceOf(msg.sender) <= _totalSupply; 57 | } 58 | 59 | /* 60 | Type: Low severity 61 | Return: Success 62 | */ 63 | function echidna_totalSupply_balances_consistency() public returns(bool){ 64 | return this.balanceOf(crytic_owner) + this.balanceOf(crytic_user) + this.balanceOf(crytic_attacker) <= totalSupply(); 65 | } 66 | 67 | /* 68 | Properties: Transferable 69 | */ 70 | 71 | /* 72 | Type: Code Quality 73 | Return: Fail or Throw 74 | */ 75 | function echidna_revert_transfer_to_zero() public returns (bool) { 76 | if (this.balanceOf(msg.sender) == 0) 77 | revert(); 78 | return transfer(address(0x0), this.balanceOf(msg.sender)); 79 | } 80 | 81 | /* 82 | Type: Code Quality 83 | Return: Fail or Throw 84 | */ 85 | function echidna_revert_transferFrom_to_zero() public returns (bool) { 86 | uint balance = this.balanceOf(msg.sender); 87 | bool approve_return = approve(msg.sender, balance); 88 | return transferFrom(msg.sender, address(0x0), this.balanceOf(msg.sender)); 89 | } 90 | 91 | /* 92 | Type: ERC20 Standard 93 | Fire: Transfer(msg.sender, msg.sender, balanceOf(msg.sender)) 94 | Return: Success 95 | */ 96 | function echidna_self_transferFrom() public returns(bool){ 97 | uint balance = this.balanceOf(msg.sender); 98 | bool approve_return = approve(msg.sender, balance); 99 | bool transfer_return = transferFrom(msg.sender, msg.sender, balance); 100 | return (this.balanceOf(msg.sender) == balance) && approve_return && transfer_return; 101 | } 102 | 103 | 104 | /* 105 | Type: ERC20 Standard 106 | Return: Success 107 | */ 108 | function echidna_self_transferFrom_to_other() public returns(bool){ 109 | uint balance = this.balanceOf(msg.sender); 110 | bool approve_return = approve(msg.sender, balance); 111 | bool transfer_return = transferFrom(msg.sender, crytic_owner, balance); 112 | return (this.balanceOf(msg.sender) == 0) && approve_return && transfer_return; 113 | } 114 | 115 | /* 116 | Type: ERC20 Standard 117 | Fire: Transfer(msg.sender, msg.sender, balanceOf(msg.sender)) 118 | Return: Success 119 | */ 120 | function echidna_self_transfer() public returns(bool){ 121 | uint balance = this.balanceOf(msg.sender); 122 | bool transfer_return = transfer(msg.sender, balance); 123 | return (this.balanceOf(msg.sender) == balance) && transfer_return; 124 | } 125 | 126 | /* 127 | Type: ERC20 Standard 128 | Fire: Transfer(msg.sender, other, 1) 129 | Return: Success 130 | */ 131 | function echidna_transfer_to_other() public returns(bool){ 132 | uint balance = this.balanceOf(msg.sender); 133 | address other = crytic_user; 134 | if (other == msg.sender) { 135 | other = crytic_owner; 136 | } 137 | if (balance >= 1) { 138 | bool transfer_other = transfer(other, 1); 139 | return (this.balanceOf(msg.sender) == balance-1) && (this.balanceOf(other) >= 1) && transfer_other; 140 | } 141 | return true; 142 | } 143 | 144 | /* 145 | Type: ERC20 Standard 146 | Fire: Transfer(msg.sender, user, balance+1) 147 | Return: Fail or Throw 148 | */ 149 | function echidna_revert_transfer_to_user() public returns(bool){ 150 | uint balance = this.balanceOf(msg.sender); 151 | if (balance == (2 ** 256 - 1)) 152 | revert(); 153 | bool transfer_other = transfer(crytic_user, balance+1); 154 | return true; 155 | } 156 | 157 | 158 | /* 159 | Properties: Not Mintable 160 | */ 161 | 162 | /* 163 | Type: Undetermined severity 164 | Return: Success 165 | */ 166 | function echidna_totalSupply_constant() public returns(bool){ 167 | return initialTotalSupply == totalSupply(); 168 | } 169 | 170 | } 171 | -------------------------------------------------------------------------------- /echidna/TBTokenERC20.yaml: -------------------------------------------------------------------------------- 1 | seqLen: 50 2 | testLimit: 100000 3 | prefix: "echidna_" 4 | deployer: "0x41414141" 5 | sender: ["0x42424242", "0x43434343"] 6 | psender: "0x43434343" 7 | dashboard: true 8 | -------------------------------------------------------------------------------- /echidna_general_config.yaml: -------------------------------------------------------------------------------- 1 | seqLen: 50 2 | testLimit: 1000000 3 | -------------------------------------------------------------------------------- /lib/calc_comparisons.js: -------------------------------------------------------------------------------- 1 | const Decimal = require('decimal.js'); 2 | 3 | function calcRelativeDiff(expected, actual) { 4 | return ((Decimal(expected).minus(Decimal(actual))).div(expected)).abs(); 5 | } 6 | 7 | function calcSpotPrice(tokenBalanceIn, tokenWeightIn, tokenBalanceOut, tokenWeightOut, swapFee) { 8 | const numer = Decimal(tokenBalanceIn).div(Decimal(tokenWeightIn)); 9 | const denom = Decimal(tokenBalanceOut).div(Decimal(tokenWeightOut)); 10 | const ratio = numer.div(denom); 11 | const scale = Decimal(1).div(Decimal(1).sub(Decimal(swapFee))); 12 | const spotPrice = ratio.mul(scale); 13 | return spotPrice; 14 | } 15 | 16 | function calcOutGivenIn(tokenBalanceIn, tokenWeightIn, tokenBalanceOut, tokenWeightOut, tokenAmountIn, swapFee) { 17 | const weightRatio = Decimal(tokenWeightIn).div(Decimal(tokenWeightOut)); 18 | const adjustedIn = Decimal(tokenAmountIn).times((Decimal(1).minus(Decimal(swapFee)))); 19 | const y = Decimal(tokenBalanceIn).div(Decimal(tokenBalanceIn).plus(adjustedIn)); 20 | const foo = y.pow(weightRatio); 21 | const bar = Decimal(1).minus(foo); 22 | const tokenAmountOut = Decimal(tokenBalanceOut).times(bar); 23 | return tokenAmountOut; 24 | } 25 | 26 | function calcInGivenOut(tokenBalanceIn, tokenWeightIn, tokenBalanceOut, tokenWeightOut, tokenAmountOut, swapFee) { 27 | const weightRatio = Decimal(tokenWeightOut).div(Decimal(tokenWeightIn)); 28 | const diff = Decimal(tokenBalanceOut).minus(tokenAmountOut); 29 | const y = Decimal(tokenBalanceOut).div(diff); 30 | const foo = y.pow(weightRatio).minus(Decimal(1)); 31 | const tokenAmountIn = (Decimal(tokenBalanceIn).times(foo)).div(Decimal(1).minus(Decimal(swapFee))); 32 | return tokenAmountIn; 33 | } 34 | 35 | function calcPoolOutGivenSingleIn(tokenBalanceIn, tokenWeightIn, poolSupply, totalWeight, tokenAmountIn, swapFee) { 36 | const normalizedWeight = Decimal(tokenWeightIn).div(Decimal(totalWeight)); 37 | const zaz = Decimal(1).sub(Decimal(normalizedWeight)).mul(Decimal(swapFee)); 38 | const tokenAmountInAfterFee = Decimal(tokenAmountIn).mul(Decimal(1).sub(zaz)); 39 | const newTokenBalanceIn = Decimal(tokenBalanceIn).add(tokenAmountInAfterFee); 40 | const tokenInRatio = newTokenBalanceIn.div(Decimal(tokenBalanceIn)); 41 | const poolRatio = tokenInRatio.pow(normalizedWeight); 42 | const newPoolSupply = poolRatio.mul(Decimal(poolSupply)); 43 | const poolAmountOut = newPoolSupply.sub(Decimal(poolSupply)); 44 | return poolAmountOut; 45 | } 46 | 47 | function calcSingleInGivenPoolOut(tokenBalanceIn, tokenWeightIn, poolSupply, totalWeight, poolAmountOut, swapFee) { 48 | const normalizedWeight = Decimal(tokenWeightIn).div(Decimal(totalWeight)); 49 | const newPoolSupply = Decimal(poolSupply).plus(Decimal(poolAmountOut)); 50 | const poolRatio = newPoolSupply.div(Decimal(poolSupply)); 51 | const boo = Decimal(1).div(normalizedWeight); 52 | const tokenInRatio = poolRatio.pow(boo); 53 | const newTokenBalanceIn = tokenInRatio.mul(Decimal(tokenBalanceIn)); 54 | const tokenAmountInAfterFee = newTokenBalanceIn.sub(Decimal(tokenBalanceIn)); 55 | const zar = (Decimal(1).sub(normalizedWeight)).mul(Decimal(swapFee)); 56 | const tokenAmountIn = tokenAmountInAfterFee.div(Decimal(1).sub(zar)); 57 | return tokenAmountIn; 58 | } 59 | 60 | module.exports = { 61 | calcSpotPrice, 62 | calcOutGivenIn, 63 | calcInGivenOut, 64 | calcPoolOutGivenSingleIn, 65 | calcSingleInGivenPoolOut, 66 | calcRelativeDiff, 67 | }; 68 | -------------------------------------------------------------------------------- /manticore/TBPoolJoinExit.py: -------------------------------------------------------------------------------- 1 | from manticore.ethereum import ManticoreEVM, ABI 2 | from manticore.core.smtlib import Operators, Z3Solver 3 | from manticore.utils import config 4 | from manticore.core.plugin import Plugin 5 | 6 | m = ManticoreEVM() 7 | 8 | # Disable the gas tracking 9 | consts_evm = config.get_group("evm") 10 | consts_evm.oog = "ignore" 11 | 12 | # Increase the solver timeout 13 | config.get_group("smt").defaultunsat = False 14 | config.get_group("smt").timeout = 3600 15 | 16 | ETHER = 10 ** 18 17 | 18 | user = m.create_account(balance=1 * ETHER) 19 | 20 | # This plugin is used to speed up the exploration and skip the require(false) paths 21 | # It won't be needed once https://github.com/trailofbits/manticore/issues/1593 is added 22 | class SkipRequire(Plugin): 23 | def will_evm_execute_instruction_callback(self, state, instruction, arguments): 24 | world = state.platform 25 | if state.platform.current_transaction.sort != 'CREATE': 26 | if instruction.semantics == "JUMPI": 27 | potential_revert = world.current_vm.read_code(world.current_vm.pc + 4) 28 | if potential_revert[0].size == 8 and potential_revert[0].value == 0xfd: 29 | state.constrain(arguments[1] == True) 30 | 31 | 32 | print(f'controller: {hex(user.address)}') 33 | 34 | skipRequire = SkipRequire() 35 | m.register_plugin(skipRequire) 36 | 37 | TestBpool = m.solidity_create_contract('./manticore/contracts/TBPoolJoinExit.sol', 38 | contract_name='TestJoinExit', 39 | owner=user) 40 | 41 | print(f'TBPoolJoinExit deployed {hex(TestBpool.address)}') 42 | 43 | # Call joinAndExitPool with symbolic values 44 | poolAmountOut = m.make_symbolic_value() 45 | poolAmountIn = m.make_symbolic_value() 46 | poolTotal = m.make_symbolic_value() 47 | _records_t_balance = m.make_symbolic_value() 48 | TestBpool.joinAndExitPool(poolAmountOut, poolAmountIn, poolTotal, _records_t_balance) 49 | 50 | print(f'joinAndExitPool Called') 51 | 52 | for state in m.ready_states: 53 | 54 | m.generate_testcase(state, name="BugFound") 55 | 56 | # Look over the 10**i, and try to generate more free tokens 57 | for i in range(0, 18): 58 | print(i) 59 | add_value = 10**i 60 | condition = Operators.AND(poolAmountOut > poolAmountIn + add_value, poolAmountIn + add_value > poolAmountIn) 61 | m.generate_testcase(state, name=f"BugFound{add_value}", only_if=condition) 62 | 63 | print(f'Results are in {m.workspace}') 64 | 65 | -------------------------------------------------------------------------------- /manticore/TBPoolJoinExitNoFee.py: -------------------------------------------------------------------------------- 1 | from manticore.ethereum import ManticoreEVM, ABI 2 | from manticore.core.smtlib import Operators, Z3Solver 3 | from manticore.utils import config 4 | from manticore.core.plugin import Plugin 5 | 6 | m = ManticoreEVM() 7 | 8 | # Disable the gas tracking 9 | consts_evm = config.get_group("evm") 10 | consts_evm.oog = "ignore" 11 | 12 | # Increase the solver timeout 13 | config.get_group("smt").defaultunsat = False 14 | config.get_group("smt").timeout = 3600 15 | 16 | ETHER = 10 ** 18 17 | 18 | user = m.create_account(balance=1 * ETHER) 19 | 20 | # This plugin is used to speed up the exploration and skip the require(false) paths 21 | # It won't be needed once https://github.com/trailofbits/manticore/issues/1593 is added 22 | class SkipRequire(Plugin): 23 | def will_evm_execute_instruction_callback(self, state, instruction, arguments): 24 | world = state.platform 25 | if state.platform.current_transaction.sort != 'CREATE': 26 | if instruction.semantics == "JUMPI": 27 | potential_revert = world.current_vm.read_code(world.current_vm.pc + 4) 28 | if potential_revert[0].size == 8 and potential_revert[0].value == 0xfd: 29 | state.constrain(arguments[1] == True) 30 | 31 | 32 | print(f'controller: {hex(user.address)}') 33 | 34 | skipRequire = SkipRequire() 35 | m.register_plugin(skipRequire) 36 | 37 | TestBpool = m.solidity_create_contract('./manticore/contracts/TBPoolJoinExitNoFee.sol', 38 | contract_name='TBPoolJoinExitNoFee', 39 | owner=user) 40 | 41 | print(f'TestJoinExit deployed {hex(TestBpool.address)}') 42 | 43 | # Call joinAndExitNoFeePool with symbolic values 44 | poolAmountOut = m.make_symbolic_value() 45 | poolAmountIn = m.make_symbolic_value() 46 | poolTotal = m.make_symbolic_value() 47 | _records_t_balance = m.make_symbolic_value() 48 | TestBpool.joinAndExitNoFeePool(poolAmountOut, poolAmountIn, poolTotal, _records_t_balance) 49 | 50 | print(f'joinAndExitNoFeePool Called') 51 | 52 | for state in m.ready_states: 53 | 54 | m.generate_testcase(state, name="BugFound") 55 | 56 | # Look over the 10**i, and try to generate more free tokens 57 | for i in range(0, 18): 58 | print(i) 59 | add_value = 10**i 60 | condition = Operators.AND(poolAmountOut > poolAmountIn + add_value, poolAmountIn + add_value > poolAmountIn) 61 | m.generate_testcase(state, name=f"BugFound{add_value}", only_if=condition) 62 | 63 | print(f'Results are in {m.workspace}') 64 | 65 | -------------------------------------------------------------------------------- /manticore/TBPoolJoinPool.py: -------------------------------------------------------------------------------- 1 | from manticore.ethereum import ManticoreEVM, ABI 2 | from manticore.core.smtlib import Operators, Z3Solver 3 | from manticore.utils import config 4 | from manticore.core.plugin import Plugin 5 | 6 | m = ManticoreEVM() 7 | 8 | # Disable the gas tracking 9 | consts_evm = config.get_group("evm") 10 | consts_evm.oog = "ignore" 11 | 12 | # Increase the solver timeout 13 | config.get_group("smt").defaultunsat = False 14 | config.get_group("smt").timeout = 3600 15 | 16 | ETHER = 10 ** 18 17 | 18 | user = m.create_account(balance=1 * ETHER) 19 | 20 | # This plugin is used to speed up the exploration and skip the require(false) paths 21 | # It won't be needed once https://github.com/trailofbits/manticore/issues/1593 is added 22 | class SkipRequire(Plugin): 23 | def will_evm_execute_instruction_callback(self, state, instruction, arguments): 24 | world = state.platform 25 | if state.platform.current_transaction.sort != 'CREATE': 26 | if instruction.semantics == "JUMPI": 27 | potential_revert = world.current_vm.read_code(world.current_vm.pc + 4) 28 | if potential_revert[0].size == 8 and potential_revert[0].value == 0xfd: 29 | state.constrain(arguments[1] == True) 30 | 31 | 32 | print(f'controller: {hex(user.address)}') 33 | 34 | skipRequire = SkipRequire() 35 | m.register_plugin(skipRequire) 36 | 37 | TestBpool = m.solidity_create_contract('./manticore/contracts/TBPoolJoinPool.sol', 38 | contract_name='TBPoolJoinPool', 39 | owner=user) 40 | 41 | print(f'TBPoolJoinPool deployed {hex(TestBpool.address)}') 42 | 43 | # Call joinAndExitNoFeePool with symbolic values 44 | poolAmountOut = m.make_symbolic_value() 45 | poolTotal = m.make_symbolic_value() 46 | _records_t_balance = m.make_symbolic_value() 47 | TestBpool.joinPool(poolAmountOut, poolTotal, _records_t_balance) 48 | 49 | print(f'joinPool Called') 50 | 51 | for state in m.ready_states: 52 | 53 | m.generate_testcase(state, name="BugFound") 54 | 55 | # Look over the 10**i, and try to generate more free tokens 56 | for i in range(0, 18): 57 | print(i) 58 | add_value = 10**i 59 | condition = Operators.AND(poolAmountOut > poolAmountIn + add_value, poolAmountIn + add_value > poolAmountIn) 60 | m.generate_testcase(state, name=f"BugFound{add_value}", only_if=condition) 61 | 62 | print(f'Results are in {m.workspace}') 63 | 64 | -------------------------------------------------------------------------------- /manticore/contracts/BNum.sol: -------------------------------------------------------------------------------- 1 | // This file is a flatenen verison of BNum 2 | // where require(cond, string) where replaced by require(cond) 3 | // To allow SkipRequire to work properly 4 | // It won't be needed once https://github.com/trailofbits/manticore/issues/1593 is added 5 | 6 | contract BConst { 7 | uint internal constant BONE = 10**18; 8 | 9 | uint internal constant MAX_BOUND_TOKENS = 8; 10 | uint internal constant BPOW_PRECISION = BONE / 10**10; 11 | 12 | uint internal constant MIN_FEE = BONE / 10**6; 13 | uint internal constant MAX_FEE = BONE / 10; 14 | uint internal constant EXIT_FEE = BONE / 10000; 15 | 16 | uint internal constant MIN_WEIGHT = BONE; 17 | uint internal constant MAX_WEIGHT = BONE * 50; 18 | uint internal constant MAX_TOTAL_WEIGHT = BONE * 50; 19 | uint internal constant MIN_BALANCE = BONE / 10**12; 20 | uint internal constant MAX_BALANCE = BONE * 10**12; 21 | 22 | uint internal constant MIN_POOL_SUPPLY = BONE; 23 | 24 | uint internal constant MIN_BPOW_BASE = 1 wei; 25 | uint internal constant MAX_BPOW_BASE = (2 * BONE) - 1 wei; 26 | 27 | uint internal constant MAX_IN_RATIO = BONE / 2; 28 | uint internal constant MAX_OUT_RATIO = (BONE / 3) + 1 wei; 29 | 30 | } 31 | contract BNum is BConst { 32 | 33 | 34 | function badd(uint a, uint b) 35 | internal pure 36 | returns (uint) 37 | { 38 | uint c = a + b; 39 | require(c >= a); 40 | return c; 41 | } 42 | 43 | function bsub(uint a, uint b) 44 | internal pure 45 | returns (uint) 46 | { 47 | (uint c, bool flag) = bsubSign(a, b); 48 | require(!flag); 49 | return c; 50 | } 51 | 52 | function bsubSign(uint a, uint b) 53 | internal pure 54 | returns (uint, bool) 55 | { 56 | if (a >= b) { 57 | return (a - b, false); 58 | } else { 59 | return (b - a, true); 60 | } 61 | } 62 | 63 | function bmul(uint a, uint b) 64 | internal pure 65 | returns (uint) 66 | { 67 | uint c0 = a * b; 68 | require(a == 0 || c0 / a == b); 69 | uint c1 = c0 + (BONE / 2); 70 | require(c1 >= c0); 71 | uint c2 = c1 / BONE; 72 | return c2; 73 | } 74 | 75 | function bdiv(uint a, uint b) 76 | internal pure 77 | returns (uint) 78 | { 79 | require(b != 0); 80 | uint c0 = a * BONE; 81 | require(a == 0 || c0 / a == BONE); // bmul overflow 82 | uint c1 = c0 + (b / 2); 83 | require(c1 >= c0); // badd require 84 | uint c2 = c1 / b; 85 | return c2; 86 | } 87 | 88 | } -------------------------------------------------------------------------------- /manticore/contracts/TBPoolJoinExitPool.sol: -------------------------------------------------------------------------------- 1 | import "./BNum.sol"; 2 | 3 | // This test is similar to TBPoolJoin but with an exit fee 4 | contract TBPoolJoinExit is BNum { 5 | 6 | // joinPool models the BPool.joinPool behavior for one token 7 | function joinPool(uint poolAmountOut, uint poolTotal, uint _records_t_balance) 8 | internal pure returns(uint) 9 | { 10 | uint ratio = bdiv(poolAmountOut, poolTotal); 11 | require(ratio != 0); 12 | 13 | uint bal = _records_t_balance; 14 | uint tokenAmountIn = bmul(ratio, bal); 15 | 16 | return tokenAmountIn; 17 | } 18 | 19 | // exitPool models the BPool.exitPool behavior for one token 20 | function exitPool(uint poolAmountIn, uint poolTotal, uint _records_t_balance) 21 | internal pure returns(uint) 22 | { 23 | uint exitFee = bmul(poolAmountIn, EXIT_FEE); 24 | uint pAiAfterExitFee = bsub(poolAmountIn, exitFee); 25 | uint ratio = bdiv(pAiAfterExitFee, poolTotal); 26 | require(ratio != 0); 27 | 28 | uint bal = _records_t_balance; 29 | uint tokenAmountOut = bmul(ratio, bal); 30 | 31 | return tokenAmountOut; 32 | } 33 | 34 | 35 | // This function model an attacker calling joinPool - exitPool and taking advantage of potential rounding 36 | // issues to generate free pool token 37 | function joinAndExitPool(uint poolAmountOut, uint poolAmountIn, uint poolTotal, uint _records_t_balance) public pure { 38 | uint tokenAmountIn = joinPool(poolAmountOut, poolTotal, _records_t_balance); 39 | 40 | // We constraint poolTotal and _records_t_balance 41 | // To have "realistic" values 42 | require(poolTotal <= 100 ether); 43 | require(poolTotal >= 1 ether); 44 | require(_records_t_balance <= 10 ether); 45 | require(_records_t_balance >= 10**6); 46 | 47 | poolTotal = badd(poolTotal, poolAmountOut); 48 | _records_t_balance = badd(_records_t_balance, tokenAmountIn); 49 | 50 | require(tokenAmountIn > 0); // prevent triggering the free token generation from joinPool 51 | 52 | require(poolTotal >= poolAmountIn); 53 | uint tokenAmountOut = exitPool(poolAmountIn, poolTotal, _records_t_balance); 54 | require(_records_t_balance >= tokenAmountOut); 55 | 56 | // We try to generate free pool share 57 | require(poolAmountOut > poolAmountIn); 58 | require(tokenAmountOut == tokenAmountIn); 59 | } 60 | 61 | } -------------------------------------------------------------------------------- /manticore/contracts/TBPoolJoinExitPoolNoFee.sol: -------------------------------------------------------------------------------- 1 | import "./BNum.sol"; 2 | 3 | // This test is similar to TBPoolJoinExit but with no exit fee 4 | contract TBPoolJoinExitNoFee is BNum { 5 | 6 | bool public echidna_no_bug_found = true; 7 | 8 | // joinPool models the BPool.joinPool behavior for one token 9 | function joinPool(uint poolAmountOut, uint poolTotal, uint _records_t_balance) 10 | internal pure returns(uint) 11 | { 12 | uint ratio = bdiv(poolAmountOut, poolTotal); 13 | require(ratio != 0); 14 | 15 | uint bal = _records_t_balance; 16 | uint tokenAmountIn = bmul(ratio, bal); 17 | 18 | return tokenAmountIn; 19 | } 20 | 21 | // exitPool models the BPool.exitPool behavior for one token where no fee is applied 22 | function exitPoolNoFee(uint poolAmountIn, uint poolTotal, uint _records_t_balance) 23 | internal pure returns(uint) 24 | { 25 | uint ratio = bdiv(poolAmountIn, poolTotal); 26 | require(ratio != 0); 27 | 28 | uint bal = _records_t_balance; 29 | uint tokenAmountOut = bmul(ratio, bal); 30 | 31 | return tokenAmountOut; 32 | } 33 | 34 | // This function model an attacker calling joinPool - exitPool and taking advantage of potential rounding 35 | // issues to generate free pool token 36 | function joinAndExitNoFeePool(uint poolAmountOut, uint poolAmountIn, uint poolTotal, uint _records_t_balance) public { 37 | uint tokenAmountIn = joinPool(poolAmountOut, poolTotal, _records_t_balance); 38 | 39 | // We constraint poolTotal and _records_t_balance 40 | // To have "realistic" values 41 | require(poolTotal <= 100 ether); 42 | require(poolTotal >= 1 ether); 43 | require(_records_t_balance <= 10 ether); 44 | require(_records_t_balance >= 10**6); 45 | 46 | poolTotal = badd(poolTotal, poolAmountOut); 47 | _records_t_balance = badd(_records_t_balance, tokenAmountIn); 48 | 49 | require(tokenAmountIn > 0); // prevent triggering the free token generation from joinPool 50 | 51 | require(poolTotal >= poolAmountIn); 52 | uint tokenAmountOut = exitPoolNoFee(poolAmountIn, poolTotal, _records_t_balance); 53 | require(_records_t_balance >= tokenAmountOut); 54 | 55 | // We try to generate free pool share 56 | require(poolAmountOut > poolAmountIn); 57 | require(tokenAmountOut == tokenAmountIn); 58 | echidna_no_bug_found = false; 59 | } 60 | 61 | 62 | } -------------------------------------------------------------------------------- /manticore/contracts/TBPoolJoinPool.sol: -------------------------------------------------------------------------------- 1 | import "./BNum.sol"; 2 | 3 | contract TBPoolJoinPool is BNum { 4 | 5 | bool public echidna_no_bug_found = true; 6 | 7 | // joinPool models the BPool.joinPool behavior for one token 8 | // A bug is found if poolAmountOut is greater than 0 9 | // And tokenAmountIn is 0 10 | function joinPool(uint poolAmountOut, uint poolTotal, uint _records_t_balance) 11 | public returns(uint) 12 | { 13 | // We constraint poolTotal and _records_t_balance 14 | // To have "realistic" values 15 | require(poolTotal <= 100 ether); 16 | require(poolTotal >= 1 ether); 17 | require(_records_t_balance <= 10 ether); 18 | require(_records_t_balance >= 10**6); 19 | 20 | uint ratio = bdiv(poolAmountOut, poolTotal); 21 | require(ratio != 0); 22 | 23 | uint bal = _records_t_balance; 24 | uint tokenAmountIn = bmul(ratio, bal); 25 | 26 | require(poolAmountOut > 0); 27 | require(tokenAmountIn == 0); 28 | 29 | echidna_no_bug_found = false; 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /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_factories.js: -------------------------------------------------------------------------------- 1 | const TMath = artifacts.require('TMath'); 2 | const BToken = artifacts.require('BToken'); 3 | const BFactory = artifacts.require('BFactory'); 4 | 5 | module.exports = async function (deployer, network, accounts) { 6 | if (network === 'development' || network === 'coverage') { 7 | deployer.deploy(TMath); 8 | } 9 | deployer.deploy(BFactory); 10 | }; 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "balancer-core", 4 | "version": "0.0.7", 5 | "license": "GPL-3.0-only", 6 | "description": "Balancer Core Contracts and ABI", 7 | "scripts": { 8 | "compile": "truffle compile", 9 | "testrpc": "ganache-cli --deterministic --gasLimit 10000000", 10 | "test": "truffle test", 11 | "test:verbose": "VERBOSE=true truffle test", 12 | "coverage": "yarn solidity-coverage", 13 | "lint": "eslint .", 14 | "lint:contracts": "solhint contracts/*.sol" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/balancer-labs/balancer-core.git" 19 | }, 20 | "bugs": { 21 | "url": "https://github.com/balancer-labs/balancer-core/issues" 22 | }, 23 | "homepage": "https://github.com/balancer-labs/balancer-core#readme", 24 | "devDependencies": { 25 | "chai": "^4.2.0", 26 | "coveralls": "^3.0.8", 27 | "eslint": "^6.7.1", 28 | "eslint-config-airbnb": "^18.0.1", 29 | "eslint-plugin-import": "^2.18.2", 30 | "eslint-plugin-jsx-a11y": "^6.2.3", 31 | "eslint-plugin-react": "^7.17.0", 32 | "ganache-core": "^2.6.1", 33 | "mocha": "^6.2.0", 34 | "solhint": "^2.3.0", 35 | "solidity-coverage": "^0.6.7", 36 | "standard": "^14.0.2", 37 | "truffle": "^5.0.41", 38 | "truffle-assertions": "^0.9.1", 39 | "web3": "^1.2.0" 40 | }, 41 | "dependencies": { 42 | "decimal.js": "^10.2.0", 43 | "ganache-cli": "^6.7.0", 44 | "global": "^4.4.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/factory.js: -------------------------------------------------------------------------------- 1 | const BPool = artifacts.require('BPool'); 2 | const BFactory = artifacts.require('BFactory'); 3 | const TToken = artifacts.require('TToken'); 4 | const truffleAssert = require('truffle-assertions'); 5 | 6 | contract('BFactory', async (accounts) => { 7 | const admin = accounts[0]; 8 | const nonAdmin = accounts[1]; 9 | const user2 = accounts[2]; 10 | const { toWei } = web3.utils; 11 | const { fromWei } = web3.utils; 12 | const { hexToUtf8 } = web3.utils; 13 | 14 | const MAX = web3.utils.toTwosComplement(-1); 15 | 16 | describe('Factory', () => { 17 | let factory; 18 | let pool; 19 | let POOL; 20 | let WETH; 21 | let DAI; 22 | let weth; 23 | let dai; 24 | 25 | before(async () => { 26 | factory = await BFactory.deployed(); 27 | weth = await TToken.new('Wrapped Ether', 'WETH', 18); 28 | dai = await TToken.new('Dai Stablecoin', 'DAI', 18); 29 | 30 | WETH = weth.address; 31 | DAI = dai.address; 32 | 33 | // admin balances 34 | await weth.mint(admin, toWei('5')); 35 | await dai.mint(admin, toWei('200')); 36 | 37 | // nonAdmin balances 38 | await weth.mint(nonAdmin, toWei('1'), { from: admin }); 39 | await dai.mint(nonAdmin, toWei('50'), { from: admin }); 40 | 41 | POOL = await factory.newBPool.call(); // this works fine in clean room 42 | await factory.newBPool(); 43 | pool = await BPool.at(POOL); 44 | 45 | await weth.approve(POOL, MAX); 46 | await dai.approve(POOL, MAX); 47 | 48 | await weth.approve(POOL, MAX, { from: nonAdmin }); 49 | await dai.approve(POOL, MAX, { from: nonAdmin }); 50 | }); 51 | 52 | it('BFactory is bronze release', async () => { 53 | const color = await factory.getColor(); 54 | assert.equal(hexToUtf8(color), 'BRONZE'); 55 | }); 56 | 57 | it('isBPool on non pool returns false', async () => { 58 | const isBPool = await factory.isBPool(admin); 59 | assert.isFalse(isBPool); 60 | }); 61 | 62 | it('isBPool on pool returns true', async () => { 63 | const isBPool = await factory.isBPool(POOL); 64 | assert.isTrue(isBPool); 65 | }); 66 | 67 | it('fails nonAdmin calls collect', async () => { 68 | await truffleAssert.reverts(factory.collect(nonAdmin, { from: nonAdmin }), 'ERR_NOT_BLABS'); 69 | }); 70 | 71 | it('admin collects fees', async () => { 72 | await pool.bind(WETH, toWei('5'), toWei('5')); 73 | await pool.bind(DAI, toWei('200'), toWei('5')); 74 | 75 | await pool.finalize(); 76 | 77 | await pool.joinPool(toWei('10'), [MAX, MAX], { from: nonAdmin }); 78 | await pool.exitPool(toWei('10'), [toWei('0'), toWei('0')], { from: nonAdmin }); 79 | 80 | // Exit fee = 0 so this wont do anything 81 | await factory.collect(POOL); 82 | 83 | const adminBalance = await pool.balanceOf(admin); 84 | assert.equal(fromWei(adminBalance), '100'); 85 | }); 86 | 87 | it('nonadmin cant set blabs address', async () => { 88 | await truffleAssert.reverts(factory.setBLabs(nonAdmin, { from: nonAdmin }), 'ERR_NOT_BLABS'); 89 | }); 90 | 91 | it('admin changes blabs address', async () => { 92 | await factory.setBLabs(user2); 93 | const blab = await factory.getBLabs(); 94 | assert.equal(blab, user2); 95 | }); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /test/math_extreme_weights.js: -------------------------------------------------------------------------------- 1 | const Decimal = require('decimal.js'); 2 | const truffleAssert = require('truffle-assertions'); 3 | const { calcRelativeDiff } = require('../lib/calc_comparisons'); 4 | 5 | const BPool = artifacts.require('BPool'); 6 | const BFactory = artifacts.require('BFactory'); 7 | const TToken = artifacts.require('TToken'); 8 | const errorDelta = 10 ** -8; 9 | const swapFee = 0.001; // 0.001; 10 | const exitFee = 0; 11 | const verbose = process.env.VERBOSE; 12 | 13 | 14 | contract('BPool', async (accounts) => { 15 | const admin = accounts[0]; 16 | const { toWei } = web3.utils; 17 | const { fromWei } = web3.utils; 18 | const MAX = web3.utils.toTwosComplement(-1); 19 | 20 | let WETH; let DAI; 21 | let weth; let dai; 22 | let factory; // BPool factory 23 | let pool; // first pool w/ defaults 24 | let POOL; // pool address 25 | 26 | const wethBalance = '1000'; 27 | const wethDenorm = '1'; 28 | 29 | let currentWethBalance = Decimal(wethBalance); 30 | let previousWethBalance = currentWethBalance; 31 | 32 | const daiBalance = '1000'; 33 | const daiDenorm = '49'; 34 | 35 | let currentDaiBalance = Decimal(daiBalance); 36 | let previousDaiBalance = currentDaiBalance; 37 | 38 | let currentPoolBalance = Decimal(0); 39 | let previousPoolBalance = Decimal(0); 40 | 41 | const sumWeights = Decimal(wethDenorm).add(Decimal(daiDenorm)); 42 | const wethNorm = Decimal(wethDenorm).div(Decimal(sumWeights)); 43 | const daiNorm = Decimal(daiDenorm).div(Decimal(sumWeights)); 44 | 45 | async function logAndAssertCurrentBalances() { 46 | let expected = currentPoolBalance; 47 | let actual = await pool.totalSupply(); 48 | actual = Decimal(fromWei(actual)); 49 | let relDif = calcRelativeDiff(expected, actual); 50 | if (verbose) { 51 | console.log('Pool Balance'); 52 | console.log(`expected: ${expected})`); 53 | console.log(`actual : ${actual})`); 54 | console.log(`relDif : ${relDif})`); 55 | } 56 | 57 | assert.isAtMost(relDif.toNumber(), errorDelta); 58 | 59 | expected = currentWethBalance; 60 | actual = await pool.getBalance(WETH); 61 | actual = Decimal(fromWei(actual)); 62 | relDif = calcRelativeDiff(expected, actual); 63 | if (verbose) { 64 | console.log('WETH Balance'); 65 | console.log(`expected: ${expected})`); 66 | console.log(`actual : ${actual})`); 67 | console.log(`relDif : ${relDif})`); 68 | } 69 | 70 | assert.isAtMost(relDif.toNumber(), errorDelta); 71 | 72 | expected = currentDaiBalance; 73 | actual = await pool.getBalance(DAI); 74 | actual = Decimal(fromWei(actual)); 75 | relDif = calcRelativeDiff(expected, actual); 76 | if (verbose) { 77 | console.log('Dai Balance'); 78 | console.log(`expected: ${expected})`); 79 | console.log(`actual : ${actual})`); 80 | console.log(`relDif : ${relDif})`); 81 | } 82 | 83 | assert.isAtMost(relDif.toNumber(), errorDelta); 84 | } 85 | 86 | before(async () => { 87 | factory = await BFactory.deployed(); 88 | 89 | POOL = await factory.newBPool.call(); // this works fine in clean room 90 | await factory.newBPool(); 91 | pool = await BPool.at(POOL); 92 | 93 | weth = await TToken.new('Wrapped Ether', 'WETH', 18); 94 | dai = await TToken.new('Dai Stablecoin', 'DAI', 18); 95 | 96 | WETH = weth.address; 97 | DAI = dai.address; 98 | 99 | await weth.mint(admin, MAX); 100 | await dai.mint(admin, MAX); 101 | 102 | await weth.approve(POOL, MAX); 103 | await dai.approve(POOL, MAX); 104 | 105 | 106 | await pool.bind(WETH, toWei(wethBalance), toWei(wethDenorm)); 107 | await pool.bind(DAI, toWei(daiBalance), toWei(daiDenorm)); 108 | 109 | await pool.setPublicSwap(true); 110 | 111 | await pool.setSwapFee(toWei(String(swapFee))); 112 | }); 113 | 114 | describe('Extreme weights', () => { 115 | it('swapExactAmountIn', async () => { 116 | const tokenIn = WETH; 117 | const tokenInAmount = toWei('500'); 118 | const tokenOut = DAI; 119 | const minAmountOut = toWei('0'); 120 | const maxPrice = MAX; 121 | 122 | const output = await pool.swapExactAmountIn.call( 123 | tokenIn, tokenInAmount, tokenOut, minAmountOut, maxPrice, 124 | ); 125 | 126 | // Checking outputs 127 | let expected = Decimal('8.23390841016124456'); 128 | let actual = Decimal(fromWei(output.tokenAmountOut)); 129 | let relDif = calcRelativeDiff(expected, actual); 130 | 131 | if (verbose) { 132 | console.log('output[0]'); 133 | console.log(`expected: ${expected})`); 134 | console.log(`actual : ${actual})`); 135 | console.log(`relDif : ${relDif})`); 136 | } 137 | 138 | assert.isAtMost(relDif.toNumber(), errorDelta); 139 | 140 | expected = Decimal('74.1844011380065814'); 141 | actual = Decimal(fromWei(output.spotPriceAfter)); 142 | relDif = calcRelativeDiff(expected, actual); 143 | 144 | if (verbose) { 145 | console.log('output[1]'); 146 | console.log(`expected: ${expected})`); 147 | console.log(`actual : ${actual})`); 148 | console.log(`relDif : ${relDif})`); 149 | } 150 | 151 | assert.isAtMost(relDif.toNumber(), errorDelta); 152 | }); 153 | 154 | 155 | it('swapExactAmountOut', async () => { 156 | const tokenIn = WETH; 157 | const maxAmountIn = MAX; 158 | const tokenOut = DAI; 159 | const tokenAmountOut = toWei('333.333333333333333333'); 160 | const maxPrice = MAX; 161 | 162 | const output = await pool.swapExactAmountOut.call( 163 | tokenIn, maxAmountIn, tokenOut, tokenAmountOut, maxPrice, 164 | ); 165 | 166 | // Checking outputs 167 | let expected = Decimal('425506505648.348073'); 168 | let actual = Decimal(fromWei(output.tokenAmountIn)); 169 | let relDif = calcRelativeDiff(expected, actual); 170 | 171 | if (verbose) { 172 | console.log('output[0]'); 173 | console.log(`expected: ${expected})`); 174 | console.log(`actual : ${actual})`); 175 | console.log(`relDif : ${relDif})`); 176 | } 177 | 178 | assert.isAtMost(relDif.toNumber(), errorDelta); 179 | 180 | expected = Decimal('31306034272.9265099'); 181 | actual = Decimal(fromWei(output.spotPriceAfter)); 182 | relDif = calcRelativeDiff(expected, actual); 183 | 184 | if (verbose) { 185 | console.log('output[1]'); 186 | console.log(`expected: ${expected})`); 187 | console.log(`actual : ${actual})`); 188 | console.log(`relDif : ${relDif})`); 189 | } 190 | 191 | assert.isAtMost(relDif.toNumber(), errorDelta); 192 | }); 193 | 194 | it('joinPool', async () => { 195 | currentPoolBalance = '100'; 196 | await pool.finalize(); 197 | 198 | // // Call function 199 | const poolAmountOut = '1'; 200 | await pool.joinPool(toWei(poolAmountOut), [MAX, MAX]); 201 | 202 | // // Update balance states 203 | previousPoolBalance = Decimal(currentPoolBalance); 204 | currentPoolBalance = Decimal(currentPoolBalance).add(Decimal(poolAmountOut)); 205 | 206 | // Balances of all tokens increase proportionally to the pool balance 207 | previousWethBalance = currentWethBalance; 208 | let balanceChange = (Decimal(poolAmountOut).div(previousPoolBalance)).mul(previousWethBalance); 209 | currentWethBalance = currentWethBalance.add(balanceChange); 210 | previousDaiBalance = currentDaiBalance; 211 | balanceChange = (Decimal(poolAmountOut).div(previousPoolBalance)).mul(previousDaiBalance); 212 | currentDaiBalance = currentDaiBalance.add(balanceChange); 213 | 214 | // Print current balances after operation 215 | await logAndAssertCurrentBalances(); 216 | }); 217 | 218 | it('exitPool', async () => { 219 | // Call function 220 | // so that the balances of all tokens will go back exactly to what they were before joinPool() 221 | const poolAmountIn = 1 / (1 - exitFee); 222 | const poolAmountInAfterExitFee = Decimal(poolAmountIn).mul(Decimal(1).sub(exitFee)); 223 | 224 | await pool.exitPool(toWei(String(poolAmountIn)), [toWei('0'), toWei('0')]); 225 | 226 | // Update balance states 227 | previousPoolBalance = currentPoolBalance; 228 | currentPoolBalance = currentPoolBalance.sub(poolAmountInAfterExitFee); 229 | // Balances of all tokens increase proportionally to the pool balance 230 | previousWethBalance = currentWethBalance; 231 | let balanceChange = (poolAmountInAfterExitFee.div(previousPoolBalance)).mul(previousWethBalance); 232 | currentWethBalance = currentWethBalance.sub(balanceChange); 233 | previousDaiBalance = currentDaiBalance; 234 | balanceChange = (poolAmountInAfterExitFee.div(previousPoolBalance)).mul(previousDaiBalance); 235 | currentDaiBalance = currentDaiBalance.sub(balanceChange); 236 | 237 | // Print current balances after operation 238 | await logAndAssertCurrentBalances(); 239 | }); 240 | 241 | 242 | it('joinswapExternAmountIn', async () => { 243 | // Call function 244 | const tokenRatio = 1.1; 245 | // increase tbalance by 1.1 after swap fee 246 | const tokenAmountIn = (1 / (1 - swapFee * (1 - wethNorm))) * (currentWethBalance * (tokenRatio - 1)); 247 | await pool.joinswapExternAmountIn(WETH, toWei(String(tokenAmountIn)), toWei('0')); 248 | // Update balance states 249 | previousWethBalance = currentWethBalance; 250 | currentWethBalance = currentWethBalance.add(Decimal(tokenAmountIn)); 251 | previousPoolBalance = currentPoolBalance; 252 | currentPoolBalance = currentPoolBalance.mul(Decimal(tokenRatio).pow(wethNorm)); // increase by 1.1**wethNorm 253 | 254 | // Print current balances after operation 255 | await logAndAssertCurrentBalances(); 256 | }); 257 | 258 | 259 | it('joinswapPoolAmountOut', async () => { 260 | // Call function 261 | const poolRatio = 1.1; 262 | const poolAmountOut = currentPoolBalance * (poolRatio - 1); 263 | await pool.joinswapPoolAmountOut(DAI, toWei(String(poolAmountOut)), MAX); 264 | // Update balance states 265 | previousPoolBalance = currentPoolBalance; 266 | currentPoolBalance = currentPoolBalance.mul(Decimal(poolRatio)); // increase by 1.1 267 | previousDaiBalance = currentDaiBalance; 268 | const numer = previousDaiBalance.mul(Decimal(poolRatio).pow(Decimal(1).div(daiNorm)).sub(Decimal(1))); 269 | const denom = Decimal(1).sub((Decimal(swapFee)).mul((Decimal(1).sub(daiNorm)))); 270 | currentDaiBalance = currentDaiBalance.plus(numer.div(denom)); 271 | 272 | // Print current balances after operation 273 | await logAndAssertCurrentBalances(); 274 | }); 275 | 276 | it('joinswapExternAmountIn should revert', async () => { 277 | // Call function 278 | const tokenRatio = 1.1; 279 | const tokenAmountIn = (1 / (1 - swapFee * (1 - wethNorm))) * (currentWethBalance * (tokenRatio)); 280 | await truffleAssert.reverts( 281 | pool.joinswapExternAmountIn(WETH, toWei(String(tokenAmountIn)), toWei('0')), 282 | 'ERR_MAX_IN_RATIO', 283 | ); 284 | }); 285 | 286 | it('joinswapPoolAmountOut should revert', async () => { 287 | // Call function 288 | const poolRatio = 0.9; 289 | const poolAmountOut = currentPoolBalance * (poolRatio); 290 | await truffleAssert.reverts( 291 | pool.joinswapPoolAmountOut(DAI, toWei(String(poolAmountOut)), MAX), 292 | 'ERR_MAX_IN_RATIO', 293 | ); 294 | }); 295 | 296 | it('exitswapExternAmountOut should revert', async () => { 297 | // Call function 298 | const poolRatioAfterExitFee = 1.1; 299 | const tokenRatioBeforeSwapFee = poolRatioAfterExitFee ** (1 / daiNorm); 300 | const tokenAmountOut = currentDaiBalance * (1 - tokenRatioBeforeSwapFee) * (1 - swapFee * (1 - daiNorm)); 301 | await truffleAssert.reverts( 302 | pool.exitswapExternAmountOut(DAI, toWei(String(tokenAmountOut)), MAX), 303 | 'ERR_MAX_OUT_RATIO', 304 | ); 305 | }); 306 | 307 | it('exitswapPoolAmountIn should revert', async () => { 308 | // Call function 309 | const poolRatioAfterExitFee = 0.9; 310 | const poolAmountIn = currentPoolBalance * (1 - poolRatioAfterExitFee) * (1 / (1 - exitFee)); 311 | await truffleAssert.reverts( 312 | pool.exitswapPoolAmountIn(WETH, toWei(String(poolAmountIn)), toWei('0')), 313 | 'ERR_MAX_OUT_RATIO', 314 | ); 315 | }); 316 | 317 | it('exitswapExternAmountOut', async () => { 318 | // Call function 319 | const poolRatioAfterExitFee = 0.9; 320 | const tokenRatioBeforeSwapFee = poolRatioAfterExitFee ** (1 / daiNorm); 321 | const tokenAmountOut = currentDaiBalance * (1 - tokenRatioBeforeSwapFee) * (1 - swapFee * (1 - daiNorm)); 322 | await pool.exitswapExternAmountOut(DAI, toWei(String(tokenAmountOut)), MAX); 323 | // Update balance states 324 | previousDaiBalance = currentDaiBalance; 325 | currentDaiBalance = currentDaiBalance.sub(Decimal(tokenAmountOut)); 326 | previousPoolBalance = currentPoolBalance; 327 | const balanceChange = previousPoolBalance.mul(Decimal(1).sub(Decimal(poolRatioAfterExitFee))); 328 | currentPoolBalance = currentPoolBalance.sub(balanceChange); 329 | 330 | // Print current balances after operation 331 | await logAndAssertCurrentBalances(); 332 | }); 333 | 334 | it('poolAmountOut = joinswapExternAmountIn(joinswapPoolAmountOut(poolAmountOut))', async () => { 335 | const poolAmountOut = 0.1; 336 | const tokenAmountIn = await pool.joinswapPoolAmountOut.call(WETH, toWei(String(poolAmountOut)), MAX); 337 | const pAo = await pool.joinswapExternAmountIn.call(WETH, String(tokenAmountIn), toWei('0')); 338 | 339 | const expected = Decimal(poolAmountOut); 340 | const actual = Decimal(fromWei(pAo)); 341 | const relDif = calcRelativeDiff(expected, actual); 342 | 343 | if (verbose) { 344 | console.log(`tokenAmountIn: ${tokenAmountIn})`); 345 | console.log('poolAmountOut'); 346 | console.log(`expected: ${expected})`); 347 | console.log(`actual : ${actual})`); 348 | console.log(`relDif : ${relDif})`); 349 | } 350 | 351 | assert.isAtMost(relDif.toNumber(), errorDelta); 352 | }); 353 | 354 | 355 | it('tokenAmountIn = joinswapPoolAmountOut(joinswapExternAmountIn(tokenAmountIn))', async () => { 356 | const tokenAmountIn = '1'; 357 | const poolAmountOut = await pool.joinswapExternAmountIn.call(DAI, toWei(tokenAmountIn), toWei('0')); 358 | const calculatedtokenAmountIn = await pool.joinswapPoolAmountOut.call(DAI, String(poolAmountOut), MAX); 359 | 360 | const expected = Decimal(tokenAmountIn); 361 | const actual = Decimal(fromWei(calculatedtokenAmountIn)); 362 | const relDif = calcRelativeDiff(expected, actual); 363 | 364 | if (verbose) { 365 | console.log(`poolAmountOut: ${poolAmountOut})`); 366 | console.log('tokenAmountIn'); 367 | console.log(`expected: ${expected})`); 368 | console.log(`actual : ${actual})`); 369 | console.log(`relDif : ${relDif})`); 370 | } 371 | 372 | assert.isAtMost(relDif.toNumber(), errorDelta); 373 | }); 374 | 375 | 376 | it('poolAmountIn = exitswapExternAmountOut(exitswapPoolAmountIn(poolAmountIn))', async () => { 377 | const poolAmountIn = 0.1; 378 | const tokenAmountOut = await pool.exitswapPoolAmountIn.call(WETH, toWei(String(poolAmountIn)), toWei('0')); 379 | const calculatedpoolAmountIn = await pool.exitswapExternAmountOut.call(WETH, String(tokenAmountOut), MAX); 380 | 381 | const expected = Decimal(poolAmountIn); 382 | const actual = Decimal(fromWei(calculatedpoolAmountIn)); 383 | const relDif = calcRelativeDiff(expected, actual); 384 | 385 | if (verbose) { 386 | console.log(`tokenAmountOut: ${tokenAmountOut})`); 387 | console.log('poolAmountIn'); 388 | console.log(`expected: ${expected})`); 389 | console.log(`actual : ${actual})`); 390 | console.log(`relDif : ${relDif})`); 391 | } 392 | 393 | assert.isAtMost(relDif.toNumber(), errorDelta); 394 | }); 395 | 396 | 397 | it('tokenAmountOut = exitswapPoolAmountIn(exitswapExternAmountOut(tokenAmountOut))', async () => { 398 | const tokenAmountOut = 1; 399 | const poolAmountIn = await pool.exitswapExternAmountOut.call(DAI, toWei(String(tokenAmountOut)), MAX); 400 | const tAo = await pool.exitswapPoolAmountIn.call(DAI, String(poolAmountIn), toWei('0')); 401 | 402 | const expected = Decimal(tokenAmountOut); 403 | const actual = Decimal(fromWei(tAo)); 404 | const relDif = calcRelativeDiff(expected, actual); 405 | 406 | if (verbose) { 407 | console.log(`poolAmountIn: ${poolAmountIn})`); 408 | console.log('tokenAmountOut'); 409 | console.log(`expected: ${expected})`); 410 | console.log(`actual : ${actual})`); 411 | console.log(`relDif : ${relDif})`); 412 | } 413 | 414 | assert.isAtMost(relDif.toNumber(), errorDelta); 415 | }); 416 | }); 417 | }); 418 | -------------------------------------------------------------------------------- /test/math_with_fees.js: -------------------------------------------------------------------------------- 1 | const Decimal = require('decimal.js'); 2 | const { 3 | calcSpotPrice, 4 | calcOutGivenIn, 5 | calcInGivenOut, 6 | calcRelativeDiff, 7 | } = require('../lib/calc_comparisons'); 8 | 9 | const BPool = artifacts.require('BPool'); 10 | const BFactory = artifacts.require('BFactory'); 11 | const TToken = artifacts.require('TToken'); 12 | const errorDelta = 10 ** -8; 13 | const swapFee = 10 ** -3; // 0.001; 14 | const exitFee = 0; 15 | const verbose = process.env.VERBOSE; 16 | 17 | contract('BPool', async (accounts) => { 18 | const { toWei } = web3.utils; 19 | const { fromWei } = web3.utils; 20 | const admin = accounts[0]; 21 | 22 | const MAX = web3.utils.toTwosComplement(-1); 23 | 24 | let WETH; let DAI; // addresses 25 | let weth; let dai; // TTokens 26 | let factory; // BPool factory 27 | let pool; // first pool w/ defaults 28 | let POOL; // pool address 29 | 30 | const wethBalance = '4'; 31 | const wethDenorm = '10'; 32 | 33 | let currentWethBalance = Decimal(wethBalance); 34 | let previousWethBalance = currentWethBalance; 35 | 36 | const daiBalance = '12'; 37 | const daiDenorm = '10'; 38 | 39 | let currentDaiBalance = Decimal(daiBalance); 40 | let previousDaiBalance = currentDaiBalance; 41 | 42 | let currentPoolBalance = Decimal(0); 43 | let previousPoolBalance = Decimal(0); 44 | 45 | const sumWeights = Decimal(wethDenorm).add(Decimal(daiDenorm)); 46 | const wethNorm = Decimal(wethDenorm).div(Decimal(sumWeights)); 47 | const daiNorm = Decimal(daiDenorm).div(Decimal(sumWeights)); 48 | 49 | async function logAndAssertCurrentBalances() { 50 | let expected = currentPoolBalance; 51 | let actual = await pool.totalSupply(); 52 | actual = Decimal(fromWei(actual)); 53 | let relDif = calcRelativeDiff(expected, actual); 54 | if (verbose) { 55 | console.log('Pool Balance'); 56 | console.log(`expected: ${expected})`); 57 | console.log(`actual : ${actual})`); 58 | console.log(`relDif : ${relDif})`); 59 | } 60 | 61 | assert.isAtMost(relDif.toNumber(), errorDelta); 62 | 63 | expected = currentWethBalance; 64 | actual = await pool.getBalance(WETH); 65 | actual = Decimal(fromWei(actual)); 66 | relDif = calcRelativeDiff(expected, actual); 67 | if (verbose) { 68 | console.log('WETH Balance'); 69 | console.log(`expected: ${expected})`); 70 | console.log(`actual : ${actual})`); 71 | console.log(`relDif : ${relDif})`); 72 | } 73 | 74 | assert.isAtMost(relDif.toNumber(), errorDelta); 75 | 76 | expected = currentDaiBalance; 77 | actual = await pool.getBalance(DAI); 78 | actual = Decimal(fromWei(actual)); 79 | relDif = calcRelativeDiff(expected, actual); 80 | if (verbose) { 81 | console.log('Dai Balance'); 82 | console.log(`expected: ${expected})`); 83 | console.log(`actual : ${actual})`); 84 | console.log(`relDif : ${relDif})`); 85 | } 86 | 87 | assert.isAtMost(relDif.toNumber(), errorDelta); 88 | } 89 | 90 | before(async () => { 91 | factory = await BFactory.deployed(); 92 | 93 | POOL = await factory.newBPool.call(); // this works fine in clean room 94 | await factory.newBPool(); 95 | pool = await BPool.at(POOL); 96 | 97 | weth = await TToken.new('Wrapped Ether', 'WETH', 18); 98 | dai = await TToken.new('Dai Stablecoin', 'DAI', 18); 99 | 100 | WETH = weth.address; 101 | DAI = dai.address; 102 | 103 | await weth.mint(admin, MAX); 104 | await dai.mint(admin, MAX); 105 | 106 | await weth.approve(POOL, MAX); 107 | await dai.approve(POOL, MAX); 108 | 109 | await pool.bind(WETH, toWei(wethBalance), toWei(wethDenorm)); 110 | await pool.bind(DAI, toWei(daiBalance), toWei(daiDenorm)); 111 | 112 | await pool.setPublicSwap(true); 113 | await pool.setSwapFee(toWei(String(swapFee))); 114 | }); 115 | 116 | describe('With fees', () => { 117 | it('swapExactAmountIn', async () => { 118 | const tokenIn = WETH; 119 | const tokenAmountIn = '2'; 120 | const tokenOut = DAI; 121 | const minAmountOut = '0'; 122 | const maxPrice = MAX; 123 | 124 | const output = await pool.swapExactAmountIn.call( 125 | tokenIn, 126 | toWei(tokenAmountIn), 127 | tokenOut, 128 | toWei(minAmountOut), 129 | maxPrice, 130 | ); 131 | 132 | // Checking outputs 133 | let expected = calcOutGivenIn( 134 | currentWethBalance, 135 | wethNorm, 136 | currentDaiBalance, 137 | daiNorm, 138 | tokenAmountIn, 139 | swapFee, 140 | ); 141 | 142 | let actual = Decimal(fromWei(output[0])); 143 | let relDif = calcRelativeDiff(expected, actual); 144 | 145 | if (verbose) { 146 | console.log('output[0]'); 147 | console.log(`expected: ${expected})`); 148 | console.log(`actual : ${actual})`); 149 | console.log(`relDif : ${relDif})`); 150 | } 151 | 152 | assert.isAtMost(relDif.toNumber(), errorDelta); 153 | 154 | expected = calcSpotPrice( 155 | currentWethBalance.plus(Decimal(2)), 156 | wethNorm, 157 | currentDaiBalance.sub(actual), 158 | daiNorm, 159 | swapFee, 160 | ); 161 | // expected = 1 / ((1 - swapFee) * (4 + 2)) / (48 / (4 + 2 * (1 - swapFee))); 162 | // expected = ((1 / (1 - swapFee)) * (4 + 2)) / (48 / (4 + 2 * (1 - swapFee))); 163 | actual = fromWei(output[1]); 164 | relDif = calcRelativeDiff(expected, actual); 165 | 166 | if (verbose) { 167 | console.log('output[1]'); 168 | console.log(`expected: ${expected})`); 169 | console.log(`actual : ${actual})`); 170 | console.log(`relDif : ${relDif})`); 171 | } 172 | 173 | assert.isAtMost(relDif.toNumber(), errorDelta); 174 | }); 175 | 176 | 177 | it('swapExactAmountOut', async () => { 178 | const tokenIn = DAI; 179 | const maxAmountIn = MAX; 180 | const tokenOut = WETH; 181 | const tokenAmountOut = '1'; 182 | const maxPrice = MAX; 183 | 184 | const output = await pool.swapExactAmountOut.call( 185 | tokenIn, 186 | maxAmountIn, 187 | tokenOut, 188 | toWei(tokenAmountOut), 189 | maxPrice, 190 | ); 191 | 192 | // Checking outputs 193 | // let expected = (48 / (4 - 1) - 12) / (1 - swapFee); 194 | let expected = calcInGivenOut( 195 | currentDaiBalance, 196 | daiNorm, 197 | currentWethBalance, 198 | wethNorm, 199 | tokenAmountOut, 200 | swapFee, 201 | ); 202 | 203 | let actual = fromWei(output[0]); 204 | let relDif = calcRelativeDiff(expected, actual); 205 | 206 | if (verbose) { 207 | console.log('output[0]'); 208 | console.log(`expected: ${expected})`); 209 | console.log(`actual : ${actual})`); 210 | console.log(`relDif : ${relDif})`); 211 | } 212 | 213 | assert.isAtMost(relDif.toNumber(), errorDelta); 214 | 215 | expected = calcSpotPrice( 216 | currentDaiBalance.plus(actual), 217 | daiNorm, 218 | currentWethBalance.sub(Decimal(1)), 219 | wethNorm, 220 | swapFee, 221 | ); 222 | 223 | actual = fromWei(output[1]); 224 | relDif = calcRelativeDiff(expected, actual); 225 | 226 | if (verbose) { 227 | console.log('output[1]'); 228 | console.log(`expected: ${expected})`); 229 | console.log(`actual : ${actual})`); 230 | console.log(`relDif : ${relDif})`); 231 | } 232 | 233 | assert.isAtMost(relDif.toNumber(), errorDelta); 234 | }); 235 | 236 | it('joinPool', async () => { 237 | currentPoolBalance = '100'; 238 | await pool.finalize(); 239 | 240 | // Call function 241 | const pAo = '1'; 242 | await pool.joinPool(toWei(pAo), [MAX, MAX]); 243 | 244 | // Update balance states 245 | previousPoolBalance = Decimal(currentPoolBalance); 246 | currentPoolBalance = Decimal(currentPoolBalance).plus(Decimal(pAo)); 247 | // Balances of all tokens increase proportionally to the pool balance 248 | previousWethBalance = currentWethBalance; 249 | let balanceChange = (Decimal(pAo).div(previousPoolBalance)).mul(previousWethBalance); 250 | currentWethBalance = currentWethBalance.plus(balanceChange); 251 | previousDaiBalance = currentDaiBalance; 252 | balanceChange = (Decimal(pAo).div(previousPoolBalance)).mul(previousDaiBalance); 253 | currentDaiBalance = currentDaiBalance.plus(balanceChange); 254 | 255 | // Print current balances after operation 256 | await logAndAssertCurrentBalances(); 257 | }); 258 | 259 | it('exitPool', async () => { 260 | // Call function 261 | // so that the balances of all tokens will go back exactly to what they were before joinPool() 262 | const pAi = 1 / (1 - exitFee); 263 | const pAiAfterExitFee = pAi * (1 - exitFee); 264 | 265 | await pool.exitPool(toWei(String(pAi)), [toWei('0'), toWei('0')]); 266 | 267 | // Update balance states 268 | previousPoolBalance = currentPoolBalance; 269 | currentPoolBalance = currentPoolBalance.sub(Decimal(pAiAfterExitFee)); 270 | // Balances of all tokens increase proportionally to the pool balance 271 | previousWethBalance = currentWethBalance; 272 | let balanceChange = (Decimal(pAiAfterExitFee).div(previousPoolBalance)).mul(previousWethBalance); 273 | currentWethBalance = currentWethBalance.sub(balanceChange); 274 | previousDaiBalance = currentDaiBalance; 275 | balanceChange = (Decimal(pAiAfterExitFee).div(previousPoolBalance)).mul(previousDaiBalance); 276 | currentDaiBalance = currentDaiBalance.sub(balanceChange); 277 | 278 | // Print current balances after operation 279 | await logAndAssertCurrentBalances(); 280 | }); 281 | 282 | 283 | it('joinswapExternAmountIn', async () => { 284 | // Call function 285 | const poolRatio = 1.1; 286 | // increase tbalance by 1.1^2 after swap fee 287 | const tAi = (1 / (1 - swapFee * (1 - wethNorm))) * (currentWethBalance * (poolRatio ** (1 / wethNorm) - 1)); 288 | 289 | const pAo = await pool.joinswapExternAmountIn.call(WETH, toWei(String(tAi)), toWei('0')); 290 | // Execute txn called above 291 | await pool.joinswapExternAmountIn(WETH, toWei(String(tAi)), toWei('0')); 292 | 293 | // Update balance states 294 | previousWethBalance = currentWethBalance; 295 | currentWethBalance = currentWethBalance.plus(Decimal(tAi)); 296 | previousPoolBalance = currentPoolBalance; 297 | currentPoolBalance = currentPoolBalance.mul(Decimal(poolRatio)); // increase by 1.1 298 | 299 | // Check pAo 300 | const expected = (currentPoolBalance.sub(previousPoolBalance)); // poolRatio = 1.1 301 | const actual = fromWei(pAo); 302 | const relDif = calcRelativeDiff(expected, actual); 303 | 304 | if (verbose) { 305 | console.log('pAo'); 306 | console.log(`expected: ${expected})`); 307 | console.log(`actual : ${actual})`); 308 | console.log(`relDif : ${relDif})`); 309 | } 310 | assert.isAtMost(relDif.toNumber(), errorDelta); 311 | 312 | // Print current balances after operation 313 | await logAndAssertCurrentBalances(); 314 | }); 315 | 316 | 317 | it('joinswapPoolAmountOut', async () => { 318 | // Call function 319 | const poolRatio = 1.1; 320 | const pAo = currentPoolBalance * (poolRatio - 1); 321 | 322 | const tAi = await pool.joinswapPoolAmountOut.call(DAI, toWei(String(pAo)), MAX); // 10% of current supply 323 | await pool.joinswapPoolAmountOut(DAI, toWei(String(pAo)), MAX); 324 | 325 | // Update balance states 326 | previousPoolBalance = currentPoolBalance; 327 | currentPoolBalance = currentPoolBalance.mul(Decimal(poolRatio)); // increase by 1.1 328 | previousDaiBalance = currentDaiBalance; 329 | // (21% + swap fees) addition to current Rock supply ; 330 | const numer = (previousDaiBalance * ((poolRatio ** (1 / daiNorm) - 1) * 1)); 331 | const denom = (1 - swapFee * (1 - daiNorm)); 332 | currentDaiBalance = currentDaiBalance.plus(Decimal(numer / denom)); 333 | 334 | // Check tAi 335 | const expected = (currentDaiBalance.sub(previousDaiBalance)); // 0.4641 -> 1.1^4 - 1 = 0.4641 336 | const actual = fromWei(tAi); 337 | const relDif = calcRelativeDiff(expected, actual); 338 | 339 | if (verbose) { 340 | console.log('tAi'); 341 | console.log(`expected: ${expected})`); 342 | console.log(`actual : ${actual})`); 343 | console.log(`relDif : ${relDif})`); 344 | } 345 | assert.isAtMost(relDif.toNumber(), errorDelta); 346 | 347 | // Print current balances after operation 348 | await logAndAssertCurrentBalances(); 349 | }); 350 | 351 | 352 | it('exitswapPoolAmountIn', async () => { 353 | // Call function 354 | const poolRatioAfterExitFee = 0.9; 355 | const pAi = currentPoolBalance * (1 - poolRatioAfterExitFee) * (1 / (1 - exitFee)); 356 | 357 | const tAo = await pool.exitswapPoolAmountIn.call(WETH, toWei(String(pAi)), toWei('0')); 358 | await pool.exitswapPoolAmountIn(WETH, toWei(String(pAi)), toWei('0')); 359 | 360 | // Update balance states 361 | previousPoolBalance = currentPoolBalance; 362 | currentPoolBalance = currentPoolBalance.sub(Decimal(pAi).mul(Decimal(1).sub(Decimal(exitFee)))); 363 | previousWethBalance = currentWethBalance; 364 | const mult = (1 - poolRatioAfterExitFee ** (1 / wethNorm)) * (1 - swapFee * (1 - wethNorm)); 365 | currentWethBalance = currentWethBalance.sub(previousWethBalance.mul(Decimal(mult))); 366 | 367 | // Check tAo 368 | const expected = (previousWethBalance.sub(currentWethBalance)); // 0.4641 -> 1.1^4 - 1 = 0.4641 369 | const actual = fromWei(tAo); 370 | const relDif = calcRelativeDiff(expected, actual); 371 | 372 | if (verbose) { 373 | console.log('tAo'); 374 | console.log(`expected: ${expected})`); 375 | console.log(`actual : ${actual})`); 376 | console.log(`relDif : ${relDif})`); 377 | } 378 | 379 | assert.isAtMost(relDif.toNumber(), errorDelta); 380 | 381 | // Print current balances after operation 382 | await logAndAssertCurrentBalances(); 383 | }); 384 | 385 | 386 | it('exitswapExternAmountOut', async () => { 387 | // Call function 388 | const poolRatioAfterExitFee = 0.9; 389 | const tokenRatioBeforeSwapFee = poolRatioAfterExitFee ** (1 / daiNorm); 390 | const tAo = currentDaiBalance * (1 - tokenRatioBeforeSwapFee) * (1 - swapFee * (1 - daiNorm)); 391 | 392 | const pAi = await pool.exitswapExternAmountOut.call(DAI, toWei(String(tAo)), MAX); 393 | await pool.exitswapExternAmountOut(DAI, toWei(String(tAo)), MAX); 394 | 395 | // Update balance states 396 | previousDaiBalance = currentDaiBalance; 397 | currentDaiBalance = currentDaiBalance.sub(Decimal(tAo)); 398 | previousPoolBalance = currentPoolBalance; 399 | const balanceChange = previousPoolBalance.mul(Decimal(1).sub(Decimal(poolRatioAfterExitFee))); 400 | currentPoolBalance = currentPoolBalance.sub(balanceChange); 401 | 402 | // check pAi 403 | // Notice the (1-exitFee) term since only pAi*(1-exitFee) is burned 404 | const expected = (previousPoolBalance.sub(currentPoolBalance)).div(Decimal(1).sub(Decimal(exitFee))); 405 | const actual = fromWei(pAi); 406 | const relDif = calcRelativeDiff(expected, actual); 407 | 408 | if (verbose) { 409 | console.log('pAi'); 410 | console.log(`expected: ${expected})`); 411 | console.log(`actual : ${actual})`); 412 | console.log(`relDif : ${relDif})`); 413 | } 414 | 415 | assert.isAtMost(relDif.toNumber(), errorDelta); 416 | 417 | // Print current balances after operation 418 | await logAndAssertCurrentBalances(); 419 | }); 420 | 421 | 422 | it('pAo = joinswapExternAmountIn(joinswapPoolAmountOut(pAo))', async () => { 423 | const pAo = 10; 424 | const tAi = await pool.joinswapPoolAmountOut.call(WETH, toWei(String(pAo)), MAX); 425 | const calculatedPAo = await pool.joinswapExternAmountIn.call(WETH, String(tAi), toWei('0')); 426 | 427 | const expected = Decimal(pAo); 428 | const actual = fromWei(calculatedPAo); 429 | const relDif = calcRelativeDiff(expected, actual); 430 | 431 | if (verbose) { 432 | console.log(`tAi: ${tAi})`); 433 | console.log('pAo'); 434 | console.log(`expected: ${expected})`); 435 | console.log(`actual : ${actual})`); 436 | console.log(`relDif : ${relDif})`); 437 | } 438 | 439 | assert.isAtMost(relDif.toNumber(), errorDelta); 440 | }); 441 | 442 | 443 | it('tAi = joinswapPoolAmountOut(joinswapExternAmountIn(tAi))', async () => { 444 | const tAi = 1; 445 | const pAo = await pool.joinswapExternAmountIn.call(DAI, toWei(String(tAi)), toWei('0')); 446 | const calculatedtAi = await pool.joinswapPoolAmountOut.call(DAI, String(pAo), MAX); 447 | 448 | const expected = Decimal(tAi); 449 | const actual = fromWei(calculatedtAi); 450 | const relDif = calcRelativeDiff(expected, actual); 451 | 452 | if (verbose) { 453 | console.log(`pAo: ${pAo})`); 454 | console.log('tAi'); 455 | console.log(`expected: ${expected})`); 456 | console.log(`actual : ${actual})`); 457 | console.log(`relDif : ${relDif})`); 458 | } 459 | 460 | assert.isAtMost(relDif.toNumber(), errorDelta); 461 | }); 462 | 463 | 464 | it('pAi = exitswapExternAmountOut(exitswapPoolAmountIn(pAi))', async () => { 465 | const pAi = 10; 466 | const tAo = await pool.exitswapPoolAmountIn.call(WETH, toWei(String(pAi)), toWei('0')); 467 | const calculatedPAi = await pool.exitswapExternAmountOut.call(WETH, String(tAo), MAX); 468 | 469 | const expected = Decimal(pAi); 470 | const actual = fromWei(calculatedPAi); 471 | const relDif = calcRelativeDiff(expected, actual); 472 | 473 | if (verbose) { 474 | console.log(`tAo: ${tAo})`); 475 | console.log('pAi'); 476 | console.log(`expected: ${expected})`); 477 | console.log(`actual : ${actual})`); 478 | console.log(`relDif : ${relDif})`); 479 | } 480 | 481 | assert.isAtMost(relDif.toNumber(), errorDelta); 482 | }); 483 | 484 | 485 | it('tAo = exitswapPoolAmountIn(exitswapExternAmountOut(tAo))', async () => { 486 | const tAo = '1'; 487 | const pAi = await pool.exitswapExternAmountOut.call(DAI, toWei(tAo), MAX); 488 | const calculatedtAo = await pool.exitswapPoolAmountIn.call(DAI, String(pAi), toWei('0')); 489 | 490 | const expected = Decimal(tAo); 491 | const actual = fromWei(calculatedtAo); 492 | const relDif = calcRelativeDiff(expected, actual); 493 | 494 | if (verbose) { 495 | console.log(`pAi: ${pAi})`); 496 | console.log('tAo'); 497 | console.log(`expected: ${expected})`); 498 | console.log(`actual : ${actual})`); 499 | console.log(`relDif : ${relDif})`); 500 | } 501 | 502 | assert.isAtMost(relDif.toNumber(), errorDelta); 503 | }); 504 | }); 505 | }); 506 | -------------------------------------------------------------------------------- /test/num.js: -------------------------------------------------------------------------------- 1 | const truffleAssert = require('truffle-assertions'); 2 | 3 | const TMath = artifacts.require('TMath'); 4 | 5 | contract('TMath', async () => { 6 | const MAX = web3.utils.toTwosComplement(-1); 7 | 8 | describe('BMath', () => { 9 | let tmath; 10 | before(async () => { 11 | tmath = await TMath.deployed(); 12 | }); 13 | 14 | it('badd throws on overflow', async () => { 15 | await truffleAssert.reverts(tmath.calc_badd(1, MAX), 'ERR_ADD_OVERFLOW'); 16 | }); 17 | 18 | it('bsub throws on underflow', async () => { 19 | await truffleAssert.reverts(tmath.calc_bsub(1, 2), 'ERR_SUB_UNDERFLOW'); 20 | }); 21 | 22 | it('bmul throws on overflow', async () => { 23 | await truffleAssert.reverts(tmath.calc_bmul(2, MAX), 'ERR_MUL_OVERFLOW'); 24 | }); 25 | 26 | it('bdiv throws on div by 0', async () => { 27 | await truffleAssert.reverts(tmath.calc_bdiv(1, 0), 'ERR_DIV_ZERO'); 28 | }); 29 | 30 | it('bpow throws on base outside range', async () => { 31 | await truffleAssert.reverts(tmath.calc_bpow(0, 2), 'ERR_BPOW_BASE_TOO_LOW'); 32 | await truffleAssert.reverts(tmath.calc_bpow(MAX, 2), 'ERR_BPOW_BASE_TOO_HIGH'); 33 | }); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /test/pool.js: -------------------------------------------------------------------------------- 1 | const truffleAssert = require('truffle-assertions'); 2 | const { calcOutGivenIn, calcInGivenOut, calcRelativeDiff } = require('../lib/calc_comparisons'); 3 | 4 | const BPool = artifacts.require('BPool'); 5 | const BFactory = artifacts.require('BFactory'); 6 | const TToken = artifacts.require('TToken'); 7 | const verbose = process.env.VERBOSE; 8 | 9 | contract('BPool', async (accounts) => { 10 | const admin = accounts[0]; 11 | const user1 = accounts[1]; 12 | const user2 = accounts[2]; 13 | const { toWei } = web3.utils; 14 | const { fromWei } = web3.utils; 15 | const errorDelta = 10 ** -8; 16 | const MAX = web3.utils.toTwosComplement(-1); 17 | 18 | let WETH; let MKR; let DAI; let 19 | XXX; // addresses 20 | let weth; let mkr; let dai; let 21 | xxx; // TTokens 22 | let factory; // BPool factory 23 | let pool; // first pool w/ defaults 24 | let POOL; // pool address 25 | 26 | before(async () => { 27 | factory = await BFactory.deployed(); 28 | 29 | POOL = await factory.newBPool.call(); 30 | await factory.newBPool(); 31 | pool = await BPool.at(POOL); 32 | 33 | weth = await TToken.new('Wrapped Ether', 'WETH', 18); 34 | mkr = await TToken.new('Maker', 'MKR', 18); 35 | dai = await TToken.new('Dai Stablecoin', 'DAI', 18); 36 | xxx = await TToken.new('XXX', 'XXX', 18); 37 | 38 | WETH = weth.address; 39 | MKR = mkr.address; 40 | DAI = dai.address; 41 | XXX = xxx.address; 42 | 43 | /* 44 | Tests assume token prices 45 | WETH - $200 46 | MKR - $500 47 | DAI - $1 48 | XXX - $0 49 | */ 50 | 51 | // Admin balances 52 | await weth.mint(admin, toWei('50')); 53 | await mkr.mint(admin, toWei('20')); 54 | await dai.mint(admin, toWei('10000')); 55 | await xxx.mint(admin, toWei('10')); 56 | 57 | // User1 balances 58 | await weth.mint(user1, toWei('25'), { from: admin }); 59 | await mkr.mint(user1, toWei('4'), { from: admin }); 60 | await dai.mint(user1, toWei('40000'), { from: admin }); 61 | await xxx.mint(user1, toWei('10'), { from: admin }); 62 | 63 | // User2 balances 64 | await weth.mint(user2, toWei('12.2222'), { from: admin }); 65 | await mkr.mint(user2, toWei('1.015333'), { from: admin }); 66 | await dai.mint(user2, toWei('0'), { from: admin }); 67 | await xxx.mint(user2, toWei('51'), { from: admin }); 68 | }); 69 | 70 | describe('Binding Tokens', () => { 71 | it('Controller is msg.sender', async () => { 72 | const controller = await pool.getController(); 73 | assert.equal(controller, admin); 74 | }); 75 | 76 | it('Pool starts with no bound tokens', async () => { 77 | const numTokens = await pool.getNumTokens(); 78 | assert.equal(0, numTokens); 79 | const isBound = await pool.isBound.call(WETH); 80 | assert(!isBound); 81 | }); 82 | 83 | it('Fails binding tokens that are not approved', async () => { 84 | await truffleAssert.reverts( 85 | pool.bind(MKR, toWei('10'), toWei('2.5')), 86 | 'ERR_BTOKEN_BAD_CALLER', 87 | ); 88 | }); 89 | 90 | it('Admin approves tokens', async () => { 91 | await weth.approve(POOL, MAX); 92 | await mkr.approve(POOL, MAX); 93 | await dai.approve(POOL, MAX); 94 | await xxx.approve(POOL, MAX); 95 | }); 96 | 97 | it('Fails binding weights and balances outside MIX MAX', async () => { 98 | await truffleAssert.reverts( 99 | pool.bind(WETH, toWei('51'), toWei('1')), 100 | 'ERR_INSUFFICIENT_BAL', 101 | ); 102 | await truffleAssert.reverts( 103 | pool.bind(MKR, toWei('0.0000000000001'), toWei('1')), 104 | 'ERR_MIN_BALANCE', 105 | ); 106 | await truffleAssert.reverts( 107 | pool.bind(DAI, toWei('1000'), toWei('0.99')), 108 | 'ERR_MIN_WEIGHT', 109 | ); 110 | await truffleAssert.reverts( 111 | pool.bind(WETH, toWei('5'), toWei('50.01')), 112 | 'ERR_MAX_WEIGHT', 113 | ); 114 | }); 115 | 116 | it('Fails finalizing pool without 2 tokens', async () => { 117 | await truffleAssert.reverts( 118 | pool.finalize(), 119 | 'ERR_MIN_TOKENS', 120 | ); 121 | }); 122 | 123 | it('Admin binds tokens', async () => { 124 | // Equal weights WETH, MKR, DAI 125 | await pool.bind(WETH, toWei('50'), toWei('5')); 126 | await pool.bind(MKR, toWei('20'), toWei('5')); 127 | await pool.bind(DAI, toWei('10000'), toWei('5')); 128 | const numTokens = await pool.getNumTokens(); 129 | assert.equal(3, numTokens); 130 | const totalDernomWeight = await pool.getTotalDenormalizedWeight(); 131 | assert.equal(15, fromWei(totalDernomWeight)); 132 | const wethDenormWeight = await pool.getDenormalizedWeight(WETH); 133 | assert.equal(5, fromWei(wethDenormWeight)); 134 | const wethNormWeight = await pool.getNormalizedWeight(WETH); 135 | assert.equal(0.333333333333333333, fromWei(wethNormWeight)); 136 | const mkrBalance = await pool.getBalance(MKR); 137 | assert.equal(20, fromWei(mkrBalance)); 138 | }); 139 | 140 | it('Admin unbinds token', async () => { 141 | await pool.bind(XXX, toWei('10'), toWei('5')); 142 | let adminBalance = await xxx.balanceOf(admin); 143 | assert.equal(0, fromWei(adminBalance)); 144 | await pool.unbind(XXX); 145 | adminBalance = await xxx.balanceOf(admin); 146 | assert.equal(10, fromWei(adminBalance)); 147 | const numTokens = await pool.getNumTokens(); 148 | assert.equal(3, numTokens); 149 | const totalDernomWeight = await pool.getTotalDenormalizedWeight(); 150 | assert.equal(15, fromWei(totalDernomWeight)); 151 | }); 152 | 153 | it('Fails binding above MAX TOTAL WEIGHT', async () => { 154 | await truffleAssert.reverts( 155 | pool.bind(XXX, toWei('1'), toWei('40')), 156 | 'ERR_MAX_TOTAL_WEIGHT', 157 | ); 158 | }); 159 | 160 | it('Fails rebinding token or unbinding random token', async () => { 161 | await truffleAssert.reverts( 162 | pool.bind(WETH, toWei('0'), toWei('1')), 163 | 'ERR_IS_BOUND', 164 | ); 165 | await truffleAssert.reverts( 166 | pool.rebind(XXX, toWei('0'), toWei('1')), 167 | 'ERR_NOT_BOUND', 168 | ); 169 | await truffleAssert.reverts( 170 | pool.unbind(XXX), 171 | 'ERR_NOT_BOUND', 172 | ); 173 | }); 174 | 175 | it('Get current tokens', async () => { 176 | const currentTokens = await pool.getCurrentTokens(); 177 | assert.sameMembers(currentTokens, [WETH, MKR, DAI]); 178 | }); 179 | 180 | it('Fails getting final tokens before finalized', async () => { 181 | await truffleAssert.reverts( 182 | pool.getFinalTokens(), 183 | 'ERR_NOT_FINALIZED', 184 | ); 185 | }); 186 | }); 187 | 188 | 189 | describe('Finalizing pool', () => { 190 | it('Fails when other users interact before finalizing', async () => { 191 | await truffleAssert.reverts( 192 | pool.bind(WETH, toWei('5'), toWei('5'), { from: user1 }), 193 | 'ERR_NOT_CONTROLLER', 194 | ); 195 | await truffleAssert.reverts( 196 | pool.rebind(WETH, toWei('5'), toWei('5'), { from: user1 }), 197 | 'ERR_NOT_CONTROLLER', 198 | ); 199 | await truffleAssert.reverts( 200 | pool.joinPool(toWei('1'), [MAX, MAX], { from: user1 }), 201 | 'ERR_NOT_FINALIZED', 202 | ); 203 | await truffleAssert.reverts( 204 | pool.exitPool(toWei('1'), [toWei('0'), toWei('0')], { from: user1 }), 205 | 'ERR_NOT_FINALIZED', 206 | ); 207 | await truffleAssert.reverts( 208 | pool.unbind(DAI, { from: user1 }), 209 | 'ERR_NOT_CONTROLLER', 210 | ); 211 | }); 212 | 213 | it('Fails calling any swap before finalizing', async () => { 214 | await truffleAssert.reverts( 215 | pool.swapExactAmountIn(WETH, toWei('2.5'), DAI, toWei('475'), toWei('200')), 216 | 'ERR_SWAP_NOT_PUBLIC', 217 | ); 218 | await truffleAssert.reverts( 219 | pool.swapExactAmountIn(DAI, toWei('2.5'), WETH, toWei('475'), toWei('200')), 220 | 'ERR_SWAP_NOT_PUBLIC', 221 | ); 222 | await truffleAssert.reverts( 223 | pool.swapExactAmountOut(WETH, toWei('2.5'), DAI, toWei('475'), toWei('200')), 224 | 'ERR_SWAP_NOT_PUBLIC', 225 | ); 226 | await truffleAssert.reverts( 227 | pool.swapExactAmountOut(DAI, toWei('2.5'), WETH, toWei('475'), toWei('200')), 228 | 'ERR_SWAP_NOT_PUBLIC', 229 | ); 230 | }); 231 | 232 | it('Fails calling any join exit swap before finalizing', async () => { 233 | await truffleAssert.reverts( 234 | pool.joinswapExternAmountIn(WETH, toWei('2.5'), toWei('0')), 235 | 'ERR_NOT_FINALIZED', 236 | ); 237 | await truffleAssert.reverts( 238 | pool.joinswapPoolAmountOut(WETH, toWei('2.5'), MAX), 239 | 'ERR_NOT_FINALIZED', 240 | ); 241 | await truffleAssert.reverts( 242 | pool.exitswapPoolAmountIn(WETH, toWei('2.5'), toWei('0')), 243 | 'ERR_NOT_FINALIZED', 244 | ); 245 | await truffleAssert.reverts( 246 | pool.exitswapExternAmountOut(WETH, toWei('2.5'), MAX), 247 | 'ERR_NOT_FINALIZED', 248 | ); 249 | }); 250 | 251 | it('Only controller can setPublicSwap', async () => { 252 | await pool.setPublicSwap(true); 253 | const publicSwap = pool.isPublicSwap(); 254 | assert(publicSwap); 255 | await truffleAssert.reverts(pool.setPublicSwap(true, { from: user1 }), 'ERR_NOT_CONTROLLER'); 256 | }); 257 | 258 | it('Fails setting low swap fees', async () => { 259 | await truffleAssert.reverts( 260 | pool.setSwapFee(toWei('0.0000001')), 261 | 'ERR_MIN_FEE', 262 | ); 263 | }); 264 | 265 | it('Fails setting high swap fees', async () => { 266 | await truffleAssert.reverts( 267 | pool.setSwapFee(toWei('0.11')), 268 | 'ERR_MAX_FEE', 269 | ); 270 | }); 271 | 272 | it('Fails nonadmin sets fees or controller', async () => { 273 | await truffleAssert.reverts( 274 | pool.setSwapFee(toWei('0.003'), { from: user1 }), 275 | 'ERR_NOT_CONTROLLER', 276 | ); 277 | await truffleAssert.reverts( 278 | pool.setController(user1, { from: user1 }), 279 | 'ERR_NOT_CONTROLLER', 280 | ); 281 | }); 282 | 283 | it('Admin sets swap fees', async () => { 284 | await pool.setSwapFee(toWei('0.003')); 285 | const swapFee = await pool.getSwapFee(); 286 | assert.equal(0.003, fromWei(swapFee)); 287 | }); 288 | 289 | it('Fails nonadmin finalizes pool', async () => { 290 | await truffleAssert.reverts( 291 | pool.finalize({ from: user1 }), 292 | 'ERR_NOT_CONTROLLER', 293 | ); 294 | }); 295 | 296 | it('Admin finalizes pool', async () => { 297 | const tx = await pool.finalize(); 298 | const adminBal = await pool.balanceOf(admin); 299 | assert.equal(100, fromWei(adminBal)); 300 | truffleAssert.eventEmitted(tx, 'Transfer', (event) => event.dst === admin); 301 | const finalized = pool.isFinalized(); 302 | assert(finalized); 303 | }); 304 | 305 | it('Fails finalizing pool after finalized', async () => { 306 | await truffleAssert.reverts( 307 | pool.finalize(), 308 | 'ERR_IS_FINALIZED', 309 | ); 310 | }); 311 | 312 | it('Cant setPublicSwap, setSwapFee when finalized', async () => { 313 | await truffleAssert.reverts(pool.setPublicSwap(false), 'ERR_IS_FINALIZED'); 314 | await truffleAssert.reverts(pool.setSwapFee(toWei('0.01')), 'ERR_IS_FINALIZED'); 315 | }); 316 | 317 | it('Fails binding new token after finalized', async () => { 318 | await truffleAssert.reverts( 319 | pool.bind(XXX, toWei('10'), toWei('5')), 320 | 'ERR_IS_FINALIZED', 321 | ); 322 | await truffleAssert.reverts( 323 | pool.rebind(DAI, toWei('10'), toWei('5')), 324 | 'ERR_IS_FINALIZED', 325 | ); 326 | }); 327 | 328 | it('Fails unbinding after finalized', async () => { 329 | await truffleAssert.reverts( 330 | pool.unbind(WETH), 331 | 'ERR_IS_FINALIZED', 332 | ); 333 | }); 334 | 335 | it('Get final tokens', async () => { 336 | const finalTokens = await pool.getFinalTokens(); 337 | assert.sameMembers(finalTokens, [WETH, MKR, DAI]); 338 | }); 339 | }); 340 | 341 | describe('User interactions', () => { 342 | it('Other users approve tokens', async () => { 343 | await weth.approve(POOL, MAX, { from: user1 }); 344 | await mkr.approve(POOL, MAX, { from: user1 }); 345 | await dai.approve(POOL, MAX, { from: user1 }); 346 | await xxx.approve(POOL, MAX, { from: user1 }); 347 | 348 | await weth.approve(POOL, MAX, { from: user2 }); 349 | await mkr.approve(POOL, MAX, { from: user2 }); 350 | await dai.approve(POOL, MAX, { from: user2 }); 351 | await xxx.approve(POOL, MAX, { from: user2 }); 352 | }); 353 | 354 | it('User1 joins pool', async () => { 355 | await pool.joinPool(toWei('5'), [MAX, MAX, MAX], { from: user1 }); 356 | const daiBalance = await pool.getBalance(DAI); 357 | assert.equal(10500, fromWei(daiBalance)); 358 | const userWethBalance = await weth.balanceOf(user1); 359 | assert.equal(22.5, fromWei(userWethBalance)); 360 | }); 361 | 362 | /* 363 | Current pool balances 364 | WETH - 52.5 365 | MKR - 21 366 | DAI - 10,500 367 | XXX - 0 368 | */ 369 | 370 | it('Fails admin unbinding token after finalized and others joined', async () => { 371 | await truffleAssert.reverts(pool.unbind(DAI), 'ERR_IS_FINALIZED'); 372 | }); 373 | 374 | it('getSpotPriceSansFee and getSpotPrice', async () => { 375 | const wethPrice = await pool.getSpotPriceSansFee(DAI, WETH); 376 | assert.equal(200, fromWei(wethPrice)); 377 | 378 | const wethPriceFee = await pool.getSpotPrice(DAI, WETH); 379 | const wethPriceFeeCheck = ((10500 / 5) / (52.5 / 5)) * (1 / (1 - 0.003)); 380 | // 200.6018054162487462 381 | assert.equal(fromWei(wethPriceFee), wethPriceFeeCheck); 382 | }); 383 | 384 | it('Fail swapExactAmountIn unbound or over min max ratios', async () => { 385 | await truffleAssert.reverts( 386 | pool.swapExactAmountIn(WETH, toWei('2.5'), XXX, toWei('100'), toWei('200'), { from: user2 }), 387 | 'ERR_NOT_BOUND', 388 | ); 389 | await truffleAssert.reverts( 390 | pool.swapExactAmountIn(WETH, toWei('26.5'), DAI, toWei('5000'), toWei('200'), { from: user2 }), 391 | 'ERR_MAX_IN_RATIO', 392 | ); 393 | }); 394 | 395 | it('swapExactAmountIn', async () => { 396 | // 2.5 WETH -> DAI 397 | const expected = calcOutGivenIn(52.5, 5, 10500, 5, 2.5, 0.003); 398 | const txr = await pool.swapExactAmountIn( 399 | WETH, 400 | toWei('2.5'), 401 | DAI, 402 | toWei('475'), 403 | toWei('200'), 404 | { from: user2 }, 405 | ); 406 | const log = txr.logs[0]; 407 | assert.equal(log.event, 'LOG_SWAP'); 408 | // 475.905805337091423 409 | 410 | const actual = fromWei(log.args[4]); 411 | const relDif = calcRelativeDiff(expected, actual); 412 | if (verbose) { 413 | console.log('swapExactAmountIn'); 414 | console.log(`expected: ${expected})`); 415 | console.log(`actual : ${actual})`); 416 | console.log(`relDif : ${relDif})`); 417 | } 418 | 419 | assert.isAtMost(relDif.toNumber(), errorDelta); 420 | 421 | const userDaiBalance = await dai.balanceOf(user2); 422 | assert.equal(fromWei(userDaiBalance), Number(fromWei(log.args[4]))); 423 | 424 | // 182.804672101083406128 425 | const wethPrice = await pool.getSpotPrice(DAI, WETH); 426 | const wethPriceFeeCheck = ((10024.094194662908577 / 5) / (55 / 5)) * (1 / (1 - 0.003)); 427 | assert.approximately(Number(fromWei(wethPrice)), Number(wethPriceFeeCheck), errorDelta); 428 | 429 | const daiNormWeight = await pool.getNormalizedWeight(DAI); 430 | assert.equal(0.333333333333333333, fromWei(daiNormWeight)); 431 | }); 432 | 433 | it('swapExactAmountOut', async () => { 434 | // ETH -> 1 MKR 435 | // const amountIn = (55 * (((21 / (21 - 1)) ** (5 / 5)) - 1)) / (1 - 0.003); 436 | const expected = calcInGivenOut(55, 5, 21, 5, 1, 0.003); 437 | const txr = await pool.swapExactAmountOut( 438 | WETH, 439 | toWei('3'), 440 | MKR, 441 | toWei('1.0'), 442 | toWei('500'), 443 | { from: user2 }, 444 | ); 445 | const log = txr.logs[0]; 446 | assert.equal(log.event, 'LOG_SWAP'); 447 | // 2.758274824473420261 448 | 449 | const actual = fromWei(log.args[3]); 450 | const relDif = calcRelativeDiff(expected, actual); 451 | if (verbose) { 452 | console.log('swapExactAmountOut'); 453 | console.log(`expected: ${expected})`); 454 | console.log(`actual : ${actual})`); 455 | console.log(`relDif : ${relDif})`); 456 | } 457 | 458 | assert.isAtMost(relDif.toNumber(), errorDelta); 459 | }); 460 | 461 | it('Fails joins exits with limits', async () => { 462 | await truffleAssert.reverts( 463 | pool.joinPool(toWei('10'), [toWei('1'), toWei('1'), toWei('1')]), 464 | 'ERR_LIMIT_IN', 465 | ); 466 | 467 | await truffleAssert.reverts( 468 | pool.exitPool(toWei('10'), [toWei('10'), toWei('10'), toWei('10')]), 469 | 'ERR_LIMIT_OUT', 470 | ); 471 | 472 | await truffleAssert.reverts( 473 | pool.joinswapExternAmountIn(DAI, toWei('100'), toWei('10')), 474 | 'ERR_LIMIT_OUT', 475 | ); 476 | 477 | await truffleAssert.reverts( 478 | pool.joinswapPoolAmountOut(DAI, toWei('10'), toWei('100')), 479 | 'ERR_LIMIT_IN', 480 | ); 481 | 482 | await truffleAssert.reverts( 483 | pool.exitswapPoolAmountIn(DAI, toWei('1'), toWei('1000')), 484 | 'ERR_LIMIT_OUT', 485 | ); 486 | 487 | await truffleAssert.reverts( 488 | pool.exitswapExternAmountOut(DAI, toWei('1000'), toWei('1')), 489 | 'ERR_LIMIT_IN', 490 | ); 491 | }); 492 | 493 | it('Fails calling any swap on unbound token', async () => { 494 | await truffleAssert.reverts( 495 | pool.swapExactAmountIn(XXX, toWei('2.5'), DAI, toWei('475'), toWei('200')), 496 | 'ERR_NOT_BOUND', 497 | ); 498 | await truffleAssert.reverts( 499 | pool.swapExactAmountIn(DAI, toWei('2.5'), XXX, toWei('475'), toWei('200')), 500 | 'ERR_NOT_BOUND', 501 | ); 502 | await truffleAssert.reverts( 503 | pool.swapExactAmountOut(XXX, toWei('2.5'), DAI, toWei('475'), toWei('200')), 504 | 'ERR_NOT_BOUND', 505 | ); 506 | await truffleAssert.reverts( 507 | pool.swapExactAmountOut(DAI, toWei('2.5'), XXX, toWei('475'), toWei('200')), 508 | 'ERR_NOT_BOUND', 509 | ); 510 | await truffleAssert.reverts( 511 | pool.joinswapExternAmountIn(XXX, toWei('2.5'), toWei('0')), 512 | 'ERR_NOT_BOUND', 513 | ); 514 | await truffleAssert.reverts( 515 | pool.joinswapPoolAmountOut(XXX, toWei('2.5'), MAX), 516 | 'ERR_NOT_BOUND', 517 | ); 518 | await truffleAssert.reverts( 519 | pool.exitswapPoolAmountIn(XXX, toWei('2.5'), toWei('0')), 520 | 'ERR_NOT_BOUND', 521 | ); 522 | await truffleAssert.reverts( 523 | pool.exitswapExternAmountOut(XXX, toWei('2.5'), MAX), 524 | 'ERR_NOT_BOUND', 525 | ); 526 | }); 527 | 528 | it('Fails calling weights, balances, spot prices on unbound token', async () => { 529 | await truffleAssert.reverts( 530 | pool.getDenormalizedWeight(XXX), 531 | 'ERR_NOT_BOUND', 532 | ); 533 | await truffleAssert.reverts( 534 | pool.getNormalizedWeight(XXX), 535 | 'ERR_NOT_BOUND', 536 | ); 537 | await truffleAssert.reverts( 538 | pool.getBalance(XXX), 539 | 'ERR_NOT_BOUND', 540 | ); 541 | await truffleAssert.reverts( 542 | pool.getSpotPrice(DAI, XXX), 543 | 'ERR_NOT_BOUND', 544 | ); 545 | await truffleAssert.reverts( 546 | pool.getSpotPrice(XXX, DAI), 547 | 'ERR_NOT_BOUND', 548 | ); 549 | await truffleAssert.reverts( 550 | pool.getSpotPriceSansFee(DAI, XXX), 551 | 'ERR_NOT_BOUND', 552 | ); 553 | await truffleAssert.reverts( 554 | pool.getSpotPriceSansFee(XXX, DAI), 555 | 'ERR_NOT_BOUND', 556 | ); 557 | }); 558 | }); 559 | 560 | describe('BToken interactions', () => { 561 | it('Token descriptors', async () => { 562 | const name = await pool.name(); 563 | assert.equal(name, 'Balancer Pool Token'); 564 | 565 | const symbol = await pool.symbol(); 566 | assert.equal(symbol, 'BPT'); 567 | 568 | const decimals = await pool.decimals(); 569 | assert.equal(decimals, 18); 570 | }); 571 | 572 | it('Token allowances', async () => { 573 | await pool.approve(user1, toWei('50')); 574 | let allowance = await pool.allowance(admin, user1); 575 | assert.equal(fromWei(allowance), 50); 576 | 577 | await pool.increaseApproval(user1, toWei('50')); 578 | allowance = await pool.allowance(admin, user1); 579 | assert.equal(fromWei(allowance), 100); 580 | 581 | await pool.decreaseApproval(user1, toWei('50')); 582 | allowance = await pool.allowance(admin, user1); 583 | assert.equal(fromWei(allowance), 50); 584 | 585 | await pool.decreaseApproval(user1, toWei('100')); 586 | allowance = await pool.allowance(admin, user1); 587 | assert.equal(fromWei(allowance), 0); 588 | }); 589 | 590 | it('Token transfers', async () => { 591 | await truffleAssert.reverts( 592 | pool.transferFrom(user2, admin, toWei('10')), 593 | 'ERR_BTOKEN_BAD_CALLER', 594 | ); 595 | 596 | await pool.transferFrom(admin, user2, toWei('1')); 597 | await pool.approve(user2, toWei('10')); 598 | await pool.transferFrom(admin, user2, toWei('1'), { from: user2 }); 599 | }); 600 | }); 601 | }); 602 | -------------------------------------------------------------------------------- /test/pool_max_tokens.js: -------------------------------------------------------------------------------- 1 | const truffleAssert = require('truffle-assertions'); 2 | 3 | const BPool = artifacts.require('BPool'); 4 | const BFactory = artifacts.require('BFactory'); 5 | const TToken = artifacts.require('TToken'); 6 | 7 | contract('BPool', async (accounts) => { 8 | const admin = accounts[0]; 9 | 10 | const { toWei } = web3.utils; 11 | const { fromWei } = web3.utils; 12 | 13 | const MAX = web3.utils.toTwosComplement(-1); 14 | 15 | let AAA; let BBB; let CCC; let DDD; let EEE; let FFF; let GGG; let HHH; let 16 | ZZZ; // addresses 17 | let aaa; let bbb; let ccc; let ddd; let eee; let fff; let ggg; let hhh; let 18 | zzz; // TTokens 19 | let factory; // BPool factory 20 | let FACTORY; // factory address 21 | let pool; // first pool w/ defaults 22 | let POOL; // pool address 23 | 24 | before(async () => { 25 | factory = await BFactory.deployed(); 26 | FACTORY = factory.address; 27 | 28 | POOL = await factory.newBPool.call(); 29 | await factory.newBPool(); 30 | pool = await BPool.at(POOL); 31 | 32 | aaa = await TToken.new('AAA', 'AAA', 18); 33 | bbb = await TToken.new('BBB', 'BBB', 18); 34 | ccc = await TToken.new('CCC', 'CCC', 18); 35 | ddd = await TToken.new('DDD', 'EEE', 18); 36 | eee = await TToken.new('EEE', 'EEE', 18); 37 | fff = await TToken.new('FFF', 'FFF', 18); 38 | ggg = await TToken.new('GGG', 'GGG', 18); 39 | hhh = await TToken.new('HHH', 'HHH', 18); 40 | zzz = await TToken.new('ZZZ', 'ZZZ', 18); 41 | 42 | AAA = aaa.address; 43 | BBB = bbb.address; 44 | CCC = ccc.address; 45 | DDD = ddd.address; 46 | EEE = eee.address; 47 | FFF = fff.address; 48 | GGG = ggg.address; 49 | HHH = hhh.address; 50 | ZZZ = zzz.address; 51 | 52 | // Admin balances 53 | await aaa.mint(admin, toWei('100')); 54 | await bbb.mint(admin, toWei('100')); 55 | await ccc.mint(admin, toWei('100')); 56 | await ddd.mint(admin, toWei('100')); 57 | await eee.mint(admin, toWei('100')); 58 | await fff.mint(admin, toWei('100')); 59 | await ggg.mint(admin, toWei('100')); 60 | await hhh.mint(admin, toWei('100')); 61 | await zzz.mint(admin, toWei('100')); 62 | }); 63 | 64 | describe('Binding Tokens', () => { 65 | it('Admin approves tokens', async () => { 66 | await aaa.approve(POOL, MAX); 67 | await bbb.approve(POOL, MAX); 68 | await ccc.approve(POOL, MAX); 69 | await ddd.approve(POOL, MAX); 70 | await eee.approve(POOL, MAX); 71 | await fff.approve(POOL, MAX); 72 | await ggg.approve(POOL, MAX); 73 | await hhh.approve(POOL, MAX); 74 | await zzz.approve(POOL, MAX); 75 | }); 76 | 77 | it('Admin binds tokens', async () => { 78 | await pool.bind(AAA, toWei('50'), toWei('1')); 79 | await pool.bind(BBB, toWei('50'), toWei('3')); 80 | await pool.bind(CCC, toWei('50'), toWei('2.5')); 81 | await pool.bind(DDD, toWei('50'), toWei('7')); 82 | await pool.bind(EEE, toWei('50'), toWei('10')); 83 | await pool.bind(FFF, toWei('50'), toWei('1.99')); 84 | await pool.bind(GGG, toWei('40'), toWei('6')); 85 | await pool.bind(HHH, toWei('70'), toWei('2.3')); 86 | 87 | const totalDernomWeight = await pool.getTotalDenormalizedWeight(); 88 | assert.equal(33.79, fromWei(totalDernomWeight)); 89 | }); 90 | 91 | it('Fails binding more than 8 tokens', async () => { 92 | await truffleAssert.reverts(pool.bind(ZZZ, toWei('50'), toWei('2')), 'ERR_MAX_TOKENS'); 93 | }); 94 | 95 | it('Rebind token at a smaller balance', async () => { 96 | await pool.rebind(HHH, toWei('50'), toWei('2.1')); 97 | const balance = await pool.getBalance(HHH); 98 | assert.equal(fromWei(balance), 50); 99 | 100 | const adminBalance = await hhh.balanceOf(admin); 101 | assert.equal(fromWei(adminBalance), 50); 102 | 103 | const factoryBalance = await hhh.balanceOf(FACTORY); 104 | assert.equal(fromWei(factoryBalance), 0); 105 | 106 | const totalDernomWeight = await pool.getTotalDenormalizedWeight(); 107 | assert.equal(33.59, fromWei(totalDernomWeight)); 108 | }); 109 | 110 | it('Fails gulp on unbound token', async () => { 111 | await truffleAssert.reverts(pool.gulp(ZZZ), 'ERR_NOT_BOUND'); 112 | }); 113 | 114 | it('Pool can gulp tokens', async () => { 115 | await ggg.transferFrom(admin, POOL, toWei('10')); 116 | 117 | await pool.gulp(GGG); 118 | const balance = await pool.getBalance(GGG); 119 | assert.equal(fromWei(balance), 50); 120 | }); 121 | 122 | it('Fails swapExactAmountIn with limits', async () => { 123 | await pool.setPublicSwap(true); 124 | await truffleAssert.reverts( 125 | pool.swapExactAmountIn( 126 | AAA, 127 | toWei('1'), 128 | BBB, 129 | toWei('0'), 130 | toWei('0.9'), 131 | ), 132 | 'ERR_BAD_LIMIT_PRICE', 133 | ); 134 | await truffleAssert.reverts( 135 | pool.swapExactAmountIn( 136 | AAA, 137 | toWei('1'), 138 | BBB, 139 | toWei('2'), 140 | toWei('3.5'), 141 | ), 142 | 'ERR_LIMIT_OUT', 143 | ); 144 | await truffleAssert.reverts( 145 | pool.swapExactAmountIn( 146 | AAA, 147 | toWei('1'), 148 | BBB, 149 | toWei('0'), 150 | toWei('3.00001'), 151 | ), 152 | 'ERR_LIMIT_PRICE', 153 | ); 154 | }); 155 | 156 | it('Fails swapExactAmountOut with limits', async () => { 157 | await truffleAssert.reverts( 158 | pool.swapExactAmountOut( 159 | AAA, 160 | toWei('51'), 161 | BBB, 162 | toWei('40'), 163 | toWei('5'), 164 | ), 165 | 'ERR_MAX_OUT_RATIO', 166 | ); 167 | await truffleAssert.reverts( 168 | pool.swapExactAmountOut( 169 | AAA, 170 | toWei('5'), 171 | BBB, 172 | toWei('1'), 173 | toWei('1'), 174 | ), 175 | 'ERR_BAD_LIMIT_PRICE', 176 | ); 177 | await truffleAssert.reverts( 178 | pool.swapExactAmountOut( 179 | AAA, 180 | toWei('1'), 181 | BBB, 182 | toWei('1'), 183 | toWei('5'), 184 | ), 185 | 'ERR_LIMIT_IN', 186 | ); 187 | await truffleAssert.reverts( 188 | pool.swapExactAmountOut( 189 | AAA, 190 | toWei('5'), 191 | BBB, 192 | toWei('1'), 193 | toWei('3.00001'), 194 | ), 195 | 'ERR_LIMIT_PRICE', 196 | ); 197 | }); 198 | }); 199 | }); 200 | -------------------------------------------------------------------------------- /truffle-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | networks: { 3 | development: { 4 | host: 'localhost', // Localhost (default: none) 5 | port: 8545, // Standard Ethereum port (default: none) 6 | network_id: '*', // Any network (default: none) 7 | gas: 10000000, 8 | }, 9 | coverage: { 10 | host: 'localhost', 11 | network_id: '*', 12 | port: 8555, 13 | gas: 0xfffffffffff, 14 | gasPrice: 0x01, 15 | }, 16 | }, 17 | // Configure your compilers 18 | compilers: { 19 | solc: { 20 | version: '0.5.12', 21 | settings: { // See the solidity docs for advice about optimization and evmVersion 22 | optimizer: { 23 | enabled: true, 24 | runs: 100, 25 | }, 26 | evmVersion: 'byzantium', 27 | }, 28 | }, 29 | }, 30 | }; 31 | --------------------------------------------------------------------------------