Skip to content

fix[lang]: disallow some builtins in pure functions#3157

Merged
charles-cooper merged 39 commits intovyperlang:masterfrom
tserg:fix/blockhash
Apr 14, 2025
Merged

fix[lang]: disallow some builtins in pure functions#3157
charles-cooper merged 39 commits intovyperlang:masterfrom
tserg:fix/blockhash

Conversation

@tserg
Copy link
Copy Markdown
Contributor

@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

this commit disallows some builtins from being called in `pure`
functions, specifically, `blockhash()` and `blobhash()`. it
accomplishes this by adding a `mutability` property to builtins, and a
case to `FunctionAnalyzer` to check the mutability property.

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
Copy Markdown
Contributor 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
Copy Markdown
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
Copy Markdown

codecov-commenter commented Nov 27, 2022

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 92.63%. Comparing base (1591a12) to head (0c0cdc1).
Report is 80 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #3157   +/-   ##
=======================================
  Coverage   92.63%   92.63%           
=======================================
  Files         122      122           
  Lines       17518    17523    +5     
  Branches     2976     2977    +1     
=======================================
+ Hits        16228    16233    +5     
  Misses        892      892           
  Partials      398      398           

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

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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
Copy Markdown
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
Copy Markdown
Contributor 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
Copy Markdown
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
@tserg
Copy link
Copy Markdown
Contributor 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
Copy Markdown
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
Copy Markdown
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)

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.
@cyberthirst
Copy link
Copy Markdown
Collaborator

should we do the same for blobhash?

@charles-cooper
Copy link
Copy Markdown
Member

should we do the same for blobhash?

Yes

Copy link
Copy Markdown
Contributor 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
Copy Markdown
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
Copy Markdown
Contributor 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.

Copy link
Copy Markdown
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.

this lgtm. requesting a look from @cyberthirst

@cyberthirst cyberthirst modified the milestones: v0.3.8, v0.4.2 Mar 10, 2025
@charles-cooper charles-cooper added the release - must release blocker label Mar 17, 2025
@cyberthirst
Copy link
Copy Markdown
Collaborator

I don't love the design, like we default to pure and then let this check self._check_call_mutability(func_type.mutability) trivially pass for some cases

I think that's pretty confusing when reading the code.

With Charles we discussed it would be better to compute this information at callsite as the arguments can affect the final mutability for the given node .. and then use it to check the mutability (so it wouldn't just trivially pass)

@tserg
Copy link
Copy Markdown
Contributor Author

tserg commented Mar 20, 2025

With Charles we discussed it would be better to compute this information at callsite as the arguments can affect the final mutability for the given node .. and then use it to check the mutability (so it wouldn't just trivially pass)

Does that mean this check should be specific to each builtin function, similar to how we check the mutability against the contract function's context for raw_call in build_IR?

@cyberthirst
Copy link
Copy Markdown
Collaborator

should we also fix this #3093 in the context of this PR?

@cyberthirst
Copy link
Copy Markdown
Collaborator

With Charles we discussed it would be better to compute this information at callsite as the arguments can affect the final mutability for the given node .. and then use it to check the mutability (so it wouldn't just trivially pass)

Does that mean this check should be specific to each builtin function, similar to how we check the mutability against the contract function's context for raw_call in build_IR?

The original thought was that the mutability would be callsite specific based on the given arguments (like raw_call can be static and delegate on different callsites).

But @charles-cooper suggested it might be unnecessary to pass the arguments to each function for evaluation because most of them don't rely on them, the mutability is statically set. So maybe we'd be ok with a raw_call specific check on callsite - unless you can think of other cases.

@tserg
Copy link
Copy Markdown
Contributor Author

tserg commented Mar 25, 2025

With Charles we discussed it would be better to compute this information at callsite as the arguments can affect the final mutability for the given node .. and then use it to check the mutability (so it wouldn't just trivially pass)

Does that mean this check should be specific to each builtin function, similar to how we check the mutability against the contract function's context for raw_call in build_IR?

The original thought was that the mutability would be callsite specific based on the given arguments (like raw_call can be static and delegate on different callsites).

But @charles-cooper suggested it might be unnecessary to pass the arguments to each function for evaluation because most of them don't rely on them, the mutability is statically set. So maybe we'd be ok with a raw_call specific check on callsite - unless you can think of other cases.

Yea raw_call looks like the odd one out where mutability depends on the arguments at callsite. Does that mean I should leave this PR as it is? If so, #3093 looks specific to raw_call so maybe it should be tackled in a separate PR?

@charles-cooper
Copy link
Copy Markdown
Member

Yea raw_call looks like the odd one out where mutability depends on the arguments at callsite. Does that mean I should leave this PR as it is? If so, #3093 looks specific to raw_call so maybe it should be tackled in a separate PR?

yea, let's deal with the call-site analysis once we tackle raw_call

@cyberthirst
Copy link
Copy Markdown
Collaborator

@charles-cooper we have a couple of builtins that rely on context.check_is_not_constant - maybe we should reuse this machinery for these cases too? i think it's better to have these checks in the frontend

_return_type: Optional[VyperType] = None
_equality_attrs = ("_id",)
_is_terminus = False
mutability: StateMutability = StateMutability.PURE
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

i think i'd like to see mutable default better .. it's a tradeoff .. we make a mistake and allow mutable function to be called in non-mutable context - or - we have the opposite default and risk some contracts not compiling

ZeroDivisionException,
)
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.
@charles-cooper charles-cooper changed the title fix[lang]: disallow blockhash in pure functions fix[lang]: disallow blockhash() in pure functions Apr 14, 2025
@charles-cooper charles-cooper changed the title fix[lang]: disallow blockhash() in pure functions fix[lang]: disallow some builtins in pure functions Apr 14, 2025
@charles-cooper charles-cooper merged commit 79001f3 into vyperlang:master Apr 14, 2025
163 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release - must release blocker

Projects

None yet

Development

Successfully merging this pull request may close these issues.

blockhash() allowed in pure functions

6 participants