Vyper Function Visibility & Payable Decorators Explained
Vyper uses decorators to declare function visibility and payable status. This guide shows how @external, @internal, and @payable work together, with a concrete example, limits, and common mistakes.
17 Aug 2026, 01:11 UTC

Why Function Visibility Matters
In Ethereum smart contracts, who can call a function and what data the function can accept are security‑critical decisions. Vyper enforces these rules at compile time using decorators that annotate each function. A mis‑decorated function can silently expose an attack vector or cause user‑facing transactions to revert.
How Vyper Decorators Work
Vyper offers two visibility decorators and one modifier:
@external– the function is an entry point that can be called from outside the contract. It is the only visibility that acceptsmsg.senderandmsg.valuefrom the transaction context.@internal– the function can only be called from within the contract’s own code. External callers cannot reach it directly.@payable– attached to an@externalfunction, it allows the function to receive Ether. Without it, any transaction that sends value will automatically revert.
Vyper does not support function overloading; each name must be unique, so decorators cannot be combined with overloaded signatures. Internal calls bypass visibility checks and can skip the @payable requirement, but developers must still guard against re‑entrancy.
Concrete Example
Below is a minimal Vyper contract that demonstrates the three decorators. Deploy it with a test network (e.g., Goerli) and interact with it using a wallet or web3 script.
# @version ^0.3.7
# Storage
balance: uint256
# Events
Withdrawn: event({
to: address,
amount: uint256
})
# External entry point that accepts Ether
@external
@payable
def deposit() -> None:
"""Increase the caller’s balance by the sent amount."""
self.balance += msg.value
# External function that can be called without sending Ether
@external
def get_balance() -> uint256:
"""Return the contract’s stored balance."""
return self.balance
# External function that triggers a withdrawal
@external
def withdraw(to: address, amount: uint256) -> None:
"""Transfer amount of Wei to to if the caller has enough balance."""
assert self.balance >= amount, "Insufficient balance"
self.balance -= amount
# Internal helper – no @payable needed
self._log_withdrawal(to, amount)
# Internal helper that emits an event
@internal
def _log_withdrawal(to: address, amount: uint256) -> None:
log Withdrawn(to=to, amount=amount)
Key points from the example:
- The
deposit()function is@external @payable, so users can send Ether to it. - The
get_balance()function is@externalbut not@payable, so sending Ether to it will revert. - The
withdraw()function is@externaland internally calls_log_withdrawal(), which is@internaland cannot be triggered by an external transaction.
Limitations & Common Mistakes
While the decorators are straightforward, several pitfalls can trip developers:
- Missing @payable – If a function is intended to receive Ether but lacks
@payable, any transaction that sends value will revert. This often appears as a “transaction failed” error with no clear reason. - Exposing internal logic – Marking a function
@externalwhen it should be internal opens it to direct calls, potentially bypassing access control logic. Always review whether a function should be callable from outside. - Re‑entrancy via internal calls – Internal functions can be called from external entry points. If an internal function modifies state and then sends Ether, a re‑entrancy attack can occur. Use the checks‑effects‑interactions pattern or Vyper’s
reentrancy_guardif available. - No overloading – Because Vyper cannot overload functions, you cannot have two functions named
withdrawwith different parameters. Use distinct names or combine logic into a single function. - Visibility mismatch – Attempting to call an
@internalfunction from an external transaction will revert with “invalid function selector”. Ensure the intended callers match the decorator.
Practical Verification Steps
- Deploy the contract to a testnet. Verify the bytecode contains the expected decorators by inspecting the source in a Vyper compiler or using an online verifier.
- Send 1 ETH to
deposit()via a transaction withvalue = 1 ether. After confirmation, callget_balance()and check that the returned value equals the sent amount. - Attempt to send ETH to
withdraw()(orget_balance()). The transaction should revert. In Remix or Hardhat, the revert reason will be “revert: insufficient balance” or “revert: transaction reverted” if no custom reason. - Call
_log_withdrawal()directly from an external transaction. The call will revert with “invalid function selector”. This confirms the@internaldecorator is enforced. - Check the event logs after a successful
withdraw()call. TheWithdrawnevent should appear with the correcttoandamountvalues.
When writing production contracts, always double‑check that:
- Functions that should accept Ether are annotated with
@payable. - Functions that should not be exposed externally are marked
@internal. - State‑changing external calls follow the checks‑effects‑interactions pattern to mitigate re‑entrancy.
Conclusion
Vyper’s decorators provide a clear, compile‑time guarantee of function visibility and payable status. By correctly applying @external, @internal, and @payable, developers can prevent accidental exposure of entry points, avoid silent transaction failures, and maintain robust security posture. Use the example contract and verification steps above as a starting point for building your own Vyper applications with confidence.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.