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

fix[lang]: disallow blockhash in pure functions #3157

Open
wants to merge 35 commits into
base: master
Choose a base branch
from

Conversation

tserg
Copy link
Collaborator

@tserg tserg commented Nov 24, 2022

What I did

Fix #3141.

How I did it

Check for Name nodes with blockhash in local validation of pure functions.

How to verify it

See test.

Commit message

fix: disallow blockhash in pure functions

Description for the changelog

Disallow blockhash in pure functions

Cute Animal Picture

Put a link to a cute animal picture inside the parenthesis-->

@tserg
Copy link
Collaborator Author

tserg commented Nov 24, 2022

An alternative and perhaps more scalable approach is to add a _reads_environment property to BuiltinFunction, which is set to True for BlockHash(). We can then define the set of builtin functions that reads from the chain by checking for this property (e.g. ENVIRONMENT_BUILTIN_FUNCTIONS). However, I encountered issues with circular imports if I define ENVIRONMENT_BUILTIN_FUNCTIONS in vyper/builtin_functions/functions.py and import it in vyper/semantics/validation/local.py.

@tserg tserg marked this pull request as ready for review November 24, 2022 09:44
Copy link
Member

@charles-cooper charles-cooper left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea, i think the clearer and more maintainable approach would be to set the mutability of the builtin correctly

@codecov-commenter
Copy link

codecov-commenter commented Nov 27, 2022

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 86.38%. Comparing base (4595938) to head (e2e9864).

❗ Current head e2e9864 differs from pull request most recent head b1517b7. Consider uploading reports for the commit b1517b7 to get more accurate results

❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #3157      +/-   ##
==========================================
+ Coverage   86.34%   86.38%   +0.03%     
==========================================
  Files          92       92              
  Lines       14028    14026       -2     
  Branches     3083     3081       -2     
==========================================
+ Hits        12113    12116       +3     
+ Misses       1487     1483       -4     
+ Partials      428      427       -1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

from vyper.builtin_functions.functions import BUILTIN_FUNCTIONS
builtin_fns = fn_node.get_descendants(vy_ast.Name, {"id": set(BUILTIN_FUNCTIONS)})

node_list.extend(builtin_fns) # type: ignore
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need to do this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To add builtin function calls to the list of nodes to check. Or should the check for builtin functions be performed elsewhere?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh for some reason i thought the mutability check was performed under visit_Call. i guess this solution is ok for now; i want to get in #2974 first though

@charles-cooper charles-cooper added this to the v0.3.8 milestone May 14, 2023
vyper/builtins/_signatures.py Fixed Show fixed Hide fixed
vyper/builtins/functions.py Fixed Show fixed Hide fixed
vyper/semantics/analysis/local.py Fixed Show fixed Hide fixed
vyper/semantics/analysis/local.py Fixed Show fixed Hide fixed
@tserg
Copy link
Collaborator Author

tserg commented May 18, 2023

I have changed the approach to one that also fixes #3425.

Comment on lines 207 to 222
# Add all calls except structs
filtered_call_nodes = [
c.func
for c in fn_node.get_descendants(vy_ast.Call)
if not (len(c.args) == 1 and isinstance(c.args[0], vy_ast.Dict))
]
node_list.extend(filtered_call_nodes) # type: ignore

for node in node_list:
t = node._metadata.get("type")
if isinstance(t, ContractFunctionT) and t.mutability == StateMutability.PURE:
# skip structs and interface instantiation
if isinstance(t, StructT) or is_type_t(t, InterfaceT):
continue

# skip ContractFunctionT and BuiltinFunction with mutability set to pure
if hasattr(t, "mutability") and t.mutability == StateMutability.PURE:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think the ast analysis is still not the way to go. i think probably a better approach is to add a mutability to all ExprInfos, and then during local.py we can check at every single expression (and maybe statement too) whether expr_info.mutability > self.func.mutability.

@charles-cooper
Copy link
Member

this is looking better. i'm still hesitant though, because i tried this myself and realized that the approach doesn't work for raw_call -- raw_call requires a call site in order to figure out whether it's a staticcall or not. but maybe we can just fenagle in some extra logic into raw_call (maybe in its fetch_call_return implementation)

@@ -1061,6 +1061,25 @@ def fetch_call_return(self, node):

kwargz = {i.arg: i.value for i in node.keywords}

delegate_call = kwargz.get("is_delegate_call")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if the user passes is_delegate_call=False?

if (delegate_call or static_call) and value is not None:
raise ArgumentException("value= may not be passed for static or delegate calls!")

fn_node = node.get_ancestor(vy_ast.FunctionDef)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note for refactoring: ideally we should pass this in a context object

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should I add a comment here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure

@@ -900,25 +900,29 @@ def set_lucky(arg1: address, arg2: int128):
print("Successfully executed an external contract call state change")


def test_constant_external_contract_call_cannot_change_state():
c = """
@pytest.mark.parametrize("state_modifying_decorator", ("payable", "nonpayable"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@pytest.mark.parametrize("state_modifying_decorator", ("payable", "nonpayable"))
@pytest.mark.parametrize("modifying", ("payable", "nonpayable"))

def test_constant_external_contract_call_cannot_change_state():
c = """
@pytest.mark.parametrize("state_modifying_decorator", ("payable", "nonpayable"))
@pytest.mark.parametrize("non_modifying_decorator", ("pure", "view"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@pytest.mark.parametrize("non_modifying_decorator", ("pure", "view"))
@pytest.mark.parametrize("constant", ("pure", "view"))

def test_constant_external_contract_call_cannot_change_state():
c = """
@pytest.mark.parametrize("state_modifying_decorator", ("payable", "nonpayable"))
@pytest.mark.parametrize("non_modifying_decorator", ("pure", "view"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@pytest.mark.parametrize("non_modifying_decorator", ("pure", "view"))
@pytest.mark.parametrize("constant", ("pure", "view"))

@@ -1061,6 +1061,26 @@ def fetch_call_return(self, node):

kwargz = {i.arg: i.value for i in node.keywords}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe get i.get_folded_value().value here

@charles-cooper charles-cooper changed the title fix: disallow blockhash in pure functions fix[lang]: disallow blockhash in pure functions Mar 25, 2024
@charles-cooper
Copy link
Member

let's revert the changes to raw_call here because they seem a bit out of scope; we can revisit in another PR.

@@ -6,7 +6,7 @@
from vyper.codegen.expr import Expr
from vyper.codegen.ir_node import IRnode
from vyper.exceptions import CompilerPanic, TypeMismatch, UnfoldableNode
from vyper.semantics.analysis.base import Modifiability
from vyper.semantics.analysis.base import Modifiability, StateMutability

Check notice

Code scanning / CodeQL

Cyclic import Note

Import of module
vyper.semantics.analysis.base
begins an import cycle.
@@ -53,7 +53,7 @@
UnfoldableNode,
ZeroDivisionException,
)
from vyper.semantics.analysis.base import Modifiability, VarInfo
from vyper.semantics.analysis.base import Modifiability, StateMutability, VarInfo

Check notice

Code scanning / CodeQL

Cyclic import Note

Import of module
vyper.semantics.analysis.base
begins an import cycle.
@cyberthirst
Copy link
Collaborator

should we do the same for blobhash?

@charles-cooper
Copy link
Member

should we do the same for blobhash?

Yes

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I revert the changes in this file?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably not. do any of them fail? it increases test coverage, right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably not. do any of them fail? it increases test coverage, right?

no, they pass, and probably increases test coverage. I will leave this file as it is then.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

blockhash() allowed in pure functions
5 participants