Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use jinja for templating #21

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ This changelog format is based on [Keep a Changelog](https://keepachangelog.com/
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased](https://github.com/iamdefinitelyahuman/brownie-token-tester)
- Use Jinja2 for templating ERC20 contract, add support for eip-2612 permit function

## [0.3.2](https://github.com/iamdefinitelyahuman/brownie-token-tester/tree/v0.3.2) - 2021-07-06
### Fixed
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,14 @@ def ERC20(
decimals: int = 18,
success: Union[bool, None] = True,
fail: Union[bool, str, None] = "revert",
use_eip2612: bool = False,
) -> Contract:
```

- The `success` kwarg is used to set the token's return value upon a successful call to `approve`, `transfer` or `transferFrom`. Valid values are `True`, `False`, and `None`.
- The `fail` kwarg sets the token's behaviour upon failed calls to the above methods. Use `"revert"` if the transaction should revert, or `True`, `False`, and `None` to return a value without reverting.
- The `use_eip2612` kwarg determines whether the token should support the `permit` function defined in `eip-2612`


The resulting deployment adheres to the [ERC20 Token Standard](https://eips.ethereum.org/EIPS/eip-20) and additionally implements one non-standard method:

Expand Down
87 changes: 33 additions & 54 deletions brownie_tokens/template.py
Original file line number Diff line number Diff line change
@@ -1,44 +1,23 @@
from brownie import Contract, compile_source
from brownie.network.account import Account
from pathlib import Path
from typing import Dict, Union
from jinja2 import Environment, PackageLoader
from typing import Union

RETURN_TYPE: Dict = {
True: " -> bool",
False: " -> bool",
None: "",
}

RETURN_STATEMENT: Dict = {
True: "return True",
False: "return False",
None: "return",
}

FAIL_STATEMENT: Dict = {
"revert": "raise",
True: "return True",
False: "return False",
None: "return",
}

STRING_CONVERT: Dict = {
"true": True,
"false": False,
"none": None,
}

with Path(__file__).parent.joinpath("token-template.vy").open() as fp:
TEMPLATE = fp.read()
env = Environment(
trim_blocks=True,
lstrip_blocks=True,
loader=PackageLoader("brownie_tokens"),
)


def ERC20(
name: str = "Test Token",
symbol: str = "TST",
decimals: int = 18,
success: Union[bool, str, None] = True,
success: Union[bool, None] = True,
fail: Union[bool, str, None] = "revert",
deployer: Union[Account, str, None] = None,
use_eip2612: bool = False,
) -> Contract:
"""
Deploy an ERC20 contract for testing purposes.
Expand All @@ -58,36 +37,36 @@ def ERC20(
to make the transaction revert.
deployer: Account | str, optional
Address to deploy the contract from.
use_eip2612: bool, optional
Include EIP-2612 (permit) support in the resulting contract

Returns
-------
Contract
Deployed ERC20 contract
"""
# understand success and fail when given as strings
if isinstance(success, str) and success.lower() in STRING_CONVERT:
success = STRING_CONVERT[success.lower()]
if isinstance(fail, str) and fail.lower() in STRING_CONVERT:
fail = STRING_CONVERT[fail.lower()]
# input validation
if not (isinstance(success, bool) or success is None):
raise TypeError(f"Argument `success` has invalid type: '{type(success)}'")
if not (isinstance(fail, (bool, str)) or fail is None):
raise TypeError(f"Argument `fail` has invalid type: '{type(fail)}'")
if isinstance(fail, str) and fail.lower() != "revert":
raise ValueError(f"Argument `fail` has invalid value: '{fail}'")

if success is None and isinstance(fail, bool) or fail is None and isinstance(success, bool):
raise ValueError(
"Return values for `success` and `fail` are incompatible: "
+ f"success=`{success}`, fail=`{fail}`"
)

if success not in RETURN_STATEMENT:
valid_keys = [str(i) for i in RETURN_STATEMENT.keys()]
raise ValueError(f"Invalid value for `success`, valid options are: {', '.join(valid_keys)}")
if fail not in FAIL_STATEMENT:
valid_keys = [str(i) for i in FAIL_STATEMENT.keys()]
raise ValueError(f"Invalid value for `fail`, valid options are: {', '.join(valid_keys)}")
if None in (fail, success) and fail is not success and fail != "revert":
raise ValueError("Cannot use `None` for only one of `success` and `fail`.")
# fetch and render template
template = env.get_template("ERC20.vy")
src = template.render(retval=success, failval=fail, use_eip2612=use_eip2612)

source = TEMPLATE.format(
return_type=RETURN_TYPE[success],
return_statement=RETURN_STATEMENT[success],
fail_statement=FAIL_STATEMENT[fail],
)
contract = compile_source(source, vyper_version="0.2.12").Vyper
ERC20 = compile_source(src, vyper_version="0.3.1").Vyper

if deployer is None:
tx_params = {"from": "0x0000000000000000000000000000000000001337", "silent": True}
else:
tx_params = {"from": deployer}
return contract.deploy(name, symbol, decimals, tx_params)
tx_params = {
"from": deployer or "0x0000000000000000000000000000000000001337",
"silent": deployer is not None,
}
return ERC20.deploy(name, symbol, decimals, tx_params)
256 changes: 256 additions & 0 deletions brownie_tokens/templates/ERC20.vy
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
{% macro return_type(retval) %}{% if retval is boolean %} -> bool {%- endif %}{% endmacro -%}

{% macro return_statement(retval) %}
{% if retval is true %}
return True
{% elif retval is false %}
return False
{% else %}
return
{% endif %}
{% endmacro -%}

{# failval should be either boolean or string #}
{# from should be either `msg.sender` or `_from` #}
{% macro validate_transfer(failval, from) %}
{% if failval is boolean or failval is none %}
user_balance: uint256 = self.balanceOf[{{ from }}]
if user_balance < _value:
{% if failval is true %}
return True
{% elif failval is false %}
return False
{% else %}
return
{% endif %}

self.balanceOf[{{ from }}] = user_balance - _value
{% else %}
self.balanceOf[{{ from }}] -= _value
{% endif %}
{% endmacro -%}

# @version 0.3.1
"""
@notice Mock ERC20
@dev See https://eips.ethereum.org/EIPS/eip-20
"""
from vyper.interfaces import ERC20

implements: ERC20


event Approval:
_owner: indexed(address)
_spender: indexed(address)
_value: uint256

event Transfer:
_from: indexed(address)
_to: indexed(address)
_value: uint256


{% if use_eip2612 is true %}
EIP712_TYPEHASH: constant(bytes32) = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")
PERMIT_TYPEHASH: constant(bytes32) = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)")
VERSION: constant(String[8]) = "v0.1.0"
{% endif %}


{% if use_eip2612 is true %}
DOMAIN_SEPARATOR: immutable(bytes32)
{% endif %}

NAME: immutable(String[128])
SYMBOL: immutable(String[64])
DECIMALS: immutable(uint8)


allowance: public(HashMap[address, HashMap[address, uint256]])
balanceOf: public(HashMap[address, uint256])
totalSupply: public(uint256)

{% if use_eip2612 is true %}
nonces: public(HashMap[address, uint256])
{% endif %}


@external
def __init__(_name: String[128], _symbol: String[64], _decimals: uint8):
NAME = _name
SYMBOL = _symbol
DECIMALS = _decimals

{% if use_eip2612 is true %}
DOMAIN_SEPARATOR = keccak256(
_abi_encode(EIP712_TYPEHASH, keccak256(_name), keccak256(VERSION), chain.id, self)
)
{% endif %}


@external
def approve(_spender: address, _value: uint256){{ return_type(retval) }}:
"""
@notice Allow `_spender` to transfer/withdraw from your account multiple times, up to `_value`
amount.
@dev If this function is called again it overwrites the current allowance with `_value`.
@param _spender The address given permission to transfer/withdraw tokens.
@param _value The amount of tokens that may be transferred.
"""
self.allowance[msg.sender][_spender] = _value

log Approval(msg.sender, _spender, _value)
{{ return_statement(retval)|trim }}

{% if use_eip2612 is true %}

@external
def permit(
_owner: address,
_spender: address,
_value: uint256,
_deadline: uint256,
_v: uint8,
_r: bytes32,
_s: bytes32
):
"""
@notice Approves spender by owner's signature to expend owner's tokens.
See https://eips.ethereum.org/EIPS/eip-2612.
@param _owner The address which is a source of funds and has signed the Permit.
@param _spender The address which is allowed to spend the funds.
@param _value The amount of tokens to be spent.
@param _deadline The timestamp after which the Permit is no longer valid.
@param _v The bytes[64] of the valid secp256k1 signature of permit by owner
@param _r The bytes[0:32] of the valid secp256k1 signature of permit by owner
@param _s The bytes[32:64] of the valid secp256k1 signature of permit by owner
"""
assert _owner != ZERO_ADDRESS
assert block.timestamp <= _deadline

nonce: uint256 = self.nonces[_owner]
digest: bytes32 = keccak256(
concat(
b"\x19\x01",
DOMAIN_SEPARATOR,
keccak256(_abi_encode(PERMIT_TYPEHASH, _owner, _spender, _value, nonce, _deadline))
)
)

assert ecrecover(digest, convert(_v, uint256), convert(_r, uint256), convert(_s, uint256)) == _owner

self.allowance[_owner][_spender] = _value
self.nonces[_owner] = nonce + 1

log Approval(_owner, _spender, _value)

{% endif %}

@external
def transfer(_to: address, _value: uint256){{ return_type(retval) }}:
"""
@notice Transfer `_value` amount of tokens to address `_to`.
@dev Reverts if caller's balance does not have enough tokens to spend.
@param _to The address to increase the balance of.
@param _value The amount of tokens to transfer.
"""
{# input validation is handled prior to template rendering #}
{{ validate_transfer(failval, "msg.sender")|indent|trim }}
self.balanceOf[_to] += _value

log Transfer(msg.sender, _to, _value)
{{ return_statement(retval)|trim }}


@external
def transferFrom(_from: address, _to: address, _value: uint256){{ return_type(retval) }}:
"""
@notice Transfers ~_value` amount of tokens from address `_from` to address `_to`.
@dev Reverts if caller's allowance is not enough, or if `_from` address does not have a balance
great enough.
@param _from The account to transfer/withdraw tokens from.
@param _to The account to transfer tokens to.
@param _value The amount of tokens to transfer.
"""
{# input validation is handled prior to template rendering #}
{% if failval is boolean or failval is none %}
allowance: uint256 = self.allowance[_from][msg.sender]
if allowance < _value:
{% if failval is true %}
return True
{% elif failval is false %}
return False
{% else %}
return
{% endif %}

{{ validate_transfer(failval, "_from")|indent|trim }}
self.balanceOf[_to] += _value
self.allowance[_from][msg.sender] = allowance - _value
{% else %}
{{ validate_transfer(failval, "_from")|indent|trim }}
self.balanceOf[_to] += _value
self.allowance[_from][msg.sender] -= _value
{% endif %}

log Transfer(_from, _to, _value)
{{ return_statement(retval)|trim }}


@external
def _mint_for_testing(_to: address, _value: uint256):
"""
@notice Mint new tokens
"""
self.balanceOf[_to] += _value
self.totalSupply += _value

log Transfer(ZERO_ADDRESS, _to, _value)


@view
@external
def name() -> String[128]:
"""
@notice Query the name of the token.
"""
return NAME


@view
@external
def symbol() -> String[64]:
"""
@notice Query the symbol of the token.
"""
return SYMBOL


@view
@external
def decimals() -> uint8:
"""
@notice Query the decimals of the token.
"""
return DECIMALS


{% if use_eip2612 is true %}
@view
@external
def DOMAIN_SEPARATOR() -> bytes32:
"""
@notice Query the DOMAIN_SEPARATOR immutable value
"""
return DOMAIN_SEPARATOR


@view
@external
def version() -> String[8]:
"""
@notice Query the contract version, used for EIP-2612
"""
return VERSION
{% endif %}
Loading