Vyper Security Tips

Write secure Vyper smart contracts. Vyper is designed with security in mind, featuring intentional limitations that reduce attack surface. This guide covers...

Last updated: January 14, 2026

Vyper Security Tips

Write secure Vyper smart contracts.

Overview

Vyper is designed with security in mind, featuring intentional limitations that reduce attack surface. This guide covers Vyper-specific security patterns.


Vyper's Security Features

Built-in Protections

Feature Benefit
Bounds checking Array access always validated
Integer protection Overflow/underflow reverts
No recursive calls Reentrancy prevented
Single inheritance No diamond problem
No inline assembly Can't bypass safety
No modifiers Logic is explicit

Access Control

Basic Pattern

# @version 0.3.9

owner: public(address)

@external
def __init__():
    self.owner = msg.sender

@external
def restricted_function():
    assert msg.sender == self.owner, "Not owner"
    # ... protected logic

Two-Step Transfer

owner: public(address)
pending_owner: public(address)

@external
def transfer_ownership(new_owner: address):
    assert msg.sender == self.owner, "Not owner"
    assert new_owner != empty(address), "Invalid address"
    self.pending_owner = new_owner

@external
def accept_ownership():
    assert msg.sender == self.pending_owner, "Not pending owner"
    self.owner = self.pending_owner
    self.pending_owner = empty(address)

Role-Based Access

ADMIN_ROLE: constant(bytes32) = keccak256("ADMIN_ROLE")
OPERATOR_ROLE: constant(bytes32) = keccak256("OPERATOR_ROLE")

roles: HashMap[bytes32, HashMap[address, bool]]

@internal
def _only_role(role: bytes32):
    assert self.roles[role][msg.sender], "Missing role"

@external
def admin_function():
    self._only_role(ADMIN_ROLE)
    # ... admin logic

@external
def grant_role(role: bytes32, account: address):
    self._only_role(ADMIN_ROLE)
    self.roles[role][account] = True

Input Validation

Always Validate

MAX_FEE: constant(uint256) = 1000  # 10%
FEE_DENOMINATOR: constant(uint256) = 10000

fee: public(uint256)

@external
def set_fee(new_fee: uint256):
    assert msg.sender == self.owner, "Not owner"
    assert new_fee <= MAX_FEE, "Fee too high"
    self.fee = new_fee

@external
def transfer(to: address, amount: uint256):
    assert to != empty(address), "Invalid recipient"
    assert amount > 0, "Amount must be positive"
    assert self.balances[msg.sender] >= amount, "Insufficient balance"

    self.balances[msg.sender] -= amount
    self.balances[to] += amount

Address Validation

@external
def set_admin(new_admin: address):
    assert msg.sender == self.owner, "Not owner"
    assert new_admin != empty(address), "Cannot be zero address"
    assert new_admin != self.admin, "Already admin"
    assert new_admin.is_contract == False, "Cannot be contract"

    self.admin = new_admin

External Calls

Check Return Values

from vyper.interfaces import ERC20

@external
def transfer_tokens(token: address, to: address, amount: uint256):
    assert msg.sender == self.owner, "Not owner"
    assert amount > 0, "Invalid amount"

    # Always check return value
    success: bool = ERC20(token).transfer(to, amount)
    assert success, "Transfer failed"

Handle Non-Standard Tokens

@external
def safe_transfer(token: address, to: address, amount: uint256):
    # Some tokens don't return bool (USDT)
    response: Bytes[32] = raw_call(
        token,
        concat(
            method_id("transfer(address,uint256)"),
            convert(to, bytes32),
            convert(amount, bytes32)
        ),
        max_outsize=32,
    )

    if len(response) > 0:
        assert convert(response, bool), "Transfer failed"

State Management

Initialization

initialized: bool

@external
def initialize(admin: address, fee: uint256):
    assert not self.initialized, "Already initialized"
    assert admin != empty(address), "Invalid admin"

    self.admin = admin
    self.fee = fee
    self.initialized = True

State Machine Pattern

enum State:
    CREATED
    ACTIVE
    PAUSED
    FINALIZED

state: public(State)

@internal
def _require_state(expected: State):
    assert self.state == expected, "Invalid state"

@external
def activate():
    self._require_state(State.CREATED)
    assert msg.sender == self.owner, "Not owner"
    self.state = State.ACTIVE

@external
def pause():
    self._require_state(State.ACTIVE)
    assert msg.sender == self.owner, "Not owner"
    self.state = State.PAUSED

