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

assure function #1325

Closed
wants to merge 6 commits into from
Closed
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
19 changes: 18 additions & 1 deletion docs/built-in-functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -351,17 +351,34 @@ CAUTION! This method will delete the contract from the Ethereum blockchain. All
----------
::

def assert(a):
def assert(a, reason):
"""
:param a: the boolean condition to assert
:type a: bool
:type reason: string_literal
"""

Asserts the specified condition, if the condition is equals to true the code will continue to run.
Otherwise, the OPCODE ``REVERT`` (0xfd) will be triggered, the code will stop it's operation, the contract's state will be reverted to the state before the transaction took place and the remaining gas will be returned to the transaction's sender.

An optional reason string literal can be supplied to the assert statement to help a programmer indicate why an assertion failed, this is done using `Error(string)` method as specified in `EIP838 <https://github.com/ethereum/EIPs/issues/838>`_.

Note: To give it a more Python like syntax, the assert function can be called without parenthesis, the syntax would be ``assert your_bool_condition``. Even though both options will compile, it's recommended to use the Pythonic version without parenthesis.


**assure**
----------
::

def assure(a):
"""
:param a: the boolean condition to assure
:type a: bool
"""

Assure is the same as an `assert` function, but uses the OPCODE ``INVALID`` (0xfe) instead, when triggerd the code will stop it's operation, the contract's state will be reverted to the state before the transaction took place and the remaining gas will NOT be returned. The purpose of this function is for use with static analyzers and similar tools, and in general is not recommended for uses outside of this use case.


**raw_log**
-----------
::
Expand Down
11 changes: 5 additions & 6 deletions tests/parser/features/test_assert.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,14 @@ def foo():
"""
c = get_contract_with_gas_estimation(code)
a0 = w3.eth.accounts[0]
pre_balance = w3.eth.getBalance(a0)
tx_hash = c.foo(transact={'from': a0, 'gas': 10**6, 'gasPrice': 10})
assert w3.eth.getTransactionReceipt(tx_hash)['status'] == 0
gas_sent = 10**6
tx_hash = c.foo(transact={'from': a0, 'gas': gas_sent, 'gasPrice': 10})
# More info on receipt status:
# https://github.com/ethereum/EIPs/blob/master/EIPS/eip-658.md#specification.
post_balance = w3.eth.getBalance(a0)
tx_receipt = w3.eth.getTransactionReceipt(tx_hash)
assert tx_receipt['status'] == 0
# Checks for gas refund from revert
# 10**5 is added to account for gas used before the transactions fails
assert pre_balance > post_balance
assert tx_receipt['gasUsed'] < gas_sent


def test_assert_reason(w3, get_contract_with_gas_estimation, assert_tx_failed):
Expand Down
67 changes: 67 additions & 0 deletions tests/parser/features/test_assure.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import pytest

from eth_tester.exceptions import (
TransactionFailed
)


def test_assure_refund(w3, get_contract):
code = """
@public
def foo():
assure(1 == 2)
"""

c = get_contract(code)
a0 = w3.eth.accounts[0]
gas_sent = 10**6
tx_hash = c.foo(transact={'from': a0, 'gas': gas_sent, 'gasPrice': 10})
tx_receipt = w3.eth.getTransactionReceipt(tx_hash)

assert tx_receipt['status'] == 0
assert tx_receipt['gasUsed'] == gas_sent # Drains all gains sent


def test_basic_assure(w3, get_contract, assert_tx_failed):
code = """
@public
def foo(val: int128) -> bool:
assure(val > 0)
assure(val == 2)
return True
"""

c = get_contract(code)

assert c.foo(2) is True

assert_tx_failed(lambda: c.foo(1))
assert_tx_failed(lambda: c.foo(-1))

with pytest.raises(TransactionFailed) as e_info:
c.foo(-2)

assert 'Invalid opcode 0xf' in e_info.value.args[0]


def test_basic_call_assure(w3, get_contract, assert_tx_failed):
code = """

@constant
@private
def _test_me(val: int128) -> bool:
return val == 33

