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
- Solidity Security Tips - Solidity patterns
- Common Vulnerability Patterns - Known issues
- Vyper Language Guide - Full guide