Arithmetic Safety

Built-in Protection

# Vyper automatically checks overflow/underflow
@external
def add_balance(amount: uint256):
    # Reverts if overflow would occur
    self.balance += amount

@external
def subtract_balance(amount: uint256):
    # Reverts if underflow would occur
    self.balance -= amount

Division Considerations

PRECISION: constant(uint256) = 10 ** 18

@external
def calculate_share(amount: uint256, total: uint256) -> uint256:
    assert total > 0, "Division by zero"

    # Multiply before divide for precision
    return amount * PRECISION / total

Events

Emit for All State Changes

event OwnershipTransferred:
    previous_owner: indexed(address)
    new_owner: indexed(address)

event FeeUpdated:
    old_fee: uint256
    new_fee: uint256

event Deposit:
    user: indexed(address)
    amount: uint256

@external
def set_owner(new_owner: address):
    assert msg.sender == self.owner, "Not owner"
    assert new_owner != empty(address), "Invalid address"

    log OwnershipTransferred(self.owner, new_owner)
    self.owner = new_owner

Interface Implementation

Complete Interface Compliance

# @version 0.3.9

from vyper.interfaces import ERC20

implements: ERC20

name: public(String[32])
symbol: public(String[8])
decimals: public(uint8)
totalSupply: public(uint256)
balanceOf: public(HashMap[address, uint256])
allowance: public(HashMap[address, HashMap[address, uint256]])

event Transfer:
    sender: indexed(address)
    receiver: indexed(address)
    amount: uint256

event Approval:
    owner: indexed(address)
    spender: indexed(address)
    amount: uint256

@external
def transfer(to: address, amount: uint256) -> bool:
    assert to != empty(address), "Invalid recipient"
    assert self.balanceOf[msg.sender] >= amount, "Insufficient balance"

    self.balanceOf[msg.sender] -= amount
    self.balanceOf[to] += amount

    log Transfer(msg.sender, to, amount)
    return True

@external
def approve(spender: address, amount: uint256) -> bool:
    assert spender != empty(address), "Invalid spender"

    self.allowance[msg.sender][spender] = amount

    log Approval(msg.sender, spender, amount)
    return True

@external
def transferFrom(sender: address, receiver: address, amount: uint256) -> bool:
    assert receiver != empty(address), "Invalid recipient"
    assert self.balanceOf[sender] >= amount, "Insufficient balance"
    assert self.allowance[sender][msg.sender] >= amount, "Insufficient allowance"

    self.allowance[sender][msg.sender] -= amount
    self.balanceOf[sender] -= amount
    self.balanceOf[receiver] += amount

    log Transfer(sender, receiver, amount)
    return True

Common Patterns

Pausable

paused: public(bool)

@internal
def _when_not_paused():
    assert not self.paused, "Contract paused"

@external
def pause():
    assert msg.sender == self.owner, "Not owner"
    assert not self.paused, "Already paused"
    self.paused = True

@external
def unpause():
    assert msg.sender == self.owner, "Not owner"
    assert self.paused, "Not paused"
    self.paused = False

@external
def deposit(amount: uint256):
    self._when_not_paused()
    # ... deposit logic

Deadline/Expiry

deadline: public(uint256)

@external
def set_deadline(timestamp: uint256):
    assert msg.sender == self.owner, "Not owner"
    assert timestamp > block.timestamp, "Must be in future"
    self.deadline = timestamp

@internal
def _before_deadline():
    assert block.timestamp < self.deadline, "Deadline passed"

@external
def time_sensitive_action():
    self._before_deadline()
    # ... action

Testing

Property-Based Tests

# Using Ape or Brownie

def test_owner_only(vault, user):
    with reverts("Not owner"):
        vault.set_fee(100, sender=user)

def test_input_validation(vault, owner):
    with reverts("Fee too high"):
        vault.set_fee(10001, sender=owner)  # > MAX_FEE

def test_arithmetic_safety(token, user, amount):
    initial = token.balanceOf(user)
    if amount > initial:
        with reverts():  # Underflow reverts
            token.transfer(other, amount, sender=user)

Vyper vs Solidity Comparison

Pattern Vyper Solidity
Access Control Explicit asserts Modifiers
Reentrancy Guard Not needed ReentrancyGuard
Overflow Protection Built-in Built-in (0.8+)
Interface Check implements: Manual
Inheritance Single only Multiple

Next Steps