Library
Mastering Ethereum: Building Smart Contracts and DApps · 14 of 15
Mastering Ethereum: Building Smart Contracts and DApps
blockchain MEDIUM

Vyper — Safety by Omission and Readability as a Security Property

vyper auditability overflow clamps decorators language-design security

Key Principle

"Vyper is an experimental, contract-oriented programming language for the EVM that strives to provide superior auditability, by making it easier for developers to produce intelligible code" (Chapter 8). Its design thesis is the inverse of language power: deliberately constrain the language so that "it is virtually impossible for developers to write misleading code." Because immutable code on an irreversible ledger means "every ambiguity an auditor misses ships permanently," readability is treated as a security property — Vyper trades verbosity and expressiveness for a smaller attack surface.

Why This Matters

A 2018 study of ~1,000,000 deployed contracts classified trace vulnerabilities into three custody-failure modes (Chapter 9): Suicidal (killable by arbitrary addresses — uncontrolled selfdestruct), Greedy (reach a state where they can never release ether — funds permanently locked), and Prodigal (release ether to arbitrary addresses — funds to the wrong party). Vyper's premise: "vulnerabilities enter through code, often unintentionally." Removing powerful-but-opaque features makes misleading code harder to write and behavior visible in a linear top-to-bottom read. If safety is not enforced by the language, "developers can write unsafe code that will successfully compile and later on 'successfully' execute" (Chapter 8) — Solidity permits SafeMath and Mythril but does not require them.

Good Examples

  • Checked conversion via convert + clamps. Vyper routes every cast through convert, backed by a conversion_table and a clamp enforcing min/max bounds; out-of-range literals raise instead of truncating (Chapter 8):
    # convert.py:82
    def convert(expr, context):
        output_type = expr.args[1].s
        if output_type in conversion_table:
            return conversion_table[output_type](expr, context)
        else:
            raise Exception("Conversion to {} is invalid.".format(output_type))
    The payoff: "a cast either succeeds losslessly or raises — never silent narrowing."
  • Decorators encode invariants in the compiler. Each function carries exactly one of @public/@private; @constant "may not mutate state variables; the compiler rejects the program if it tries"; @payable is required to transfer value. A function "cannot be both @payable and @constant" — "a function that transfers value has by definition updated the state, so cannot be @constant" (Chapter 8). Violations fail compilation, not production.
  • Compiler-level overflow protection (LLL clamps). Vyper compiles .vy → bytecode + ABI via an intermediate LLL stage, "the injection point for non-disableable safety 'clamps'" applied "on every literal load, function-argument pass, and variable assignment; cannot be disabled" (Chapter 8), plus a SafeMath-equivalent for integer arithmetic. Motivating failure: BeautyChain (BEC) ERC20, April 2018 — an integer overflow let a malicious transfer mint over 5.7×10^58 tokens; "the overflow-as-mint failure mode is exactly what compiler-level clamping prevents."
  • Declare-before-use ordering (inherited from Python): "If theBool was declared below topFunction or if topFunction was declared below lowerFunction this contract would not compile" — keeps control and data flow followable top-to-bottom for auditors (Chapter 8).

Counterpoints

Vyper's deliberate omissions, each removing a hiding place (Chapter 8/9):

  • No modifiers. "modifiers are pervasive: they can mutate state, not just check it." The failure mode is the reviewer's false assumption — a modifier that looks like a guard silently advances state (e.g. stageTimeConfirmation moving stage from SafeStage to DangerStage). Vyper's fix: inline asserts; make state changes explicit in the body.
  • No class inheritance (no multiple inheritance, no polymorphism). Inheritance "forces coders and auditors to jump across files to reconstruct behavior"; multiple inheritance "compounds this into code too complex to reason about." Tradeoff: lost code reuse.
  • No inline assembly. Direct EVM-instruction access (e.g. 3 0x80 mload add 0x80 mstore) is omitted: Vyper "considers the loss of readability to be too high a price to pay for the extra power." Tradeoff: lost low-level control.
  • No function overloading. With overloading "a reader/auditor can't tell which f runs from the call site alone." Vyper bans it so the name-to-implementation mapping is 1:1.
  • No implicit typecasting. Solidity's uint16(0x12345678) yields 0x5678, silently losing the high half with no error — "exactly the kind of bug that survives audit and then loses funds."

[DATED 2018: Solidity 0.8+ folds overflow checks in by default (obsoleting SafeMath), narrowing Vyper's advantage on that point; security-by-maturity now also leans on audited libraries (OpenZeppelin), not language minimalism alone.]

Key Quotes

"Vyper is designed to make it easier to write secure code, or equally to make it more difficult to accidentally write misleading or vulnerable code." — Antonopoulos & Wood, Chapter 9 "If safety is not enforced by the language, developers can write unsafe code that will successfully compile and later on 'successfully' execute." — Antonopoulos & Wood, Chapter 8

Rules of Thumb

  • Prefer explicit-and-local: make checks (assert) and state changes visible in the function body, not hidden in modifiers.
  • Follow Condition → Effects → Interaction order — run value-moving interactions last (the checks-effects-interactions reentrancy defense).
  • Treat every implicit narrowing cast as a latent funds-loss bug; force checked conversions.
  • Push safety into the compiler (non-disableable clamps) rather than relying on optional tooling.
  • When auditability and expressiveness conflict, bias toward "correctness, at the expense of some flexibility" (Chapter 8).

Related References