@public
def foo(val: int128) -> int128:
assure(self._test_me(val))
return -123
"""

c = get_contract(code)

assert c.foo(33) == -123

assert_tx_failed(lambda: c.foo(1))
assert_tx_failed(lambda: c.foo(1))
assert_tx_failed(lambda: c.foo(-1))
12 changes: 12 additions & 0 deletions vyper/compile_lll.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,18 @@ def compile_to_assembly(code, withargs=None, existing_labels=None, break_dest=No
# if arg.valency == 1 and arg != code.args[-1]:
# o.append('POP')
return o
# Assure (if false, invalid opcode)
elif code.value == 'assure':
o = compile_to_assembly(code.args[0], withargs, existing_labels, break_dest, height)
end_symbol = mksymbol()
o.extend([
end_symbol,
'JUMPI',
'INVALID',
end_symbol,
'JUMPDEST'
])
return o
# Assert (if false, exit)
elif code.value == 'assert':
o = compile_to_assembly(code.args[0], withargs, existing_labels, break_dest, height)
Expand Down
8 changes: 8 additions & 0 deletions vyper/functions/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1125,6 +1125,13 @@ def _can_compare_with_uint256(operand):
)


@signature('*')
def assure(expr, args, kwargs, context):
with context.assertion_scope():
sub = Expr.parse_value_expr(args[0], context)
return LLLnode.from_list(['assure', sub], typ=None, pos=getpos(expr))


def _clear():
raise ParserException(
"This function should never be called! `clear()` is currently handled "
Expand Down Expand Up @@ -1171,4 +1178,5 @@ def _clear():
'raw_call': raw_call,
'raw_log': raw_log,
'create_with_code_of': create_with_code_of,
'assure': assure
}
1 change: 1 addition & 0 deletions vyper/opcodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@
'CLAMP_NONZERO': [None, 1, 1, 19],
'ASSERT': [None, 1, 0, 85],
'ASSERT_REASON': [None, 3, 0, 85],
'ASSURE': [None, 1, 0, 85],
'PASS': [None, 0, 0, 0],
'BREAK': [None, 0, 0, 20],
'CONTINUE': [None, 0, 0, 20],
Expand Down
60 changes: 33 additions & 27 deletions vyper/parser/stmt.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,34 +366,18 @@ def call(self):
pack_logging_topics,
)

is_self_function = (
isinstance(self.stmt.func, ast.Attribute)
) and isinstance(self.stmt.func.value, ast.Name) and self.stmt.func.value.id == "self"

is_log_call = (
isinstance(self.stmt.func, ast.Attribute)
) and isinstance(self.stmt.func.value, ast.Name) and self.stmt.func.value.id == 'log'

if isinstance(self.stmt.func, ast.Name):
if self.stmt.func.id in stmt_dispatch_table:
if self.stmt.func.id == 'clear':
return self._clear()
else:
return stmt_dispatch_table[self.stmt.func.id](self.stmt, self.context)
elif self.stmt.func.id in dispatch_table:
raise StructureException(
"Function {} can not be called without being used.".format(
self.stmt.func.id
),
self.stmt,
)
else:
raise StructureException(
"Unknown function: '{}'.".format(self.stmt.func.id),
self.stmt,
)
elif is_self_function:
is_self_function, is_log_call = False, False
if isinstance(self.stmt.func, ast.Attribute) and isinstance(self.stmt.func.value, ast.Name):
if self.stmt.func.value.id == "self":
is_self_function = True
elif self.stmt.func.value.id == "log":
is_log_call = True

# self.<function_name> call
if is_self_function:
return self_call.make_call(self.stmt, self.context)

# log.<event_name> call
elif is_log_call:
if self.stmt.func.attr not in self.context.sigs['self']:
raise EventDeclarationException("Event not declared yet: %s" % self.stmt.func.attr)
Expand Down Expand Up @@ -440,6 +424,28 @@ def call(self):
add_gas_estimate=inargsize * 10,
)
], typ=None, pos=getpos(self.stmt))

# Function call
elif isinstance(self.stmt.func, ast.Name):
if self.stmt.func.id in stmt_dispatch_table:
if self.stmt.func.id == 'clear':
return self._clear()
else:
return stmt_dispatch_table[self.stmt.func.id](self.stmt, self.context)
elif self.stmt.func.id in dispatch_table:
raise StructureException(
"Function {} can not be called without being used.".format(
self.stmt.func.id
),
self.stmt,
)
else:
raise StructureException(
"Unknown function: '{}'.".format(self.stmt.func.id),
self.stmt,
)

# External call
else:
return external_call.make_external_call(self.stmt, self.context)

Expand Down