From f19ce547f2a57aa953372782ef4a58c6d4dbe6e3 Mon Sep 17 00:00:00 2001 From: tserg <8017125+tserg@users.noreply.github.com> Date: Sat, 11 Feb 2023 21:26:26 +0800 Subject: [PATCH] add import pass --- examples/unused_import/erc20_fail.vy | 3 + .../unused_import/erc20_implement_pass.vy | 149 ++++++++++++++++++ examples/unused_import/erc20_use_pass.vy | 9 ++ medusa/analysis/analyse.py | 8 +- medusa/analysis/passes/__init__.py | 1 + medusa/analysis/passes/unused_import.py | 51 ++++++ tests/expectations.py | 3 + 7 files changed, 222 insertions(+), 2 deletions(-) create mode 100644 examples/unused_import/erc20_fail.vy create mode 100644 examples/unused_import/erc20_implement_pass.vy create mode 100644 examples/unused_import/erc20_use_pass.vy create mode 100644 medusa/analysis/passes/unused_import.py diff --git a/examples/unused_import/erc20_fail.vy b/examples/unused_import/erc20_fail.vy new file mode 100644 index 0000000..aa911ad --- /dev/null +++ b/examples/unused_import/erc20_fail.vy @@ -0,0 +1,3 @@ +# @version ^0.3.7 + +from vyper.interfaces import ERC20 diff --git a/examples/unused_import/erc20_implement_pass.vy b/examples/unused_import/erc20_implement_pass.vy new file mode 100644 index 0000000..a0c7c61 --- /dev/null +++ b/examples/unused_import/erc20_implement_pass.vy @@ -0,0 +1,149 @@ +# @version ^0.3.7 + +# @dev Implementation of ERC-20 token standard. +# @author Takayuki Jimba (@yudetamago) +# https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md + +from vyper.interfaces import ERC20 +from vyper.interfaces import ERC20Detailed + +implements: ERC20 +implements: ERC20Detailed + +event Transfer: + sender: indexed(address) + receiver: indexed(address) + value: uint256 + +event Approval: + owner: indexed(address) + spender: indexed(address) + value: uint256 + +name: public(String[32]) +symbol: public(String[32]) +decimals: public(uint8) + +# NOTE: By declaring `balanceOf` as public, vyper automatically generates a 'balanceOf()' getter +# method to allow access to account balances. +# The _KeyType will become a required parameter for the getter and it will return _ValueType. +# See: https://vyper.readthedocs.io/en/v0.1.0-beta.8/types.html?highlight=getter#mappings +balanceOf: public(HashMap[address, uint256]) +# By declaring `allowance` as public, vyper automatically generates the `allowance()` getter +allowance: public(HashMap[address, HashMap[address, uint256]]) +# By declaring `totalSupply` as public, we automatically create the `totalSupply()` getter +totalSupply: public(uint256) +minter: address + + +@external +def __init__(_name: String[32], _symbol: String[32], _decimals: uint8, _supply: uint256): + init_supply: uint256 = _supply * 10 ** convert(_decimals, uint256) + self.name = _name + self.symbol = _symbol + self.decimals = _decimals + self.balanceOf[msg.sender] = init_supply + self.totalSupply = init_supply + self.minter = msg.sender + log Transfer(empty(address), msg.sender, init_supply) + + + +@external +def transfer(_to : address, _value : uint256) -> bool: + """ + @dev Transfer token for a specified address + @param _to The address to transfer to. + @param _value The amount to be transferred. + """ + # NOTE: vyper does not allow underflows + # so the following subtraction would revert on insufficient balance + self.balanceOf[msg.sender] -= _value + self.balanceOf[_to] += _value + log Transfer(msg.sender, _to, _value) + return True + + +@external +def transferFrom(_from : address, _to : address, _value : uint256) -> bool: + """ + @dev Transfer tokens from one address to another. + @param _from address The address which you want to send tokens from + @param _to address The address which you want to transfer to + @param _value uint256 the amount of tokens to be transferred + """ + # NOTE: vyper does not allow underflows + # so the following subtraction would revert on insufficient balance + self.balanceOf[_from] -= _value + self.balanceOf[_to] += _value + # NOTE: vyper does not allow underflows + # so the following subtraction would revert on insufficient allowance + self.allowance[_from][msg.sender] -= _value + log Transfer(_from, _to, _value) + return True + + +@external +def approve(_spender : address, _value : uint256) -> bool: + """ + @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender. + Beware that changing an allowance with this method brings the risk that someone may use both the old + and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this + race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards: + https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + @param _spender The address which will spend the funds. + @param _value The amount of tokens to be spent. + """ + self.allowance[msg.sender][_spender] = _value + log Approval(msg.sender, _spender, _value) + return True + + +@external +def mint(_to: address, _value: uint256): + """ + @dev Mint an amount of the token and assigns it to an account. + This encapsulates the modification of balances such that the + proper events are emitted. + @param _to The account that will receive the created tokens. + @param _value The amount that will be created. + """ + assert msg.sender == self.minter + assert _to != empty(address) + self.totalSupply += _value + self.balanceOf[_to] += _value + log Transfer(empty(address), _to, _value) + + +@internal +def _burn(_to: address, _value: uint256): + """ + @dev Internal function that burns an amount of the token of a given + account. + @param _to The account whose tokens will be burned. + @param _value The amount that will be burned. + """ + assert _to != empty(address) + self.totalSupply -= _value + self.balanceOf[_to] -= _value + log Transfer(_to, empty(address), _value) + + +@external +def burn(_value: uint256): + """ + @dev Burn an amount of the token of msg.sender. + @param _value The amount that will be burned. + """ + self._burn(msg.sender, _value) + + +@external +def burnFrom(_to: address, _value: uint256): + """ + @dev Burn an amount of the token from a given account. + @param _to The account whose tokens will be burned. + @param _value The amount that will be burned. + """ + self.allowance[_to][msg.sender] -= _value + self._burn(_to, _value) diff --git a/examples/unused_import/erc20_use_pass.vy b/examples/unused_import/erc20_use_pass.vy new file mode 100644 index 0000000..f5ca7d7 --- /dev/null +++ b/examples/unused_import/erc20_use_pass.vy @@ -0,0 +1,9 @@ +# @version ^0.3.7 + +from vyper.interfaces import ERC20 + + +@external +def foo(a: address) -> uint256: + x: uint256 = ERC20(a).balanceOf(a) + return x diff --git a/medusa/analysis/analyse.py b/medusa/analysis/analyse.py index 66af8af..aa3361e 100644 --- a/medusa/analysis/analyse.py +++ b/medusa/analysis/analyse.py @@ -1,9 +1,13 @@ from vyper import ast as vy_ast from medusa.analysis.base import BaseAnalyser -from medusa.analysis.passes import DeadStoreAnalyser, UnusedParamAnalyser +from medusa.analysis.passes import DeadStoreAnalyser, UnusedImportAnalyser, UnusedParamAnalyser -PASSES = {"Dead Store": DeadStoreAnalyser, "Unused Function Parameter": UnusedParamAnalyser} +PASSES = { + "Dead Store": DeadStoreAnalyser, + "Unused Import": UnusedImportAnalyser, + "Unused Function Parameter": UnusedParamAnalyser, +} def analyse(ast: vy_ast.Module) -> dict[BaseAnalyser, set[vy_ast.VyperNode]]: diff --git a/medusa/analysis/passes/__init__.py b/medusa/analysis/passes/__init__.py index 9df519b..12652f0 100644 --- a/medusa/analysis/passes/__init__.py +++ b/medusa/analysis/passes/__init__.py @@ -1,2 +1,3 @@ from medusa.analysis.passes.dead_store import DeadStoreAnalyser +from medusa.analysis.passes.unused_import import UnusedImportAnalyser from medusa.analysis.passes.unused_param import UnusedParamAnalyser diff --git a/medusa/analysis/passes/unused_import.py b/medusa/analysis/passes/unused_import.py new file mode 100644 index 0000000..c2f25af --- /dev/null +++ b/medusa/analysis/passes/unused_import.py @@ -0,0 +1,51 @@ +from vyper import ast as vy_ast + +from medusa.analysis.base import BaseAnalyser + + +class UnusedImportAnalyser(BaseAnalyser): + """ + Check for unused imports. + + Example: + ``` + from vyper.interfaces import ERC20 + ``` + + `ERC20` is never used. + + This pass does not work for custom interfaces yet. + """ + + _id = "Unused Import" + + def analyse(self, ast: vy_ast.Module, analysis: dict[BaseAnalyser, set[vy_ast.VyperNode]]): + + import_nodes = ast.get_descendants((vy_ast.Import, vy_ast.ImportFrom)) + + for n in import_nodes: + if isinstance(n, vy_ast.Import): + interface_name = n.alias + elif isinstance(n, vy_ast.ImportFrom): + interface_name = n.name + + # Check for `implements` + is_implemented = ( + len( + ast.get_descendants( + vy_ast.AnnAssign, + {"target.id": "implements", "annotation.id": interface_name}, + ) + ) + > 0 + ) + + # Check for usage as `Interface(address).function()` + is_used = len(ast.get_descendants(vy_ast.Call, {"func.id": interface_name})) > 0 + + if not is_implemented and not is_used: + temp = analysis.get(self, set()) + temp.add(n) + analysis[self] = temp + + return analysis diff --git a/tests/expectations.py b/tests/expectations.py index df79f3f..16c3ed0 100644 --- a/tests/expectations.py +++ b/tests/expectations.py @@ -8,4 +8,7 @@ ("unused_param/double_alt", {"Unused Function Parameter": 1}), ("unused_param/namespace_single", {"Unused Function Parameter": 1}), ("unused_param/nested_namespace_single", {"Unused Function Parameter": 1}), + ("unused_import/erc20_implement_pass", {}), + ("unused_import/erc20_use_pass", {}), + ("unused_import/erc20_fail", {"Unused Import": 1}), ]