#37584 [SC-Insight] Nonpayable Not Respected For Internal Function
Was this helpful?
Was this helpful?
Submitted on Dec 9th 2024 at 20:41:03 UTC by @anatomist for
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
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.
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
and nonpayable
decorators for internal functions. This eliminates the ambiguity since the compiler mandates payable
and nonpayable
should only be applied to external functions. The approach is similar to solidity, which doesn't have nonpayable
, but restricts payable
attributes to external functions.
Default to payable
for internal functions. Since payable
is less restrictive, defaulting to payable
for internal functions allows us to generate strict checks for user specified nonpayable
everywhere. However, care must be taken for external functions, which should still default to nonpayable
to not break developer expectations.
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
.
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
Already shown in Vulnerability Details and Impact Details section.