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