├── .flake8 ├── .github └── workflows │ └── test.yml ├── .pre-commit-config.yaml ├── .pre-commit-hooks.yaml ├── CHANGES.md ├── LICENSE.md ├── README.md ├── blackadder ├── __init__.py ├── main.py └── vyper_compat.py ├── setup.py └── tests ├── data ├── Vault.vy ├── bad-indent.ndiff └── long-line.ndiff ├── test_fixers.py └── test_vyper.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | # black likes this (https://github.com/psf/black/blob/master/docs/compatible_configs.md#flake8) 3 | max-line-length = 88 4 | extend-ignore = E203 5 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - '*' 8 | tags: 9 | - v* 10 | jobs: 11 | build: 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | python-version: [3.6, 3.7, 3.8, 3.9] 17 | os: [ubuntu-latest, macOS-latest, windows-latest] 18 | 19 | env: 20 | PUBLISH_TO_PYPI: ${{ matrix.python-version == '3.9' && matrix.os == 'ubuntu-latest' && github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') }} 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | 25 | - name: Set up Python ${{ matrix.python-version }} 26 | uses: actions/setup-python@v2 27 | with: 28 | python-version: ${{ matrix.python-version }} 29 | 30 | - name: Install dependencies 31 | run: | 32 | python -m pip install --upgrade pip 33 | python -m pip install --upgrade pytest 34 | python -m pip install -e . 35 | 36 | - name: Unit tests 37 | run: | 38 | pytest -v 39 | 40 | - name: Build wheels 41 | if: env.PUBLISH_TO_PYPI == 'true' 42 | run: pip install wheel && python setup.py sdist bdist_wheel 43 | 44 | - name: Publish to PyPI 45 | # publish only if commit is tagged and we are on master, on a single item of the matrix, 46 | if: env.PUBLISH_TO_PYPI == 'true' 47 | uses: pypa/gh-action-pypi-publish@master 48 | with: 49 | user: __token__ 50 | password: ${{ secrets.PYPI_PASSWORD }} 51 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | 2 | repos: 3 | - repo: https://github.com/psf/black 4 | rev: stable 5 | hooks: 6 | - id: black 7 | language_version: python3 8 | 9 | - repo: https://gitlab.com/pycqa/flake8 10 | rev: 3.8.4 11 | hooks: 12 | - id: flake8 13 | language_version: python3 14 | -------------------------------------------------------------------------------- /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | - id: blackadder 2 | name: blackadder 3 | entry: blackadder 4 | files: \.(vy)$ 5 | language: python 6 | language_version: python3 7 | args: [--fast] 8 | require_serial: true -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vyperlang/blackadder/aa5e5cb3a5052683f8aa900670d5a3dfc82766ad/CHANGES.md -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2020 spinoch 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [_black_](https://github.com/psf/black)-[_adder_](https://github.com/vyperlang/vyper) 2 | 3 | A code formatter for vyper. 4 | 5 | ## Usage 6 | 7 | To run it manually, first install using pip: 8 | ``` 9 | pip install blackadder 10 | ``` 11 | Then run the following in you terminal: 12 | ``` 13 | blackadder --fast --include '\.vy$' . 14 | ``` 15 | 16 | You can also use pre-commit hooks: put this in your repo's `.pre-commit-config.yaml` and add Python's `pre-commit` to your test requirements. 17 | ``` 18 | repos: 19 | - repo: https://github.com/spinoch/blackadder 20 | rev: "master" 21 | hooks: 22 | - id: blackadder 23 | language_version: python3 24 | ``` 25 | 26 | 27 | 28 | ## Warning 29 | This is still in an experimental state. Formatting may be done incorrectly for `log` keywords if the next variable name is less than 3 characters long. 30 | If you run into issues, check out the logs to see what happens. 31 | 32 | As of now, `blackadder` will fail if invoked without `--fast`. 33 | In general, black options shouldn't be expected to work except for `--diff` and `--include`. 34 | -------------------------------------------------------------------------------- /blackadder/__init__.py: -------------------------------------------------------------------------------- 1 | from black import format_str as black_format_str 2 | from black import FileContent 3 | from blackadder.vyper_compat import pre_format_str, post_format_str 4 | 5 | 6 | def format_str_override(src_contents: str, **kwargs) -> FileContent: 7 | vyper_types_names, src_contents = pre_format_str(src_contents) 8 | dst_contents = black_format_str(src_contents=src_contents, **kwargs) 9 | dst_contents = post_format_str(vyper_types_names, dst_contents) 10 | return dst_contents 11 | 12 | 13 | # Override black's `format_str` 14 | import black # noqa 15 | 16 | black.format_str = format_str_override 17 | 18 | from black import * # noqa: F4, E4 19 | -------------------------------------------------------------------------------- /blackadder/main.py: -------------------------------------------------------------------------------- 1 | from black_vyper import patched_main 2 | 3 | patched_main() 4 | -------------------------------------------------------------------------------- /blackadder/vyper_compat.py: -------------------------------------------------------------------------------- 1 | import re 2 | from vyper.ast.pre_parser import VYPER_CLASS_TYPES, VYPER_EXPRESSION_TYPES, pre_parse 3 | 4 | WHITESPACE_EXCEPT_LINEBREAK = r"[^\S\r\n]" 5 | # Whitespace plus optional (multiple) `\` followed by a line break 6 | MIDDLE_WHITESPACE = ( 7 | rf"{WHITESPACE_EXCEPT_LINEBREAK}+" 8 | rf"(?:\\{WHITESPACE_EXCEPT_LINEBREAK}*\r?\n{WHITESPACE_EXCEPT_LINEBREAK}*)*" 9 | ) 10 | REPLACEMENT_CHARACTER = "_" # character used in variable name replacements 11 | 12 | VYPER_DEPRECATED_CLASS_TYPES = {"contract"} 13 | VYPER_CLASS_TYPES |= VYPER_DEPRECATED_CLASS_TYPES 14 | 15 | 16 | def pre_format_str(src_contents): 17 | src_contents = src_contents.lstrip() 18 | 19 | vyper_types_names = re.findall( 20 | rf"^(?:[^\S\r\n]*)" 21 | fr"(?P{'|'.join(VYPER_CLASS_TYPES.union(VYPER_EXPRESSION_TYPES))})" 22 | fr"{MIDDLE_WHITESPACE}" 23 | fr"(?P\w+).*$", 24 | src_contents, 25 | flags=re.M, 26 | ) 27 | assert len(vyper_types_names) == len(pre_parse(src_contents)[0]) 28 | 29 | REGEX_SUBSTITUTE_VYPER_TYPES = re.compile( 30 | fr"^(?P[^\S\r\n]*)" 31 | fr"(?P{'|'.join(VYPER_CLASS_TYPES.union(VYPER_EXPRESSION_TYPES))})" 32 | fr"(?P{MIDDLE_WHITESPACE})" 33 | fr"(?P\w+)" 34 | fr"(?P.*)$", 35 | flags=re.M, 36 | ) 37 | 38 | # Convert vyper-specific declarations to valid Python 39 | for vyper_type, vyper_name in vyper_types_names: 40 | if vyper_type in VYPER_CLASS_TYPES: 41 | replacement_type = "class" 42 | elif vyper_type in VYPER_EXPRESSION_TYPES: 43 | replacement_type = "yield" 44 | else: 45 | raise RuntimeError(f"Unknown vyper type: {vyper_type}") 46 | 47 | type_length_difference = len(vyper_type) - len(replacement_type) 48 | # We will replace the variable name by an arbitrary name 49 | # so that the number of characters of type + name match 50 | replacement_name_length = len(vyper_name) + type_length_difference 51 | if replacement_name_length < 1: 52 | # Variable name length must be at least 1 53 | replacement_name_length = 1 54 | 55 | replacement_name = REPLACEMENT_CHARACTER * replacement_name_length 56 | 57 | # TODO: can fail if `vyper_name` is too short 58 | # (when using `log` with a variable of less than 3 characters length) 59 | # assert ( len(vyper_type) + len(vyper_name) == \ 60 | # len(replacement_type) + len(replacement_name))) 61 | def _replacement_function(match): 62 | return ( 63 | f"{match.group('leading_whitespace')}" 64 | f"{replacement_type}" 65 | f"{match.group('middle_whitespace')}" 66 | f"{replacement_name}" 67 | f"{match.group('trailing_characters')}" 68 | ) 69 | 70 | # Substitute the original string 71 | src_contents = REGEX_SUBSTITUTE_VYPER_TYPES.sub( 72 | _replacement_function, string=src_contents, count=1 73 | ) 74 | 75 | return vyper_types_names, src_contents 76 | 77 | 78 | def post_format_str( 79 | vyper_types_names, 80 | dst_contents, 81 | ): 82 | # ?P saves the group under match.group("$name") 83 | REGEX_EXTRACT_VYPER_NAMES = re.compile( 84 | fr"^(?P[^\S\r\n]*)(?:class|yield)" 85 | fr"(?P{MIDDLE_WHITESPACE})" 86 | fr"(?P{REPLACEMENT_CHARACTER}+)" 87 | fr"(?P.*)$", 88 | flags=re.MULTILINE, 89 | ) 90 | 91 | for vyper_type, var_name in vyper_types_names: 92 | 93 | def _replacement_function(match): 94 | return ( 95 | f"{match.group('leading_whitespace')}" 96 | f"{vyper_type}" 97 | f"{match.group('middle_whitespace')}" 98 | f"{var_name}" 99 | f"{match.group('trailing_characters')}" 100 | ) 101 | 102 | dst_contents = REGEX_EXTRACT_VYPER_NAMES.sub( 103 | _replacement_function, string=dst_contents, count=1 104 | ) 105 | 106 | return dst_contents 107 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Parts of this file were taken from black (https://github.com/psf/black/) 3 | """ 4 | from setuptools import setup 5 | import sys 6 | 7 | assert sys.version_info >= (3, 6, 0), "black requires Python 3.6+" 8 | from pathlib import Path # noqa E402 9 | 10 | CURRENT_DIR = Path(__file__).parent 11 | sys.path.insert(0, str(CURRENT_DIR)) # for setuptools.build_meta 12 | 13 | 14 | def get_long_description() -> str: 15 | return ( 16 | (CURRENT_DIR / "README.md").read_text(encoding="utf8") 17 | + "\n\n" 18 | + (CURRENT_DIR / "CHANGES.md").read_text(encoding="utf8") 19 | ) 20 | 21 | 22 | setup( 23 | name="blackadder", 24 | use_scm_version=True, 25 | setup_requires=["setuptools_scm"], 26 | description="The uncompromising code formatter.", 27 | long_description=get_long_description(), 28 | long_description_content_type="text/markdown", 29 | packages=["blackadder"], 30 | python_requires=">=3.6", 31 | install_requires=["black>=20.8b1", "vyper>=0.2.7"], 32 | test_suite="tests", 33 | tests_require=["pytest"], 34 | classifiers=[ 35 | "Development Status :: 3 - Alpha", 36 | "Environment :: Console", 37 | "Intended Audience :: Developers", 38 | "License :: OSI Approved :: MIT License", 39 | "Operating System :: OS Independent", 40 | "Programming Language :: Python", 41 | "Programming Language :: Python :: 3.6", 42 | "Programming Language :: Python :: 3.7", 43 | "Programming Language :: Python :: 3.8", 44 | "Programming Language :: Python :: 3.9", 45 | "Programming Language :: Python :: 3 :: Only", 46 | "Topic :: Software Development :: Libraries :: Python Modules", 47 | "Topic :: Software Development :: Quality Assurance", 48 | ], 49 | entry_points={ 50 | "console_scripts": [ 51 | "blackadder=blackadder:patched_main", 52 | ] 53 | }, 54 | ) 55 | -------------------------------------------------------------------------------- /tests/data/Vault.vy: -------------------------------------------------------------------------------- 1 | #@version 0.2.7 2 | 3 | API_VERSION: constant(String[28]) = "0.1.2" 4 | 5 | # TODO: Add ETH Configuration 6 | # TODO: Add Delegated Configuration 7 | from vyper.interfaces import ERC20 8 | 9 | implements: ERC20 10 | 11 | interface \ 12 | \ 13 | \ 14 | DetailedERC20: 15 | def name() -> String[42]: view 16 | def symbol() -> String[20]: view 17 | def decimals() -> uint256: view 18 | 19 | interface Strategy: 20 | def strategist() -> address: view 21 | def estimatedTotalAssets() -> uint256: view 22 | def withdraw(_amount: uint256): nonpayable 23 | def migrate(_newStrategy: address): nonpayable 24 | 25 | event Transfer: 26 | sender: indexed(address) 27 | receiver: indexed(address) 28 | value: uint256 29 | 30 | event Approval: 31 | owner: indexed(address) 32 | spender: indexed(address) 33 | value: uint256 34 | 35 | 36 | name: public(String[64]) 37 | symbol: public(String[32]) 38 | decimals: public(uint256) 39 | 40 | balanceOf: public(HashMap[address, uint256]) 41 | allowance: public(HashMap[address, HashMap[address, uint256]]) 42 | totalSupply: public(uint256) 43 | 44 | token: public(ERC20) 45 | governance: public(address) 46 | guardian: public(address) 47 | pendingGovernance: address 48 | 49 | struct StrategyParams: 50 | performanceFee: uint256 # Strategist's fee (basis points) 51 | activation: uint256 # Activation block.number 52 | debtLimit: uint256 # Maximum borrow amount 53 | rateLimit: uint256 # Increase/decrease per block 54 | lastReport: uint256 # block.number of the last time a report occured 55 | totalDebt: uint256 # Total outstanding debt that Strategy has 56 | totalReturns: uint256 # Total returns that Strategy has realized for Vault 57 | 58 | event StrategyAdded: 59 | strategy: indexed(address) 60 | debtLimit: uint256 # Maximum borrow amount 61 | rateLimit: uint256 # Increase/decrease per block 62 | performanceFee: uint256 # Strategist's fee (basis points) 63 | 64 | event StrategyReported: 65 | strategy: indexed(address) 66 | returnAdded: uint256 67 | debtAdded: uint256 68 | totalReturn: uint256 69 | totalDebt: uint256 70 | debtLimit: uint256 71 | 72 | # NOTE: Track the total for overhead targeting purposes 73 | strategies: public(HashMap[address, StrategyParams]) 74 | MAXIMUM_STRATEGIES: constant(uint256) = 20 75 | 76 | # Ordering that `withdraw` uses to determine which strategies to pull funds from 77 | # NOTE: Does *NOT* have to match the ordering of all the current strategies that 78 | # exist, but it is recommended that it does or else withdrawal depth is 79 | # limited to only those inside the the queue. 80 | # NOTE: Ordering is determined by governance, and should be balanced according 81 | # to risk, slippage, and/or volitility. Can also be ordered to increase the 82 | # withdrawal speed of a particular strategy. 83 | # NOTE: The first time a ZERO_ADDRESS is encountered, it stops withdrawing 84 | withdrawalQueue: public(address[MAXIMUM_STRATEGIES]) 85 | 86 | emergencyShutdown: public(bool) 87 | 88 | depositLimit: public(uint256) # Limit for totalAssets the Vault can hold 89 | debtLimit: public(uint256) # Debt limit for the Vault across all strategies 90 | totalDebt: public(uint256) # Amount of tokens that all strategies have borrowed 91 | lastReport: public(uint256) # Number of blocks since last report 92 | 93 | rewards: public(address) # Rewards contract where Governance fees are sent to 94 | managementFee: public(uint256) # Governance Fee for management of Vault (given to `rewards`) 95 | performanceFee: public(uint256) # Governance Fee for performance of Vault (given to `rewards`) 96 | FEE_MAX: constant(uint256) = 10_000 # 100%, or 10k basis points 97 | BLOCKS_PER_YEAR: constant(uint256) = 2_300_000 98 | 99 | @external 100 | def __init__( 101 | _token: address, 102 | _governance: address, 103 | _rewards: address, 104 | _nameOverride: String[64], 105 | _symbolOverride: String[32] 106 | ): 107 | # TODO: Non-detailed Configuration? 108 | self.token = ERC20(_token) 109 | if _nameOverride == "": 110 | self.name = concat(DetailedERC20(_token).symbol(), " yVault") 111 | else: 112 | self.name = _nameOverride 113 | if _symbolOverride == "": 114 | self.symbol = concat("yv", DetailedERC20(_token).symbol()) 115 | else: 116 | self.symbol = _symbolOverride 117 | self.decimals = DetailedERC20(_token).decimals() 118 | self.governance = _governance 119 | self.rewards = _rewards 120 | self.guardian = msg.sender 121 | self.performanceFee = 450 # 4.5% of yield (per strategy) 122 | self.managementFee = 200 # 2% per year 123 | self.depositLimit = MAX_UINT256 # Start unlimited 124 | self.lastReport = block.number 125 | 126 | 127 | @pure 128 | @external 129 | def apiVersion() -> String[28]: 130 | return API_VERSION 131 | 132 | 133 | @external 134 | def setName(_name: String[42]): 135 | assert msg.sender == self.governance 136 | self.name = _name 137 | 138 | 139 | @external 140 | def setSymbol(_symbol: String[20]): 141 | assert msg.sender == self.governance 142 | self.symbol = _symbol 143 | 144 | 145 | # 2-phase commit for a change in governance 146 | @external 147 | def setGovernance(_governance: address): 148 | assert msg.sender == self.governance 149 | self.pendingGovernance = _governance 150 | 151 | 152 | @external 153 | def acceptGovernance(): 154 | assert msg.sender == self.pendingGovernance 155 | self.governance = msg.sender 156 | 157 | 158 | @external 159 | def setRewards(_rewards: address): 160 | assert msg.sender == self.governance 161 | self.rewards = _rewards 162 | 163 | 164 | @external 165 | def setDepositLimit(_limit: uint256): 166 | assert msg.sender == self.governance 167 | self.depositLimit = _limit 168 | 169 | 170 | @external 171 | def setPerformanceFee(_fee: uint256): 172 | assert msg.sender == self.governance 173 | self.performanceFee = _fee 174 | 175 | 176 | @external 177 | def setManagementFee(_fee: uint256): 178 | assert msg.sender == self.governance 179 | self.managementFee = _fee 180 | 181 | 182 | @external 183 | def setGuardian(_guardian: address): 184 | assert msg.sender in [self.guardian, self.governance] 185 | self.guardian = _guardian 186 | 187 | 188 | @external 189 | def setEmergencyShutdown(_active: bool): 190 | """ 191 | Activates Vault mode where all Strategies go into full withdrawal 192 | """ 193 | assert msg.sender in [self.guardian, self.governance] 194 | self.emergencyShutdown = _active 195 | 196 | 197 | @external 198 | def setWithdrawalQueue(_queue: address[MAXIMUM_STRATEGIES]): 199 | assert msg.sender == self.governance 200 | # HACK: Temporary until Vyper adds support for Dynamic arrays 201 | for i in range(MAXIMUM_STRATEGIES): 202 | if _queue[i] == ZERO_ADDRESS and self.withdrawalQueue[i] == ZERO_ADDRESS: 203 | break 204 | assert self.strategies[_queue[i]].activation > 0 205 | self.withdrawalQueue[i] = _queue[i] 206 | 207 | 208 | @internal 209 | def _transfer(_from: address, _to: address, _value: uint256): 210 | # Protect people from accidentally sending their shares to bad places 211 | assert not (_to in [self, ZERO_ADDRESS]) 212 | self.balanceOf[_from] -= _value 213 | self.balanceOf[_to] += _value 214 | log \ 215 | Transfer(_from, _to, _value) 216 | 217 | 218 | @external 219 | def transfer(_to: address, _value: uint256) -> bool: 220 | self._transfer(msg.sender, _to, _value) 221 | return True 222 | 223 | 224 | @external 225 | def transferFrom(_from : address, _to : address, _value : uint256) -> bool: 226 | if self.allowance[_from][msg.sender] < MAX_UINT256: # Unlimited approval (saves an SSTORE) 227 | self.allowance[_from][msg.sender] -= _value 228 | self._transfer(_from, _to, _value) 229 | return True 230 | 231 | 232 | @external 233 | def approve(_spender : address, _value : uint256) -> bool: 234 | """ 235 | @dev Approve the passed address to spend the specified amount of tokens on behalf of 236 | msg.sender. Beware that changing an allowance with this method brings the risk 237 | that someone may use both the old and the new allowance by unfortunate transaction 238 | ordering. See https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 239 | @param _spender The address which will spend the funds. 240 | @param _value The amount of tokens to be spent. 241 | """ 242 | self.allowance[msg.sender][_spender] = _value 243 | log Approval(msg.sender, _spender, _value) 244 | return True 245 | 246 | 247 | @view 248 | @internal 249 | def _totalAssets() -> uint256: 250 | return self.token.balanceOf(self) + self.totalDebt 251 | 252 | 253 | @view 254 | @external 255 | def totalAssets() -> uint256: 256 | return self._totalAssets() 257 | 258 | 259 | @view 260 | @internal 261 | def _balanceSheetOfStrategy(_strategy: address) -> uint256: 262 | return Strategy(_strategy).estimatedTotalAssets() 263 | 264 | 265 | @view 266 | @external 267 | def balanceSheetOfStrategy(_strategy: address) -> uint256: 268 | return self._balanceSheetOfStrategy(_strategy) 269 | 270 | 271 | @view 272 | @external 273 | def totalBalanceSheet(_strategies: address[2 * MAXIMUM_STRATEGIES]) -> uint256: 274 | """ 275 | Measure the total balance sheet of this Vault, using the list of strategies 276 | given above. (2x the expected maximum is used for safety's sake) 277 | NOTE: The safety of this function depends *entirely* on the list of strategies 278 | given as the function argument. Care should be taken to choose this list 279 | to ensure that the estimate is accurate. No additional checking is used. 280 | NOTE: Guardian should use this value vs. `totalAssets()` to determine 281 | if a condition exists where the Vault is experiencing a dangerous 282 | 'balance sheet' attack, leading Vault shares to be worth less than 283 | what their price on paper is (based on their debt) 284 | """ 285 | balanceSheet: uint256 = self.token.balanceOf(self) 286 | 287 | for strategy in _strategies: 288 | if strategy == ZERO_ADDRESS: 289 | break 290 | balanceSheet += self._balanceSheetOfStrategy(strategy) 291 | 292 | return balanceSheet 293 | 294 | 295 | @internal 296 | def _issueSharesForAmount(_to: address, _amount: uint256) -> uint256: 297 | # NOTE: shares must be issued prior to taking on new collateral, 298 | # or calculation will be wrong. This means that only *trusted* 299 | # tokens (with no capability for exploitive behavior) can be used 300 | shares: uint256 = 0 301 | # HACK: Saves 2 SLOADs (~4000 gas) 302 | totalSupply: uint256 = self.totalSupply 303 | if totalSupply > 0: 304 | # Mint amount of shares based on what the Vault is managing overall 305 | shares = _amount * totalSupply / self._totalAssets() 306 | else: 307 | # No existing shares, so mint 1:1 308 | shares = _amount 309 | 310 | # Mint new shares 311 | self.totalSupply = totalSupply + shares 312 | self.balanceOf[_to] += shares 313 | log Transfer(ZERO_ADDRESS, _to, shares) 314 | 315 | return shares 316 | 317 | 318 | @external 319 | def deposit(_amount: uint256 = MAX_UINT256, _recipient: address = msg.sender) -> uint256: 320 | assert not self.emergencyShutdown # Deposits are locked out 321 | 322 | amount: uint256 = _amount 323 | 324 | # If _amount not specified, transfer the full token balance 325 | if amount == MAX_UINT256: 326 | amount = self.token.balanceOf(msg.sender) 327 | 328 | # Ensure we are depositing something 329 | assert amount > 0 330 | 331 | # Ensure deposit limit is respected 332 | assert self._totalAssets() + amount <= self.depositLimit 333 | 334 | # NOTE: Measuring this based on the total outstanding debt that this contract 335 | # has ("expected value") instead of the total balance sheet it has 336 | # ("estimated value") has important security considerations, and is 337 | # done intentionally. If this value were measured against external 338 | # systems, it could be purposely manipulated by an attacker to withdraw 339 | # more assets than they otherwise should be able to claim by redeeming 340 | # their shares. 341 | # 342 | # On deposit, this means that shares are issued against the total amount 343 | # that the deposited capital can be given in service of the debt that 344 | # Strategies assume. If that number were to be lower than the "expected value" 345 | # at some future point, depositing shares via this method could entitle the 346 | # depositor to *less* than the deposited value once the "realized value" is 347 | # updated from further reportings by the Strategies to the Vaults. 348 | # 349 | # Care should be taken by integrators to account for this discrepency, 350 | # by using the view-only methods of this contract (both off-chain and 351 | # on-chain) to determine if depositing into the Vault is a "good idea" 352 | 353 | # Issue new shares (needs to be done before taking deposit to be accurate) 354 | # Shares are issued to recipient (may be different from msg.sender) 355 | shares: uint256 = self._issueSharesForAmount(_recipient, amount) 356 | 357 | # Get new collateral 358 | reserve: uint256 = self.token.balanceOf(self) 359 | # Tokens are transferred from msg.sender (may be different from _recipient) 360 | self.token.transferFrom(msg.sender, self, amount) 361 | # TODO: `Deflationary` configuration only 362 | assert self.token.balanceOf(self) - reserve == amount # Deflationary token check 363 | 364 | return shares # Just in case someone wants them 365 | 366 | 367 | @view 368 | @internal 369 | def _shareValue(_shares: uint256) -> uint256: 370 | return (_shares * (self._totalAssets())) / self.totalSupply 371 | 372 | 373 | @view 374 | @internal 375 | def _sharesForAmount(_amount: uint256) -> uint256: 376 | if self._totalAssets() > 0: 377 | return (_amount * self.totalSupply) / self._totalAssets() 378 | else: 379 | return 0 380 | 381 | 382 | @view 383 | @external 384 | def maxAvailableShares() -> uint256: 385 | shares: uint256 = self._sharesForAmount(self.token.balanceOf(self)) 386 | 387 | for strategy in self.withdrawalQueue: 388 | if strategy == ZERO_ADDRESS: 389 | break 390 | shares += self._sharesForAmount(self.strategies[strategy].totalDebt) 391 | 392 | return shares 393 | 394 | 395 | @external 396 | def withdraw(_shares: uint256 = MAX_UINT256, _recipient: address = msg.sender) -> uint256: 397 | shares: uint256 = _shares # May reduce this number below 398 | 399 | # If _shares not specified, transfer full share balance 400 | if shares == MAX_UINT256: 401 | shares = self.balanceOf[msg.sender] 402 | 403 | # Limit to only the shares they own 404 | assert shares <= self.balanceOf[msg.sender] 405 | 406 | # NOTE: Measuring this based on the total outstanding debt that this contract 407 | # has ("expected value") instead of the total balance sheet it has 408 | # ("estimated value") has important security considerations, and is 409 | # done intentionally. If this value were measured against external 410 | # systems, it could be purposely manipulated by an attacker to withdraw 411 | # more assets than they otherwise should be able to claim by redeeming 412 | # their shares. 413 | # 414 | # On withdrawal, this means that shares are redeemed against the total 415 | # amount that the deposited capital had "realized" since the point it 416 | # was deposited, up until the point it was withdrawn. If that number 417 | # were to be higher than the "expected value" at some future point, 418 | # withdrawing shares via this method could entitle the depositor to 419 | # *more* than the expected value once the "realized value" is updated 420 | # from further reportings by the Strategies to the Vaults. 421 | # 422 | # Under exceptional scenarios, this could cause earlier withdrawals to 423 | # earn "more" of the underlying assets than Users might otherwise be 424 | # entitled to, if the Vault's estimated value were otherwise measured 425 | # through external means, accounting for whatever exceptional scenarios 426 | # exist for the Vault (that aren't covered by the Vault's own design) 427 | value: uint256 = self._shareValue(shares) 428 | 429 | if value > self.token.balanceOf(self): 430 | # We need to go get some from our strategies in the withdrawal queue 431 | # NOTE: This performs forced withdrawals from each strategy. There is 432 | # a 0.5% withdrawal fee assessed on each forced withdrawal (<= 0.5% total) 433 | for strategy in self.withdrawalQueue: 434 | if strategy == ZERO_ADDRESS: 435 | break # We've exhausted the queue 436 | 437 | amountNeeded: uint256 = value - self.token.balanceOf(self) 438 | 439 | if amountNeeded == 0: 440 | break # We're done withdrawing 441 | 442 | # NOTE: Don't withdraw more than the debt so that strategy can still 443 | # continue to work based on the profits it has 444 | # NOTE: This means that user will lose out on any profits that each 445 | # strategy in the queue would return on next harvest, benefitting others 446 | amountNeeded = min(amountNeeded, self.strategies[strategy].totalDebt) 447 | if amountNeeded == 0: 448 | continue # Nothing to withdraw from this strategy, try the next one 449 | 450 | # Force withdraw amount from each strategy in the order set by governance 451 | before: uint256 = self.token.balanceOf(self) 452 | Strategy(strategy).withdraw(amountNeeded) 453 | withdrawn: uint256 = self.token.balanceOf(self) - before 454 | 455 | # Reduce the strategy's debt by the amount withdrawn ("realized returns") 456 | # NOTE: This doesn't add to returns as it's not earned by "normal means" 457 | self.strategies[strategy].totalDebt -= withdrawn 458 | self.totalDebt -= withdrawn 459 | 460 | # NOTE: We have withdrawn everything possible out of the withdrawal queue 461 | # but we still don't have enough to fully pay them back, so adjust 462 | # to the total amount we've freed up through forced withdrawals 463 | if value > self.token.balanceOf(self): 464 | value = self.token.balanceOf(self) 465 | shares = self._sharesForAmount(value) 466 | 467 | # Burn shares (full value of what is being withdrawn) 468 | self.totalSupply -= shares 469 | self.balanceOf[msg.sender] -= shares 470 | log Transfer(msg.sender, ZERO_ADDRESS, shares) 471 | 472 | # Withdraw remaining balance to _recipient (may be different to msg.sender) (minus fee) 473 | self.token.transfer(_recipient, value) 474 | 475 | return value 476 | 477 | 478 | @view 479 | @external 480 | def pricePerShare() -> uint256: 481 | return self._shareValue(10 ** self.decimals) 482 | 483 | 484 | @internal 485 | def _organizeWithdrawalQueue(): 486 | # Reorganize based on premise that if there is an empty value between two 487 | # actual values, then the empty value should be replaced by the later value 488 | # NOTE: Relative ordering of non-zero values is maintained 489 | offset: uint256 = 0 490 | for idx in range(MAXIMUM_STRATEGIES): 491 | strategy: address = self.withdrawalQueue[idx] 492 | if strategy == ZERO_ADDRESS: 493 | offset += 1 # how many values we need to shift, always `<= idx` 494 | elif offset > 0: 495 | self.withdrawalQueue[idx-offset] = strategy 496 | self.withdrawalQueue[idx] = ZERO_ADDRESS 497 | 498 | 499 | @external 500 | def addStrategy( 501 | _strategy: address, 502 | _debtLimit: uint256, 503 | _rateLimit: uint256, 504 | _performanceFee: uint256, 505 | ): 506 | assert msg.sender == self.governance 507 | assert self.strategies[_strategy].activation == 0 508 | self.strategies[_strategy] = StrategyParams({ 509 | performanceFee: _performanceFee, 510 | activation: block.number, 511 | debtLimit: _debtLimit, 512 | rateLimit: _rateLimit, 513 | lastReport: block.number, 514 | totalDebt: 0, 515 | totalReturns: 0, 516 | }) 517 | self.debtLimit += _debtLimit 518 | log StrategyAdded(_strategy, _debtLimit, _rateLimit, _performanceFee) 519 | 520 | # queue is full 521 | assert self.withdrawalQueue[MAXIMUM_STRATEGIES-1] == ZERO_ADDRESS 522 | self.withdrawalQueue[MAXIMUM_STRATEGIES-1] = _strategy 523 | self._organizeWithdrawalQueue() 524 | 525 | 526 | @external 527 | def updateStrategyDebtLimit( 528 | _strategy: address, 529 | _debtLimit: uint256, 530 | ): 531 | assert msg.sender == self.governance 532 | assert self.strategies[_strategy].activation > 0 533 | self.debtLimit -= self.strategies[_strategy].debtLimit 534 | self.strategies[_strategy].debtLimit = _debtLimit 535 | self.debtLimit += _debtLimit 536 | 537 | 538 | @external 539 | def updateStrategyRateLimit( 540 | _strategy: address, 541 | _rateLimit: uint256, 542 | ): 543 | assert msg.sender == self.governance 544 | assert self.strategies[_strategy].activation > 0 545 | self.strategies[_strategy].rateLimit = _rateLimit 546 | 547 | 548 | @external 549 | def updateStrategyPerformanceFee( 550 | _strategy: address, 551 | _performanceFee: uint256, 552 | ): 553 | assert msg.sender == self.governance 554 | assert self.strategies[_strategy].activation > 0 555 | self.strategies[_strategy].performanceFee = _performanceFee 556 | 557 | 558 | @external 559 | def migrateStrategy(_oldVersion: address, _newVersion: address): 560 | """ 561 | Only Governance can migrate a strategy to a new version 562 | NOTE: Strategy must successfully migrate all capital and positions to 563 | new Strategy, or else this will upset the balance of the Vault 564 | NOTE: The new strategy should be "empty" e.g. have no prior commitments 565 | to this Vault, otherwise it could have issues 566 | """ 567 | assert msg.sender == self.governance 568 | 569 | assert self.strategies[_oldVersion].activation > 0 570 | assert self.strategies[_newVersion].activation == 0 571 | 572 | strategy: StrategyParams = self.strategies[_oldVersion] 573 | self.strategies[_oldVersion] = empty(StrategyParams) 574 | self.strategies[_newVersion] = strategy 575 | 576 | Strategy(_oldVersion).migrate(_newVersion) 577 | # TODO: Ensure a smooth transition in terms of strategy return 578 | 579 | for idx in range(MAXIMUM_STRATEGIES): 580 | if self.withdrawalQueue[idx] == _oldVersion: 581 | self.withdrawalQueue[idx] = _newVersion 582 | return # Don't need to reorder anything because we swapped 583 | 584 | 585 | @external 586 | def revokeStrategy(_strategy: address = msg.sender): 587 | """ 588 | Governance can revoke a strategy 589 | OR 590 | A strategy can revoke itself (Emergency Exit Mode) 591 | """ 592 | assert msg.sender in [_strategy, self.governance, self.guardian] 593 | self.debtLimit -= self.strategies[_strategy].debtLimit 594 | self.strategies[_strategy].debtLimit = 0 595 | 596 | 597 | @external 598 | def addStrategyToQueue(_strategy: address): 599 | assert msg.sender == self.governance 600 | # Must be a current strategy 601 | assert self.strategies[_strategy].activation > 0 and self.strategies[_strategy].totalDebt > 0 602 | # Check if queue is full 603 | assert self.withdrawalQueue[MAXIMUM_STRATEGIES-1] == ZERO_ADDRESS 604 | # Can't already be in the queue 605 | for strategy in self.withdrawalQueue: 606 | if strategy == ZERO_ADDRESS: 607 | break 608 | assert strategy != _strategy 609 | self.withdrawalQueue[MAXIMUM_STRATEGIES-1] = _strategy 610 | self._organizeWithdrawalQueue() 611 | 612 | 613 | @external 614 | def removeStrategyFromQueue(_strategy: address): 615 | # NOTE: We don't do this with revokeStrategy because it should still 616 | # be possible to withdraw from it if it's unwinding 617 | assert msg.sender == self.governance 618 | for idx in range(MAXIMUM_STRATEGIES): 619 | if self.withdrawalQueue[idx] == _strategy: 620 | self.withdrawalQueue[idx] = ZERO_ADDRESS 621 | self._organizeWithdrawalQueue() 622 | return # We found the right location and cleared it 623 | raise # We didn't find the strategy in the queue 624 | 625 | 626 | @view 627 | @internal 628 | def _debtOutstanding(_strategy: address) -> uint256: 629 | """ 630 | Amount of tokens in strtaegy that Vault wants to recall 631 | """ 632 | strategy_debtLimit: uint256 = self.strategies[_strategy].debtLimit 633 | strategy_totalDebt: uint256 = self.strategies[_strategy].totalDebt 634 | 635 | if self.emergencyShutdown: 636 | return strategy_totalDebt 637 | elif strategy_totalDebt <= strategy_debtLimit: 638 | return 0 639 | else: 640 | return strategy_totalDebt - strategy_debtLimit 641 | 642 | 643 | @view 644 | @external 645 | def debtOutstanding(_strategy: address = msg.sender) -> uint256: 646 | return self._debtOutstanding(_strategy) 647 | 648 | 649 | @view 650 | @internal 651 | def _creditAvailable(_strategy: address) -> uint256: 652 | """ 653 | Amount of tokens in vault a strategy has access to as a credit line 654 | """ 655 | if self.emergencyShutdown: 656 | return 0 657 | 658 | strategy_debtLimit: uint256 = self.strategies[_strategy].debtLimit 659 | strategy_totalDebt: uint256 = self.strategies[_strategy].totalDebt 660 | strategy_rateLimit: uint256 = self.strategies[_strategy].rateLimit 661 | strategy_lastReport: uint256 = self.strategies[_strategy].lastReport 662 | 663 | # Exhausted credit line 664 | if strategy_debtLimit <= strategy_totalDebt or self.debtLimit <= self.totalDebt: 665 | return 0 666 | 667 | # Start with debt limit left for the strategy 668 | available: uint256 = strategy_debtLimit - strategy_totalDebt 669 | 670 | # Adjust by the global debt limit left 671 | available = min(available, self.debtLimit - self.totalDebt) 672 | 673 | # Adjust by the rate limit algorithm (limits the step size per reporting period) 674 | blockDelta: uint256 = block.number - strategy_lastReport 675 | # NOTE: Protect against unnecessary overflow faults here 676 | # NOTE: Set `strategy_rateLimit` to a really high number to disable the rate limit 677 | # NOTE: *NEVER* set `strategy_rateLimit` to 0 or else this will always throw 678 | if available / strategy_rateLimit >= blockDelta: 679 | available = min(available, strategy_rateLimit * blockDelta) 680 | 681 | # Can only borrow up to what the contract has in reserve 682 | # NOTE: Running near 100% is discouraged 683 | return min(available, self.token.balanceOf(self)) 684 | 685 | 686 | @view 687 | @external 688 | def creditAvailable(_strategy: address = msg.sender) -> uint256: 689 | return self._creditAvailable(_strategy) 690 | 691 | 692 | @view 693 | @internal 694 | def _expectedReturn(_strategy: address) -> uint256: 695 | strategy_lastReport: uint256 = self.strategies[_strategy].lastReport 696 | strategy_totalReturns: uint256 = self.strategies[_strategy].totalReturns 697 | strategy_activation: uint256 = self.strategies[_strategy].activation 698 | 699 | blockDelta: uint256 = (block.number - strategy_lastReport) 700 | if blockDelta > 0: 701 | return (strategy_totalReturns * blockDelta) / (block.number - strategy_activation) 702 | else: 703 | return 0 # Covers the scenario when block.number == strategy_activation 704 | 705 | 706 | @view 707 | @external 708 | def expectedReturn(_strategy: address = msg.sender) -> uint256: 709 | return self._expectedReturn(_strategy) 710 | 711 | 712 | @external 713 | def report(_return: uint256) -> uint256: 714 | """ 715 | Strategies call this. 716 | _return: amount Strategy has made on it's investment since its last report, 717 | and is free to be given back to Vault as earnings 718 | returns: amount of debt outstanding (iff totalDebt > debtLimit) 719 | """ 720 | # NOTE: For approved strategies, this is the most efficient behavior. 721 | # Strategy reports back what it has free (usually in terms of ROI) 722 | # and then Vault "decides" here whether to take some back or give it more. 723 | # Note that the most it can take is `_return`, and the most it can give is 724 | # all of the remaining reserves. Anything outside of those bounds is abnormal 725 | # behavior. 726 | # NOTE: All approved strategies must have increased diligience around 727 | # calling this function, as abnormal behavior could become catastrophic 728 | 729 | # Only approved strategies can call this function 730 | assert self.strategies[msg.sender].activation > 0 731 | 732 | # Outstanding debt the Vault wants to take back from the Strategy (if any) 733 | debt: uint256 = self._debtOutstanding(msg.sender) 734 | 735 | # Issue new shares to cover fees 736 | # NOTE: In effect, this reduces overall share price by the combined fee 737 | governance_fee: uint256 = ( 738 | self._totalAssets() * (block.number - self.lastReport) * self.managementFee 739 | ) / FEE_MAX / BLOCKS_PER_YEAR 740 | self.lastReport = block.number 741 | strategist_fee: uint256 = 0 # Only applies in certain conditions 742 | 743 | # NOTE: Applies if strategy is not shutting down, or it is but all debt paid off 744 | # NOTE: No fee is taken when a strategy is unwinding it's position, until all debt is paid 745 | if _return > debt: 746 | strategist_fee = ( 747 | (_return - debt) * self.strategies[msg.sender].performanceFee 748 | ) / FEE_MAX 749 | governance_fee += (_return - debt) * self.performanceFee / FEE_MAX 750 | 751 | # NOTE: This must be called prior to taking new collateral, 752 | # or the calculation will be wrong! 753 | # NOTE: This must be done at the same time, to ensure the relative 754 | # ratio of governance_fee : strategist_fee is kept intact 755 | total_fee: uint256 = governance_fee + strategist_fee 756 | reward: uint256 = self._issueSharesForAmount(self, total_fee) 757 | 758 | # Send the rewards out as new shares in this Vault 759 | if strategist_fee > 0: 760 | strategist_reward: uint256 = (strategist_fee * reward) / total_fee 761 | self._transfer(self, Strategy(msg.sender).strategist(), strategist_reward) 762 | # NOTE: Governance earns any dust leftover from flooring math above 763 | self._transfer(self, self.rewards, self.balanceOf[self]) 764 | 765 | # Compute the line of credit the Vault is able to offer the Strategy (if any) 766 | credit: uint256 = self._creditAvailable(msg.sender) 767 | 768 | # Give/take balance to Strategy, based on the difference between the return and 769 | # the credit increase we are offering (if any) 770 | # NOTE: This is just used to adjust the balance of tokens between the Strategy and 771 | # the Vault based on the strategy's debt limit (as well as the Vault's). 772 | if _return < credit: # credit surplus, give to strategy 773 | self.token.transfer(msg.sender, credit - _return) 774 | elif _return > credit: # credit deficit, take from strategy 775 | self.token.transferFrom(msg.sender, self, _return - credit) 776 | 777 | # else, don't do anything because it is performing well as is 778 | 779 | # Update the actual debt based on the full credit we are extending to the Strategy 780 | # or the returns if we are taking funds back 781 | # NOTE: credit + self.strategies[msg.sender].totalDebt is always < self.debtLimit 782 | # NOTE: At least one of `credit` or `debt` is always 0 (both can be 0) 783 | if credit > 0: 784 | self.strategies[msg.sender].totalDebt += credit 785 | self.totalDebt += credit 786 | 787 | # Returns are always "realized gains" 788 | self.strategies[msg.sender].totalReturns += _return 789 | 790 | elif debt > 0: # We're repaying debt now, so there are no gains 791 | if _return <= debt: 792 | # Pay down our debt with profit 793 | # NOTE: Cannot return more than you borrowed 794 | self.strategies[msg.sender].totalDebt -= _return 795 | self.totalDebt -= _return 796 | debt -= _return # Debt payment complete (to report back to strategy) 797 | 798 | else: 799 | # Finish off our debt payments here 800 | self.totalDebt -= debt 801 | self.strategies[msg.sender].totalDebt -= debt 802 | 803 | # Returns are always "realized gains" (after we have paid off our debt) 804 | self.strategies[msg.sender].totalReturns += _return - debt 805 | debt = 0 # All debts paid off (to report back to strategy) 806 | 807 | elif _return > 0: # No debt to pay, nor credit to expand with, add to profit! 808 | self.strategies[msg.sender].totalReturns += _return 809 | 810 | # else, no credit/debt to manage, nor returns to report. Nothing really happened! 811 | 812 | # Update reporting time 813 | self.strategies[msg.sender].lastReport = block.number 814 | 815 | log \ 816 | StrategyReported( 817 | msg.sender, 818 | _return, 819 | credit, 820 | self.strategies[msg.sender].totalReturns, 821 | self.strategies[msg.sender].totalDebt, 822 | self.strategies[msg.sender].debtLimit, 823 | ) 824 | 825 | if self.strategies[msg.sender].totalDebt == 0 or self.emergencyShutdown: 826 | # Take every last penny the Strategy has (Emergency Exit/revokeStrategy) 827 | # NOTE: This is different than `debt` in order to extract *all* of the returns 828 | return self._balanceSheetOfStrategy(msg.sender) 829 | else: 830 | # Otherwise, just return what we have as debt outstanding 831 | return debt 832 | 833 | 834 | @internal 835 | def erc20_safe_transfer(_token: address, _to: address, _value: uint256): 836 | # HACK: Used to handle non-compliant tokens like USDT 837 | _response: Bytes[32] = raw_call( 838 | _token, 839 | concat( 840 | method_id("transfer(address,uint256)"), 841 | convert(_to, bytes32), 842 | convert(_value, bytes32) 843 | ), 844 | max_outsize=32 845 | ) 846 | if len(_response) > 0: 847 | assert convert(_response, bool), "Transfer failed!" 848 | 849 | 850 | @external 851 | def sweep(_token: address): 852 | # Can't be used to steal what this Vault is protecting 853 | assert _token != self.token.address 854 | self.erc20_safe_transfer(_token, self.governance, ERC20(_token).balanceOf(self)) 855 | -------------------------------------------------------------------------------- /tests/data/bad-indent.ndiff: -------------------------------------------------------------------------------- 1 | @external 2 | def foo(): 3 | a: uint256 = ( 4 | 1234567890 5 | + 1234567890 6 | + 1234567890 7 | + 1234567890 8 | + 1234567890 9 | + 1234567890 10 | + 1234567890 11 | - ) 12 | + ) 13 | 14 | -------------------------------------------------------------------------------- /tests/data/long-line.ndiff: -------------------------------------------------------------------------------- 1 | @external 2 | def foo(): 3 | - a: uint256 = 1234567890 + 1234567890 + 1234567890 + 1234567890 + 1234567890 + 1234567890 + 1234567890 4 | + a: uint256 = ( 5 | + 1234567890 6 | + + 1234567890 7 | + + 1234567890 8 | + + 1234567890 9 | + + 1234567890 10 | + + 1234567890 11 | + + 1234567890 12 | + ) 13 | 14 | -------------------------------------------------------------------------------- /tests/test_fixers.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import black 4 | import difflib 5 | import itertools 6 | import pytest 7 | 8 | import blackadder 9 | import vyper 10 | 11 | 12 | DATA_DIR = Path(__file__).parent / "data" 13 | # NOTE: Do this so pytest creates pretty test names 14 | TEST_CASES = list(f.name for f in DATA_DIR.glob("*.ndiff")) 15 | 16 | OUTPUT_FORMATS = ["abi", "bytecode", "bytecode_runtime"] 17 | 18 | 19 | @pytest.mark.parametrize("test_case", TEST_CASES) 20 | def test_apply_blackadder(test_case): 21 | # NOTE: Get the file from the test case name (undoing previous slimming) 22 | lines = list((DATA_DIR / test_case).read_text().splitlines()) 23 | # Construct before/after fixer applied 24 | unchanged = "\n".join(difflib.restore(lines, 1)) # Lines starting with '-' 25 | expected = "\n".join(difflib.restore(lines, 2)) # Lines starting with '+' 26 | 27 | # Fixer works as expected 28 | fixed = blackadder.format_str_override( 29 | unchanged, 30 | mode=black.Mode(), 31 | ) 32 | assert fixed == expected 33 | 34 | # Fixer doesn't change critical compiler artifacts in any way 35 | unchanged_compilation = vyper.compile_code(unchanged, output_formats=OUTPUT_FORMATS) 36 | fixed_compilation = vyper.compile_code(fixed, output_formats=OUTPUT_FORMATS) 37 | for artifact in OUTPUT_FORMATS: 38 | assert unchanged_compilation[artifact] == fixed_compilation[artifact] 39 | -------------------------------------------------------------------------------- /tests/test_vyper.py: -------------------------------------------------------------------------------- 1 | # FIXME: proper asserts, not just smoke tests 2 | 3 | import blackadder 4 | import black 5 | import pytest 6 | 7 | 8 | def test_vyper(): 9 | with open("tests/data/Vault.vy", "r") as f: 10 | src = f.read() 11 | assert blackadder.format_str_override( 12 | src, 13 | mode=black.Mode(), 14 | ) 15 | 16 | 17 | @pytest.mark.xfail(reason="Only --fast works atm") 18 | def test_vyper_stable(): # Same as above except fast = False 19 | assert blackadder.reformat_one( 20 | black.Path("data/Vault.vy"), 21 | fast=False, 22 | write_back=black.WriteBack.NO, 23 | mode=black.Mode(), 24 | report=black.Report(check=True), 25 | ) 26 | --------------------------------------------------------------------------------