diff --git a/.gitignore b/.gitignore index 89b1e920f0..56215b8493 100644 --- a/.gitignore +++ b/.gitignore @@ -49,4 +49,4 @@ vyper/vyper_git_commithash.txt *.spec # mac -.DS_Store \ No newline at end of file +.DS_Store diff --git a/docs/compiling-a-contract.rst b/docs/compiling-a-contract.rst index 3e0e48213f..751af980b2 100644 --- a/docs/compiling-a-contract.rst +++ b/docs/compiling-a-contract.rst @@ -6,17 +6,19 @@ Command-Line Compiler Tools Vyper includes the following command-line scripts for compiling contracts: -* ``vyper``: Compiles vyper contract files into ``IR`` or bytecode +* ``vyper``: Compiles vyper contract or archive files * ``vyper-json``: Provides a JSON interface to the compiler .. note:: The ``--help`` flag gives verbose explanations of how to use each of these scripts. +.. _vyper-cli-command: + vyper ----- -``vyper`` provides command-line access to the compiler. It can generate various outputs including simple binaries, ASTs, interfaces and source mappings. +``vyper`` provides CLI access to the compiler. It can generate various outputs including simple binaries, ASTs, interfaces and source mappings. To compile a contract: @@ -29,7 +31,7 @@ Include the ``-f`` flag to specify which output formats to return. Use ``vyper - .. code:: shell - $ vyper -f abi,abi_python,bytecode,bytecode_runtime,interface,external_interface,ast,annotated_ast,ir,ir_json,ir_runtime,hex-ir,asm,opcodes,opcodes_runtime,source_map,method_identifiers,userdoc,devdoc,metadata,combined_json,layout yourFileName.vy + $ vyper -f abi,abi_python,bytecode,bytecode_runtime,blueprint_bytecode,interface,external_interface,ast,annotated_ast,integrity,ir,ir_json,ir_runtime,asm,opcodes,opcodes_runtime,source_map,source_map_runtime,archive,solc_json,method_identifiers,userdoc,devdoc,metadata,combined_json,layout yourFileName.vy .. note:: The ``opcodes`` and ``opcodes_runtime`` output of the compiler has been returning incorrect opcodes since ``0.2.0`` due to a lack of 0 padding (patched via `PR 3735 `_). If you rely on these functions for debugging, please use the latest patched versions. @@ -95,7 +97,6 @@ Importing Interfaces 1. Interfaces defined in the ``interfaces`` field of the input JSON. 2. Derived interfaces generated from contracts in the ``sources`` field of the input JSON. -3. (Optional) The local filesystem, if a root path was explicitly declared via the ``-p`` flag. See :ref:`searching_for_imports` for more information on Vyper's import system. @@ -121,7 +122,7 @@ Remix IDE Compiler Optimization Modes =========================== -The vyper CLI tool accepts an optimization mode ``"none"``, ``"codesize"``, or ``"gas"`` (default). It can be set using the ``--optimize`` flag. For example, invoking ``vyper --optimize codesize MyContract.vy`` will compile the contract, optimizing for code size. As a rough summary of the differences between gas and codesize mode, in gas optimized mode, the compiler will try to generate bytecode which minimizes gas (up to a point), including: +The Vyper CLI tool accepts an optimization mode ``"none"``, ``"codesize"``, or ``"gas"`` (default). It can be set using the ``--optimize`` flag. For example, invoking ``vyper --optimize codesize MyContract.vy`` will compile the contract, optimizing for code size. As a rough summary of the differences between gas and codesize mode, in gas optimized mode, the compiler will try to generate bytecode which minimizes gas (up to a point), including: * using a sparse selector table which optimizes for gas over codesize * inlining some constants, and @@ -192,11 +193,50 @@ The following is a list of supported EVM versions, and changes in the compiler i - Functions marked with ``@nonreentrant`` are protected with TLOAD/TSTORE instead of SLOAD/SSTORE - The ``MCOPY`` opcode will be generated automatically by the compiler for most memory operations. +.. _integrity-hash: + +Integrity Hash +============== + +To help tooling detect whether two builds are the same, Vyper provides the ``-f integrity`` output, which outputs the integrity hash of a contract. The integrity hash is recursively defined as the sha256 of the source code with the integrity hashes of its dependencies (imports). + +.. _vyper-archives: + +Vyper Archives +============== + +A Vyper archive is a compileable bundle of input sources and settings. Technically, it is a `ZIP file `_, with a special structure to make it useable as input to the compiler. It can use any suffix, but the convention is to use a ``.zip`` suffix or ``.vyz`` suffix. It must contain a ``MANIFEST/`` folder, with the following directory structure. + +:: + + MANIFEST + ├── cli_settings.txt + ├── compilation_targets + ├── compiler_version + ├── integrity + ├── searchpaths + └── settings.json + +* ``cli_settings.txt`` is a text representation of the settings that were used on the compilation run that generated this archive. +* ``compilation_targets`` is a newline separated list of compilation targets. Currently only one compilation is supported +* ``compiler_version`` is a text representation of the compiler version used to generate this archive +* ``integrity`` is the :ref:`integrity hash ` of the input contract +* ``searchpaths`` is a newline-separated list of the search paths used on this compilation run +* ``settings.json`` is a json representation of the settings used on this compilation run. It is 1:1 with ``cli_settings.txt``, but both are provided as they are convenient for different workflows (typically, manually vs automated). + +A Vyper archive file can be produced by requesting the ``-f archive`` output format. The compiler can also produce the archive in base64 encoded form using the ``--base64`` flag. The Vyper compiler can accept both ``.vyz`` and base64-encoded Vyper archives directly as input. + +.. code-block:: bash + + $ vyper -f archive my_contract.vy -o my_contract.vyz # write the archive to my_contract.vyz + $ vyper -f archive my_contract.vy --base64 > my_contract.vyz.b64 # write the archive, as base64-encoded text + $ vyper my_contract.vyz # compile my_contract.vyz + $ vyper my_contract.vyz.b64 # compile my_contract.vyz.b64 Compiler Input and Output JSON Description ========================================== -Especially when dealing with complex or automated setups, the recommended way to compile is to use :ref:`vyper-json` and the JSON-input-output interface. +JSON input/output is provided for compatibility with solidity, however, the recommended way is to use the aforementioned :ref:`Vyper archives `. So-called "standard json" input can be generated from a contract using the ``vyper -f solc_json`` output format. Where possible, the Vyper JSON compiler formats follow those of `Solidity `_. @@ -205,7 +245,7 @@ Where possible, the Vyper JSON compiler formats follow those of `Solidity `. Functions may accept input arguments and return variables in order to pass values between them. -.. _function-visibility: - Visibility ---------- -All functions must include exactly one visibility decorator. +.. _function-visibility: + +You can optionally declare a function's visibility by using a :ref:`decorator `. There are three visibility levels in Vyper: + + * ``@external``: exposed in the selector table, can be called by an external call into this contract + * ``@internal`` (default): can be invoked only from within this contract. Not available to external callers + * ``@deploy``: constructor code. This is code which is invoked once in the lifetime of a contract, upon its deploy. It is not available at runtime to either external callers or internal call invocations. At this time, only the :ref:`__init__() function ` may be marked as ``@deploy``. + External Functions ****************** @@ -50,20 +55,50 @@ A Vyper contract cannot call directly between two external functions. If you mus Internal Functions ****************** -Internal functions (marked with the ``@internal`` decorator) are only accessible from other functions within the same contract. They are called via the :ref:`self` object: +Internal functions (optionally marked with the ``@internal`` decorator) are only accessible from other functions within the same contract. They are invoked via the :ref:`self` object: .. code-block:: vyper - @internal - def _times_two(amount: uint256, two: uint256 = 2) -> uint256: - return amount * two + def _times_two(amount: uint256) -> uint256: + return amount * 2 @external def calculate(amount: uint256) -> uint256: return self._times_two(amount) +Or for internal functions which are defined in :ref:`imported modules `, they are invoked by prefixing the name of the module to the function name: + +.. code-block:: vyper + import calculator_library + + @external + def calculate(amount: uint256) -> uint256: + return calculator_library._times_two(amount) + +.. note:: + As of v0.4.0, the ``@internal`` decorator is optional. That is, functions with no visibility decorator default to being ``internal``. + .. note:: - Since calling an ``internal`` function is realized by jumping to its entry label, the internal function dispatcher ensures the correctness of the jumps. Please note that for ``internal`` functions which use more than one default parameter, Vyper versions ``>=0.3.8`` are strongly recommended due to the security advisory `GHSA-ph9x-4vc9-m39g `_. + Please note that for ``internal`` functions which use more than one default parameter, Vyper versions ``>=0.3.8`` are recommended due to the security advisory `GHSA-ph9x-4vc9-m39g `_. + + +The ``__init__`` Function +------------------------- + +.. _init-function: + +The ``__init__()`` function, also known as the constructor, is a special initialization function that is only called at the time of deploying a contract. It can be used to set initial values for storage or immutable variables. It must be declared with the ``@deploy`` decorator. A common use case is to set an ``owner`` variable with the creator of the contract: + +.. code-block:: vyper + + owner: address + + @deploy + def __init__(): + self.owner = msg.sender + +Additionally, :ref:`immutable variables ` may only be set within the constructor. + Mutability ---------- @@ -72,10 +107,10 @@ Mutability You can optionally declare a function's mutability by using a :ref:`decorator `. There are four mutability levels: - * **Pure**: does not read from the contract state or any environment variables. - * **View**: may read from the contract state, but does not alter it. - * **Nonpayable**: may read from and write to the contract state, but cannot receive Ether. - * **Payable**: may read from and write to the contract state, and can receive Ether. + * ``@pure``: does not read from the contract state or any environment variables. + * ``@view``: may read from the contract state, but does not alter it. + * ``@nonpayable`` (default): may read from and write to the contract state, but cannot receive Ether. + * ``@payable``: may read from and write to the contract state, and can receive Ether. .. code-block:: vyper @@ -151,7 +186,7 @@ If the function is annotated as ``@payable``, this function is executed whenever Considerations ************** -Just as in Solidity, Vyper generates a default function if one isn't found, in the form of a ``REVERT`` call. Note that this still `generates an exception `_ and thus will not succeed in receiving funds. +Just as in Solidity, Vyper generates a default function if one isn't found, in the form of a ``REVERT`` call. Note that this rolls back state changes, and thus will not succeed in receiving funds. Ethereum specifies that the operations will be rolled back if the contract runs out of gas in execution. ``send`` calls to the contract come with a free stipend of 2300 gas, which does not leave much room to perform other operations except basic logging. **However**, if the sender includes a higher gas amount through a ``call`` instead of ``send``, then more complex functionality can be run. @@ -168,33 +203,17 @@ Lastly, although the default function receives no arguments, it can still access * the amount of ETH sent (``msg.value``) * the gas provided (``msg.gas``). -The ``__init__`` Function -------------------------- - -``__init__`` is a special initialization function that may only be called at the time of deploying a contract. It can be used to set initial values for storage variables. A common use case is to set an ``owner`` variable with the creator the contract: - -.. code-block:: vyper - - owner: address - - @external - def __init__(): - self.owner = msg.sender - -You cannot call to other contract functions from the initialization function. - .. _function-decorators: Decorators Reference -------------------- -All functions must include one :ref:`visibility ` decorator (``@external`` or ``@internal``). The remaining decorators are optional. - =============================== =========================================================== Decorator Description =============================== =========================================================== -``@external`` Function can only be called externally +``@external`` Function can only be called externally, it is part of the runtime selector table ``@internal`` Function can only be called within current contract +``@deploy`` Function is called only at deploy time ``@pure`` Function does not read contract state or environment variables ``@view`` Function does not alter contract state ``@payable`` Function is able to receive Ether @@ -233,7 +252,7 @@ The ``for`` statement is a control flow construct used to iterate over a value: .. code-block:: vyper - for i in : + for i: in : ... The iterated value can be a static array, a dynamic array, or generated from the built-in ``range`` function. @@ -246,16 +265,16 @@ You can use ``for`` to iterate through the values of any array variable: .. code-block:: vyper foo: int128[3] = [4, 23, 42] - for i in foo: + for i: int128 in foo: ... In the above, example, the loop executes three times with ``i`` assigned the values of ``4``, ``23``, and then ``42``. -You can also iterate over a literal array, as long as a common type can be determined for each item in the array: +You can also iterate over a literal array, as long as the annotated type is valid for each item in the array: .. code-block:: vyper - for i in [4, 23, 42]: + for i: int128 in [4, 23, 42]: ... Some restrictions: @@ -270,32 +289,32 @@ Ranges are created using the ``range`` function. The following examples are vali .. code-block:: vyper - for i in range(STOP): + for i: uint256 in range(STOP): ... -``STOP`` is a literal integer greater than zero. ``i`` begins as zero and increments by one until it is equal to ``STOP``. +``STOP`` is a literal integer greater than zero. ``i`` begins as zero and increments by one until it is equal to ``STOP``. ``i`` must be of the same type as ``STOP``. .. code-block:: vyper - for i in range(stop, bound=N): + for i: uint256 in range(stop, bound=N): ... -Here, ``stop`` can be a variable with integer type, greater than zero. ``N`` must be a compile-time constant. ``i`` begins as zero and increments by one until it is equal to ``stop``. If ``stop`` is larger than ``N``, execution will revert at runtime. In certain cases, you may not have a guarantee that ``stop`` is less than ``N``, but still want to avoid the possibility of runtime reversion. To accomplish this, use the ``bound=`` keyword in combination with ``min(stop, N)`` as the argument to ``range``, like ``range(min(stop, N), bound=N)``. This is helpful for use cases like chunking up operations on larger arrays across multiple transactions. +Here, ``stop`` can be a variable with integer type, greater than zero. ``N`` must be a compile-time constant. ``i`` begins as zero and increments by one until it is equal to ``stop``. If ``stop`` is larger than ``N``, execution will revert at runtime. In certain cases, you may not have a guarantee that ``stop`` is less than ``N``, but still want to avoid the possibility of runtime reversion. To accomplish this, use the ``bound=`` keyword in combination with ``min(stop, N)`` as the argument to ``range``, like ``range(min(stop, N), bound=N)``. This is helpful for use cases like chunking up operations on larger arrays across multiple transactions. ``i``, ``stop`` and ``N`` must be of the same type. Another use of range can be with ``START`` and ``STOP`` bounds. .. code-block:: vyper - for i in range(START, STOP): + for i: uint256 in range(START, STOP): ... -Here, ``START`` and ``STOP`` are literal integers, with ``STOP`` being a greater value than ``START``. ``i`` begins as ``START`` and increments by one until it is equal to ``STOP``. +Here, ``START`` and ``STOP`` are literal integers, with ``STOP`` being a greater value than ``START``. ``i`` begins as ``START`` and increments by one until it is equal to ``STOP``. ``i``, ``START`` and ``STOP`` must be of the same type. Finally, it is possible to use ``range`` with runtime `start` and `stop` values as long as a constant `bound` value is provided. In this case, Vyper checks at runtime that `end - start <= bound`. -``N`` must be a compile-time constant. +``N`` must be a compile-time constant. ``i``, ``stop`` and ``N`` must be of the same type. .. code-block:: vyper - for i in range(start, end, bound=N): + for i: uint256 in range(start, end, bound=N): ... diff --git a/docs/deploying-contracts.rst b/docs/deploying-contracts.rst index d4fa4cec3f..4954ea8cec 100644 --- a/docs/deploying-contracts.rst +++ b/docs/deploying-contracts.rst @@ -21,7 +21,16 @@ Once you are ready to deploy your contract to a public test net or the main net, vyper -f abi yourFileName.vy # returns ABI -* Use the remote compiler provided by the `Remix IDE `_ to compile and deploy your contract on your net of choice. Remix also provides a JavaScript VM to test deploy your contract. +* Use `Titanoboa `_: -.. note:: - While the vyper version of the Remix IDE compiler is updated on a regular basis it might be a bit behind the latest version found in the master branch of the repository. Make sure the byte code matches the output from your local compiler. +.. code-block:: python + + import boa + boa.set_network_env() + from eth_account import Account + # in a real codebase, always load private keys safely from an encrypted store! + boa.env.add_account(Account()) + deployer = boa.load_partial("yourFileName.vy") + deployer.deploy() + +* Use the development environment provided at https://try.vyperlang.org to compile and deploy your contract on your net of choice. try.vyperlang.org comes "batteries-included", with Titanoboa pre-installed, and browser signer integration as well. diff --git a/docs/index.rst b/docs/index.rst index a7b0fbb4f2..5baaebb339 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -23,7 +23,7 @@ Because of this Vyper provides the following features: * **Strong typing** * **Clean and understandable compiler code** * **Support for pure functions**: Anything marked ``pure`` is not allowed to change the state. -* **Code reuse through composition**: Vyper supports code reuse through composition, and to help auditors, requires syntactic marking of dependencies which potentially modify state. +* **Code reuse through composition**: Vyper supports code reuse through composition, and requires syntactic marking of dependencies which potentially modify state. Following the principles and goals, Vyper **does not** provide the following features: @@ -35,5 +35,3 @@ Following the principles and goals, Vyper **does not** provide the following fea * **Recursive calling**: Recursive calling makes it impossible to set an upper bound on gas limits, opening the door for gas limit attacks. * **Infinite-length loops**: Similar to recursive calling, infinite-length loops make it impossible to set an upper bound on gas limits, opening the door for gas limit attacks. * **Binary fixed point**: Decimal fixed point is better, because any decimal fixed point value written as a literal in code has an exact representation, whereas with binary fixed point approximations are often required (e.g. (0.2)\ :sub:`10` = (0.001100110011...)\ :sub:`2`, which needs to be truncated), leading to unintuitive results, e.g. in Python 0.3 + 0.3 + 0.3 + 0.1 != 1. - -Vyper **does not** strive to be a 100% replacement for everything that can be done in Solidity; it will deliberately forbid things or make things harder if it deems fit to do so for the goal of increasing security. diff --git a/docs/interfaces.rst b/docs/interfaces.rst index b22facf030..acc0ce91f3 100644 --- a/docs/interfaces.rst +++ b/docs/interfaces.rst @@ -24,7 +24,11 @@ The defined interface can then be used to make external calls, given a contract @external def test(foobar: FooBar): - foobar.calculate() + extcall foobar.test1() + + @external + def test2(foobar: FooBar) -> uint256: + return staticcall foobar.calculate() The interface name can also be used as a type annotation for storage variables. You then assign an address value to the variable to access that interface. Note that casting an address to an interface is possible, e.g. ``FooBar()``: @@ -32,15 +36,21 @@ The interface name can also be used as a type annotation for storage variables. foobar_contract: FooBar - @external + @deploy def __init__(foobar_address: address): self.foobar_contract = FooBar(foobar_address) @external def test(): - self.foobar_contract.calculate() + extcall self.foobar_contract.test1() + +Specifying ``payable`` or ``nonpayable`` annotation in the interface indicates that the call made to the external contract will be able to alter storage, whereas ``view`` and ``pure`` calls will use a ``STATICCALL`` ensuring no storage can be altered during execution. Additionally, ``payable`` allows non-zero value to be sent along with the call. -Specifying ``payable`` or ``nonpayable`` annotation indicates that the call made to the external contract will be able to alter storage, whereas the ``view`` ``pure`` call will use a ``STATICCALL`` ensuring no storage can be altered during execution. Additionally, ``payable`` allows non-zero value to be sent along with the call. +Either the ``extcall`` or ``staticcall`` keyword is required to precede the external call to distinguish it from internal calls. The keyword must match the visibility of the function, ``staticcall`` for ``pure`` and ``view`` functions, and ``extcall`` for ``payable`` and ``nonpayable`` functions. Additionally, the output of a ``staticcall`` must be assigned to a result. + +.. warning:: + + If the signature in an interface does not match the actual signature of the called contract, you can get runtime errors or undefined behavior. For instance, if you accidentally mark a ``nonpayable`` function as ``view``, calling that function may result in the EVM reverting execution in the called contract. .. code-block:: vyper @@ -52,10 +62,10 @@ Specifying ``payable`` or ``nonpayable`` annotation indicates that the call made @external def test(foobar: FooBar): - foobar.calculate() # cannot change storage - foobar.query() # cannot change storage, but reads itself - foobar.update() # storage can be altered - foobar.pay(value=1) # storage can be altered, and value can be sent + s: uint256 = staticcall foobar.calculate() # cannot change storage + s = staticcall foobar.query() # cannot change storage, but reads itself + extcall foobar.update() # storage can be altered + extcall foobar.pay(value=1) # storage can be altered, and value can be sent Vyper offers the option to set the following additional keyword arguments when making external calls: @@ -72,95 +82,17 @@ The ``default_return_value`` parameter can be used to handle ERC20 tokens affect .. code-block:: vyper - IERC20(USDT).transfer(msg.sender, 1, default_return_value=True) # returns True - IERC20(USDT).transfer(msg.sender, 1) # reverts because nothing returned + extcall IERC20(USDT).transfer(msg.sender, 1, default_return_value=True) # returns True + extcall IERC20(USDT).transfer(msg.sender, 1) # reverts because nothing returned .. warning:: When ``skip_contract_check=True`` is used and the called function returns data (ex.: ``x: uint256 = SomeContract.foo(skip_contract_check=True)``, no guarantees are provided by the compiler as to the validity of the returned value. In other words, it is undefined behavior what happens if the called contract did not exist. In particular, the returned value might point to garbage memory. It is therefore recommended to only use ``skip_contract_check=True`` to call contracts which have been manually ensured to exist at the time of the call. -Importing Interfaces -==================== - -Interfaces are imported with ``import`` or ``from ... import`` statements. - -Imported interfaces are written using standard Vyper syntax. The body of each function is ignored when the interface is imported. If you are defining a standalone interface, it is normally specified by using a ``pass`` statement: - -.. code-block:: vyper - - @external - def test1(): - pass - - @external - def calculate() -> uint256: - pass - -You can also import a fully implemented contract and Vyper will automatically convert it to an interface. It is even possible for a contract to import itself to gain access to its own interface. - -.. code-block:: vyper - - import greeter as Greeter - - name: public(String[10]) - - @external - def __init__(_name: String[10]): - self.name = _name - - @view - @external - def greet() -> String[16]: - return concat("Hello ", Greeter(msg.sender).name()) - -Imports via ``import`` ----------------------- - -With absolute ``import`` statements, you **must** include an alias as a name for the imported package. In the following example, failing to include ``as Foo`` will raise a compile error: - -.. code-block:: vyper - - import contract.foo as Foo - -Imports via ``from ... import`` -------------------------------- - -Using ``from`` you can perform both absolute and relative imports. You may optionally include an alias - if you do not, the name of the interface will be the same as the file. - -.. code-block:: vyper - - # without an alias - from contract import foo - - # with an alias - from contract import foo as Foo - -Relative imports are possible by prepending dots to the contract name. A single leading dot indicates a relative import starting with the current package. Two leading dots indicate a relative import from the parent of the current package: - -.. code-block:: vyper - - from . import foo - from ..interfaces import baz - -.. _searching_for_imports: - -Searching For Interface Files ------------------------------ - -When looking for a file to import, Vyper will first search relative to the same folder as the contract being compiled. For absolute imports, it also searches relative to the root path for the project. Vyper checks for the file name with a ``.vy`` suffix first, then ``.json``. - -When using the command line compiler, the root path defaults to the current working directory. You can change it with the ``-p`` flag: - -:: - - $ vyper my_project/contracts/my_contract.vy -p my_project - -In the above example, the ``my_project`` folder is set as the root path. A contract cannot perform a relative import that goes beyond the top-level folder. - Built-in Interfaces =================== -Vyper includes common built-in interfaces such as `ERC20 `_ and `ERC721 `_. These are imported from ``ethereum.ercs``: +Vyper includes common built-in interfaces such as `IERC20 `_ and `IERC721 `_. These are imported from ``ethereum.ercs``: .. code-block:: vyper @@ -182,7 +114,7 @@ You can define an interface for your contract with the ``implements`` statement: implements: FooBarInterface -This imports the defined interface from the vyper file at ``an_interface.vy`` (or ``an_interface.json`` if using ABI json interface type) and ensures your current contract implements all the necessary external functions. If any interface functions are not included in the contract, it will fail to compile. This is especially useful when developing contracts around well-defined standards such as ERC20. +This imports the defined interface from the vyper file at ``an_interface.vyi`` (or ``an_interface.json`` if using ABI json interface type) and ensures your current contract implements all the necessary external functions. If any interface functions are not included in the contract, it will fail to compile. This is especially useful when developing contracts around well-defined standards such as ERC20. .. note:: @@ -192,11 +124,26 @@ This imports the defined interface from the vyper file at ``an_interface.vy`` (o Prior to v0.4.0, ``implements`` required that events defined in an interface were re-defined in the "implementing" contract. As of v0.4.0, this is no longer required because events can be used just by importing them. Any events used in a contract will automatically be exported in the ABI output. +Standalone Interfaces +===================== + +Standalone interfaces are written using a variant of standard Vyper syntax. The body of each function must be an ellipsis (``...``). Interface files must have a ``.vyi`` suffix in order to be found by an import statement. + +.. code-block:: vyper + # ISomeInterface.vyi + + @external + def test1(): + ... + + @external + def calculate() -> uint256: + ... Extracting Interfaces ===================== -Vyper has a built-in format option to allow you to make your own Vyper interfaces easily. +Vyper has a built-in format option to allow you to easily export a Vyper interface from a pre-existing contract. :: @@ -207,11 +154,11 @@ Vyper has a built-in format option to allow you to make your own Vyper interface @view @external def delegated(addr: address) -> bool: - pass + ... # ... -If you want to do an external call to another contract, Vyper provides an external interface extract utility as well. +If you want to export it as an inline interface, Vyper provides a utility to extract that as well. :: @@ -225,4 +172,4 @@ If you want to do an external call to another contract, Vyper provides an extern def forwardWeight(delegate_with_weight_to_forward: address): nonpayable # ... -The output can then easily be copy-pasted to be consumed. +The output can then easily be copy-pasted directly in a regular vyper file. diff --git a/docs/resources.rst b/docs/resources.rst index c2b0e3e427..977df2b3eb 100644 --- a/docs/resources.rst +++ b/docs/resources.rst @@ -18,7 +18,7 @@ General Frameworks and tooling ---------------------- -- `Titanoboa – An experimental Vyper interpreter with pretty tracebacks, forking, debugging features and more `_ +- `Titanoboa – A Vyper interpreter with pretty tracebacks, forking, debugging features and more `_ - `ApeWorX – The Ethereum development framework for Python Developers, Data Scientists, and Security Professionals `_ - `VyperDeployer – A helper smart contract to compile and test Vyper contracts in Foundry `_ - `🐍 snekmate – Vyper smart contract building blocks `_ diff --git a/docs/scoping-and-declarations.rst b/docs/scoping-and-declarations.rst index 838720c25b..9b59cb8c4f 100644 --- a/docs/scoping-and-declarations.rst +++ b/docs/scoping-and-declarations.rst @@ -33,6 +33,8 @@ The compiler automatically creates getter functions for all public storage varia For public arrays, you can only retrieve a single element via the generated getter. This mechanism exists to avoid high gas costs when returning an entire array. The getter will accept an argument to specify which element to return, for example ``data(0)``. +.. _immutable-variables: + Declaring Immutable Variables ----------------------------- @@ -42,7 +44,7 @@ Variables can be marked as ``immutable`` during declaration: DATA: immutable(uint256) - @external + @deploy def __init__(_data: uint256): DATA = _data @@ -123,8 +125,6 @@ Module Scope Variables and other items declared outside of a code block (functions, constants, event and struct definitions, ...), are visible even before they were declared. This means you can use module-scoped items before they are declared. -An exception to this rule is that you can only call functions that have already been declared. - Accessing Module Scope from Functions ************************************* @@ -161,9 +161,10 @@ It is not permitted for a memory or calldata variable to shadow the name of an i a: immutable(bool) - @external + @deploy def __init__(): a = True + @external def foo(a:bool) -> bool: # input argument cannot have the same name as a constant or immutable variable @@ -230,7 +231,7 @@ In a ``for`` statement, the target variable exists within the scope of the loop. @external def foo(a: bool) -> int128: - for i in [1, 2, 3]: + for i: int128 in [1, 2, 3]: pass i: bool = False @@ -240,6 +241,6 @@ The following contract fails to compile because ``a`` has not been declared outs @external def foo(a: bool) -> int128: - for i in [1, 2, 3]: + for i: int128 in [1, 2, 3]: a: int128 = i a += 3 diff --git a/docs/statements.rst b/docs/statements.rst index 34f15828a1..801eb36ae5 100644 --- a/docs/statements.rst +++ b/docs/statements.rst @@ -15,7 +15,7 @@ The ``break`` statement terminates the nearest enclosing ``for`` loop. .. code-block:: vyper - for i in [1, 2, 3, 4, 5]: + for i: uint256 in [1, 2, 3, 4, 5]: if i == a: break @@ -28,7 +28,7 @@ The ``continue`` statement begins the next cycle of the nearest enclosing ``for` .. code-block:: vyper - for i in [1, 2, 3, 4, 5]: + for i: uint256 in [1, 2, 3, 4, 5]: if i != a: continue ... diff --git a/docs/structure-of-a-contract.rst b/docs/structure-of-a-contract.rst index 561f3000dd..fc817cf4b6 100644 --- a/docs/structure-of-a-contract.rst +++ b/docs/structure-of-a-contract.rst @@ -54,6 +54,72 @@ EVM Version The EVM version can be set with the ``evm-version`` pragma, which is documented in :ref:`evm-version`. +Imports +======= + +Import statements allow you to import :ref:`modules` or :ref:`interfaces` with the ``import`` or ``from ... import`` syntax. + +Imports via ``import`` +---------------------- + +You may import modules (defined in ``.vy`` files) and interfaces (defined in ``.vyi`` or ``.json`` files) via ``import`` statements. You may use plain or ``as`` variants. + +.. code-block:: vyper + + # without an alias + import foo + + # with an alias + import my_package.foo as bar + +Imports via ``from ... import`` +------------------------------- + +Using ``from`` you can perform both absolute and relative imports. You may optionally include an alias - if you do not, the name of the interface will be the same as the file. + +.. code-block:: vyper + + # without an alias + from my_package import foo + + # with an alias + from my_package import foo as bar + +Relative imports are possible by prepending dots to the contract name. A single leading dot indicates a relative import starting with the current package. Two leading dots indicate a relative import from the parent of the current package: + +.. code-block:: vyper + + from . import foo + from ..interfaces import baz + +Further higher directories can be accessed with ``...``, ``....`` etc., as in Python. + +.. _searching_for_imports: + +Searching For Imports +----------------------------- + +When looking for a file to import, Vyper will first search relative to the same folder as the contract being compiled. It then checks for the file in the provided search paths, in the precedence provided. Vyper checks for the file name with a ``.vy`` suffix first, then ``.vyi``, then ``.json``. + +When using the :ref:`vyper CLI `, the search path defaults to the current working directory, plus the python `syspath `_. You can append to the search path with the ``-p`` flag, e.g.: + +:: + + $ vyper my_project/contracts/my_contract.vy -p ../path/to/other_project + +In the above example, the ``my_project`` folder is set as the root path. + +.. note:: + + Including the python syspath on the search path means that any Vyper module in the current ``virtualenv`` is discoverable by the Vyper compiler, and Vyper packages can be published to and installed from PyPI and accessed via ``import`` statements with no additional configuration. Keep in mind that best practice is always to install packages *within* a ``virtualenv`` and not globally! + +You can additionally disable the behavior of adding the syspath to the search path with the CLI flag ``--disable-sys-path``: + +:: + + $ vyper --disable-sys-path my_project/my_contract.vy + +When compiling from a :ref:`.vyz archive file ` or :ref:`standard json input `, the search path is already part of the bundle, it cannot be changed from the command line. .. _structure-state-variables: @@ -91,6 +157,47 @@ Functions may be called internally or externally depending on their :ref:`visibi See the :ref:`Functions ` documentation for more information. +.. _modules: + +Modules +========== + +A module is a set of function definitions and variable declarations which enables code reuse. Vyper favors code reuse through composition, rather than inheritance. + +Broadly speaking, a module contains: + +* function definitions +* state variable declarations +* type definitions + +Therefore, a module encapsulates + +* functionality (types and functions), and +* state (variables), which may be tightly coupled with that functionality + +Modules can be added to contracts by importing them from a ``.vy`` file. Any ``.vy`` file is a valid module which can be imported into another contract! This is a very powerful feature which allows you to assemble contracts via other contracts as building blocks. + +.. code-block:: vyper + # my_module.vy + + def perform_some_computation() -> uint256: + return 5 + + @external + def some_external_function() -> uint256: + return 6 + +.. code-block:: vyper + import my_module + + exports: my_module.some_external_function + + @external + def foo() -> uint256: + return my_module.perform_some_computation() + +Modules are opt-in by design. That is, any operations involving state or exposing external functions must be explicitly opted into using the ``exports``, ``uses`` or ``initializes`` keywords. See the :ref:`Modules ` documentation for more information. + Events ====== @@ -112,12 +219,14 @@ Events provide an interface for the EVM's logging facilities. Events may be logg See the :ref:`Event ` documentation for more information. +.. _interfaces: + Interfaces ========== An interface is a set of function definitions used to enable calls between smart contracts. A contract interface defines all of that contract's externally available functions. By importing the interface, your contract now knows how to call these functions in other contracts. -Interfaces can be added to contracts either through inline definition, or by importing them from a separate file. +Interfaces can be added to contracts either through inline definition, or by importing them from a separate ``.vyi`` file. .. code-block:: vyper diff --git a/docs/style-guide.rst b/docs/style-guide.rst index 10869076eb..3c9e8681ae 100644 --- a/docs/style-guide.rst +++ b/docs/style-guide.rst @@ -152,7 +152,7 @@ Type Annotations Tests ===== -We use the `pytest `_ framework for testing, and :ref:`eth-tester` for our local development chain. +We use the `pytest `_ framework for testing. Best Practices -------------- diff --git a/docs/testing-contracts-ethtester.rst b/docs/testing-contracts-ethtester.rst deleted file mode 100644 index 92522a1eca..0000000000 --- a/docs/testing-contracts-ethtester.rst +++ /dev/null @@ -1,81 +0,0 @@ -.. _testing-contracts-ethtester: - -Testing with Ethereum Tester -############################ - -`Ethereum Tester `_ is a tool suite for testing Ethereum based applications. - -This section provides a quick overview of testing with ``eth-tester``. To learn more, you can view the documentation at the `Github repo `_ or join the `Gitter `_ channel. - -Getting Started -=============== - -Prior to testing, the Vyper specific contract conversion and the blockchain related fixtures need to be set up. These fixtures will be used in every test file and should therefore be defined in `conftest.py `_. - -.. note:: - - Since the testing is done in the pytest framework, you can make use of `pytest.ini, tox.ini and setup.cfg `_ and you can use most IDEs' pytest plugins. - -.. literalinclude:: ../tests/conftest.py - :caption: conftest.py - :language: python - :linenos: - -The final two fixtures are optional and will be discussed later. The rest of this chapter assumes that you have this code set up in your ``conftest.py`` file. - -Alternatively, you can import the fixtures to ``conftest.py`` or use `pytest plugins `_. - -Writing a Basic Test -==================== - -Assume the following simple contract ``storage.vy``. It has a single integer variable and a function to set that value. - -.. literalinclude:: ../examples/storage/storage.vy - :caption: storage.vy - :linenos: - :language: vyper - -We create a test file ``test_storage.py`` where we write our tests in pytest style. - -.. literalinclude:: ../tests/functional/examples/storage/test_storage.py - :caption: test_storage.py - :linenos: - :language: python - -First we create a fixture for the contract which will compile our contract and set up a Web3 contract object. We then use this fixture for our test functions to interact with the contract. - -.. note:: - To run the tests, call ``pytest`` or ``python -m pytest`` from your project directory. - -Events and Failed Transactions -============================== - -To test events and failed transactions we expand our simple storage contract to include an event and two conditions for a failed transaction: ``advanced_storage.vy`` - -.. literalinclude:: ../examples/storage/advanced_storage.vy - :caption: advanced_storage.vy - :linenos: - :language: vyper - -Next, we take a look at the two fixtures that will allow us to read the event logs and to check for failed transactions. - -.. literalinclude:: ../tests/conftest.py - :caption: conftest.py - :language: python - :pyobject: tx_failed - -The fixture to assert failed transactions defaults to check for a ``TransactionFailed`` exception, but can be used to check for different exceptions too, as shown below. Also note that the chain gets reverted to the state before the failed transaction. - -.. literalinclude:: ../tests/conftest.py - :caption: conftest.py - :language: python - :pyobject: get_logs - -This fixture will return a tuple with all the logs for a certain event and transaction. The length of the tuple equals the number of events (of the specified type) logged and should be checked first. - -Finally, we create a new file ``test_advanced_storage.py`` where we use the new fixtures to test failed transactions and events. - -.. literalinclude:: ../tests/functional/examples/storage/test_advanced_storage.py - :caption: test_advanced_storage.py - :linenos: - :language: python diff --git a/docs/testing-contracts-titanoboa.rst b/docs/testing-contracts-titanoboa.rst new file mode 100644 index 0000000000..2dfbb5e630 --- /dev/null +++ b/docs/testing-contracts-titanoboa.rst @@ -0,0 +1,6 @@ +.. _testing-contracts-titanoboa: + +Testing with Titanoboa +###################### + +Titanoboa is a Vyper interpreter which is fast and provides a "swiss-army knife" toolkit for developing vyper applications. The best place to start is at `the official docs `_, and skip down to the `testing reference `_ for an overview of testing strategies. Finally, a more detailed API reference is available in the `API reference subsection `_. diff --git a/docs/testing-contracts.rst b/docs/testing-contracts.rst index 043af416fb..3e92819957 100644 --- a/docs/testing-contracts.rst +++ b/docs/testing-contracts.rst @@ -5,13 +5,13 @@ Testing a Contract For testing Vyper contracts we recommend the use of `pytest `_ along with one of the following packages: + * `Titanoboa `_: A Vyper interpreter, pretty tracebacks, forking, debugging and deployment features. Maintained by the Vyper team. * `Brownie `_: A development and testing framework for smart contracts targeting the Ethereum Virtual Machine - * `Ethereum Tester `_: A tool suite for testing ethereum applications Example usage for each package is provided in the sections listed below. .. toctree:: :maxdepth: 2 + testing-contracts-titanoboa.rst testing-contracts-brownie.rst - testing-contracts-ethtester.rst diff --git a/docs/toctree.rst b/docs/toctree.rst index e3583db56b..65bbe3ab9b 100644 --- a/docs/toctree.rst +++ b/docs/toctree.rst @@ -25,6 +25,7 @@ Vyper control-structures.rst scoping-and-declarations.rst built-in-functions.rst + using-modules.rst interfaces.rst event-logging.rst natspec.rst diff --git a/docs/types.rst b/docs/types.rst index f82153b1b9..752e06b14f 100644 --- a/docs/types.rst +++ b/docs/types.rst @@ -230,7 +230,8 @@ Decimals **Keyword:** ``decimal`` -A decimal is a type to store a decimal fixed point value. +A decimal is a type to store a decimal fixed point value. As of v0.4.0, decimals must be enabled with the CLI flag ``--enable-decimals``. + Values ****** @@ -562,7 +563,7 @@ Dynamic arrays represent bounded arrays whose length can be modified at runtime, .. code-block:: vyper - for item in self.my_array: + for item: uint256 in self.my_array: self.my_array[0] = item In the ABI, they are represented as ``_Type[]``. For instance, ``DynArray[int128, 3]`` gets represented as ``int128[]``, and ``DynArray[DynArray[int128, 3], 3]`` gets represented as ``int128[][]``. diff --git a/docs/using-modules.rst b/docs/using-modules.rst new file mode 100644 index 0000000000..4a8af1a7d9 --- /dev/null +++ b/docs/using-modules.rst @@ -0,0 +1,194 @@ +.. _modules: + +Modules +####### + +A module is a set of function definitions and variable declarations which enables code reuse. Vyper favors code reuse through composition, rather than inheritance. A module encapsulates everything needed for code reuse, from type and function declarations to state. It is important to note that functions which make use of defined state must be initialized in order to use that state, whereas functions that are "pure" do not require this. + +Declaring and using modules +=========================== + +The simplest way to define a module is to write a contract. In Vyper, any contract is a valid module! For example, the following contract is also a valid module. + +.. code-block:: vyper + + # ownable.vy + + owner: address + + @deploy + def __init__(): + self.owner = msg.sender + + def _check_owner(): + assert self.owner == msg.sender + + @pure + def _times_two(x: uint256) -> uint256: + return x * 2 + + @external + def update_owner(new_owner: address): + self._check_owner() + + self.owner = new_owner + +This contract basically has two bits of functionality which can be reused upon import, the ``_check_owner()`` function and the ``update_owner()`` function. The ``_check_owner()`` is an internal function which can be used as a helper to check ownership in importing modules, while the ``update_owner()`` is an external function which an importing module can itself :ref:`export ` as an externally facing piece of functionality. + +You can use this module's functionality simply by importing it, however any functionality that you do not use from a module will not be included in the final compilation target. For example, if you don't use the ``initializes`` statement to declare a module's location in the storage layout, you cannot use its state. Similarly, if you don't explicitly ``export`` an external function from a module, it will not appear in the runtime code. + +Importing a module +================== + +A module can be imported using ``import`` or ``from ... import`` statements. The following are all equivalent ways to import the above module: + +.. code-block:: vyper + + import ownable # accessible as `ownable` + import ownable as ow # accessible as `ow` + from . import ownable # accessible as `ownable` + from . import ownable as ow # accessible as `ow` + +When importing using the ``as`` keyword, the module will be referred to by its alias in the rest of the contract. + +The ``_times_two()`` helper function in the above module can be immediately used without any further work since it is "pure" and doesn't depend on initialized state. + +.. code-block:: vyper + + import ownable as helper + @external + def my_function(x: uint256) -> uint256: + return helper._times_two(x) + +The other functions cannot be used yet, because they touch the ``ownable`` module's state. There are two ways to declare a module so that its state can be used. + +Initializing a module +===================== + +In order to use a module's state, it must be "initialized". A module can be initialized with the ``initializes`` keyword. This declares the module's location in the contract's :ref:`Storage Layout `. It also creates a requirement to invoke the module's :ref:`__init__() function `, if it has one. This is a well-formedness requirement, since it does not make sense to access a module's state unless its ``__init__()`` function has been called. + +.. code-block:: vyper + + import ownable + + initializes: ownable + + @deploy + def __init__(): + ownable.__init__() + + @external + def my_access_controlled_function(): + ownable._check_owner() # reverts unless msg.sender == ownable.owner + + ... # do things that only the owner can do + +It is a compile-time error to invoke a module's ``__init__()`` function more than once! + +A module's state can be directly accessed just by prefixing the name of a variable with the module's alias, like follows: + +.. code-block:: vyper + + @external + def get_owner() -> address: + return ownable.owner + + +The ``uses`` statement +====================== + +Another way of using a contract's state without directly initializing it is to use the ``uses`` keyword. This is a more advanced usage which is expected to be mostly utilized by library designers. The ``uses`` statement allows a module to use another module's state but defer its initialization to another module in the compilation tree (most likely a user of the library in question). + +This is best illustrated with an example: + +.. code-block:: vyper + + # ownable_2step.vy + import ownable + + uses: ownable + + # does not export ownable.transfer_ownership! + + pending_owner: address # the pending owner in the 2-step transfer process + + @deploy + def __init__(): + self.pending_owner = empty(address) + + @external + def begin_transfer(new_owner: address): + ownable._check_owner() + + self.pending_owner = new_owner + + @external + def accept_transfer(): + assert msg.sender == self.pending_owner + + ownable.owner = new_owner + self.pending_owner = empty(address) + +Here, the ``ownable_2step`` module does not want to seal off access to calling the ``ownable`` module's ``__init__()`` function. So, it utilizes the ``uses: ownable`` statement to get access to the ``ownable`` module's state, without the requirement to initialize it. Note that this is a valid module, but it is not a valid contract (that is, it cannot produce bytecode) because it does not initialize the ``ownable`` module. To make a valid contract, the user of the ``ownable_2step`` module would be responsible for initializing the ``ownable`` module themselves (as in the next section: :ref:`initializing dependencies `). + +Whether to ``use`` or ``initialize`` a module is a choice which is left up to the library designer. + +Technical notes on the design +----------------------------- + +This section contains some notes on the design from a language design perspective. It can be safely skipped if you are just interested in how to use modules, and not necessarily in programming language theory. + +The design of the module system takes inspiration from (but is not directly related to) the rust language's `borrow checker `_. In the language of type systems, module initialization is modeled as an affine constraint which is promoted to a linear constraint if the module's state is touched in the compilation target. In practice, what this means is: + +* A module must be "used" or "initialized" before its state can be accessed in an import +* A module may be "used" many times +* A module which is "used" or its state touched must be "initialized" exactly once + +.. _init-dependencies: + +Initializing a module with dependencies +======================================= + +Sometimes, you may encounter a module which itself ``uses`` other modules. Vyper's module system is designed to allow this, but it requires you make explicit the access to the imported module's state. The above ``ownable_2step.vy`` contract is an example of this. If you wanted to initialize the ``ownable_2step`` module, it would use the special ``:=`` (aka "walrus") syntax, and look something like this: + +.. code-block:: vyper + + import ownable + import ownable_2step + + initializes: ownable + + # ownable is explicitly declared as a state dependency of `ownable_2step` + initializes: ownable_2step[ownable := ownable] + + @deploy + def __init__(): + ownable.__init__() + ownable_2step.__init__() + + # export all external functions from ownable_2step + exports: ownable_2step.__interface__ + +.. _exporting-functions: + +Exporting functions +=================== + +In Vyper, ``@external`` functions are not automatically exposed (i.e., included in the runtime code) in the importing contract. This is a safety feature, it means that any externally facing functionality must be explicitly defined in the top-level of the compilation target. + +So, exporting external functions from modules is accomplished using the ``exports`` keyword. In Vyper, functions can be exported individually, or, a wholesale export of all the functions in a module can be done. The following are all ways of exporting functions from an imported module. + +.. code-block:: vyper + + # export a single function from `ownable_2step` + exports: ownable_2step.transfer_ownership + + # export multiple functions from `ownable_2step`, being explicit about + # which specific functions are being exported + exports: ( + ownable_2step.transfer_ownership, + ownable_2step.accept_ownership, + ) + + # export all external functions from `ownable_2step` + exports: ownable_2step.__interface__ diff --git a/vyper/cli/vyper_compile.py b/vyper/cli/vyper_compile.py index 226a76242a..b7e664b975 100755 --- a/vyper/cli/vyper_compile.py +++ b/vyper/cli/vyper_compile.py @@ -42,6 +42,7 @@ ir_json - Intermediate representation in JSON format ir_runtime - Intermediate representation of runtime bytecode in list format asm - Output the EVM assembly of the deployable bytecode +integrity - Output the integrity hash of the source code archive - Output the build as an archive file solc_json - Output the build in solc json format """ @@ -163,8 +164,16 @@ def _parse_args(argv): "--hex-ir", help="Represent integers as hex values in the IR", action="store_true" ) parser.add_argument( - "--path", "-p", help="Set the root path for contract imports", action="append", dest="paths" + "--path", + "-p", + help="Add a path to the compiler's search path", + action="append", + dest="paths", ) + parser.add_argument( + "--disable-sys-path", help="Disable the use of sys.path", action="store_true" + ) + parser.add_argument("-o", help="Set the output path", dest="output_path") parser.add_argument( "--experimental-codegen", @@ -173,9 +182,6 @@ def _parse_args(argv): dest="experimental_codegen", ) parser.add_argument("--enable-decimals", help="Enable decimals", action="store_true") - parser.add_argument( - "--disable-sys-path", help="Disable the use of sys.path", action="store_true" - ) args = parser.parse_args(argv)