#37584 [SC-Insight] Nonpayable Not Respected For Internal Function
Submitted on Dec 9th 2024 at 20:41:03 UTC by @anatomist for Attackathon | Ethereum Protocol
Report ID: #37584
Report Type: Smart Contract
Report severity: Insight
Target: https://github.com/vyperlang/vyper
Impacts:
(Compiler) Elimination of security checks
(Compiler) Incorrect bytecode generation leading to incorrect behavior
(Compiler) Unexpected behavior
Description
Brief/Intro
Vyper merges payable / nonpayable distinctions into mutability enum allowing those decorators to be used for internal functions, but does not fully enforces it. This may lead to ambiguity on what contract functions guard against. In the worst case, this may result in missed checks causing program behavior to diverge from user intentions, and eventually lead to funds loss.
Vulnerability Details
Vyper StateMutability
encompasses both pure
/ view
/ (mutable
) and the slightly different payable
/ nonpayable
distinctions.
This means they're treated the same in many contexts, including whether the decorator can be specified for internal functions. While pure
and view
are required annotations for internal functions to keep semantic checks simple, nonpayable
and payable
are a lot more ambiguous when used in this context.
For example, compile time checks are enforced when attempting a call a nonpayable
function from a view
function.
Based on the compiler warning and the fact that practically all other decorators (nonreentrant, view, pure) are enforced for all functions, users may be inclined to believe nonpayable
checks are respected for internal functions too, and zerovalue()
in the following example reverts when msg.value
is not 0.
However, nonpayable
decorators are only enforced when specified for external
and deploy
functions, so in practice, the check is skipped.
Going into details, vyper currently defaults functions without StateMutability
decorators to nonpayable
. In our opinion, this is the root cause behind the lack of nonpayable checks bytecode emission, since the compiler can't differentiate between an explicitly specified nonpayable
and an implicit placeholder nonpayable
. There are two clean ways to work around this.
Disallow the usage of
payable
andnonpayable
decorators for internal functions. This eliminates the ambiguity since the compiler mandatespayable
andnonpayable
should only be applied to external functions. The approach is similar to solidity, which doesn't havenonpayable
, but restrictspayable
attributes to external functions.Default to
payable
for internal functions. Sincepayable
is less restrictive, defaulting topayable
for internal functions allows us to generate strict checks for user specifiednonpayable
everywhere. However, care must be taken for external functions, which should still default tononpayable
to not break developer expectations.
Impact Details
In certain situations, this might lead to impactful scenarios. For example, the following vault contract has a funds draining bug due to the nonpayable
check not being enforced for zerovalue
when no_mint
is set to True
.
References
https://github.com/vyperlang/vyper/blob/c8691ac5dd95623991e51205bc90a720fc513766/vyper/semantics/analysis/base.py#L25
https://github.com/vyperlang/vyper/blob/c8691ac5dd95623991e51205bc90a720fc513766/vyper/codegen/module.py#L403
Proof of Concept
Proof of Concept
Already shown in Vulnerability Details and Impact Details section.
Was this helpful?