理解可见性、可变性、构造函数、循环、断言和重入保护。
在 Vyper 里,函数定义和控制流几乎就是安全边界本身。可见性、可变性、循环上界和重入保护, 都直接体现在语法层里,而不是藏在惯例或辅助库中。
Vyper 有三种可见性级别:
| 标记 | 含义 |
|---|---|
@external | 进入 ABI 选择器表,可被外部交易或其他合约调用 |
@internal(默认) | 只在合约内部可用,外部调用者不可访问 |
@deploy | 构造阶段执行,目前仅用于 __init__() |
外部函数是合约接口的一部分,只能通过交易或外部合约调用。
vyper
@external
def add_seven(a: int128) -> int128:
return a + 7
@external
def add_with_default(a: uint256, b: uint256 = 3) -> uint256:
return a + b一个 Vyper 合约不能在两个外部函数之间直接互调。如果确实需要,可以通过接口实现。
默认参数与 ABI
对于带默认参数的外部函数(如 def my_func(x: uint256, b: uint256 = 1)),
编译器会基于 N 个默认参数生成 N+1 个重载函数选择器。
例如 withdraw(uint256,address,address) 和 withdraw(uint256) 是两个不同的选择器。
内部函数通过 self 对象调用:
vyper
def _times_two(amount: uint256) -> uint256:
return amount * 2
@external
def calculate(amount: uint256) -> uint256:
return self._times_two(amount)从导入模块调用内部函数时,使用模块名前缀:
vyper
import calculator_library
@external
def calculate(amount: uint256) -> uint256:
return calculator_library._times_two(amount)标记内部函数为 @payable 表示它可以访问 msg.value。一个 @nonpayable 的内部函数可以被外部 @payable 函数调用,但它自身无法访问 msg.value。
从 v0.4.0 起,@internal 装饰器是可选的——没有可见性装饰器的函数默认为内部函数。
__init__() 是特殊的初始化函数,仅在部署时调用一次。必须使用 @deploy 装饰器:
vyper
owner: address
@deploy
def __init__():
self.owner = msg.sender常见用途:
可变性标记描述函数与状态和 ETH 的交互方式:
| 标记 | 含义 |
|---|---|
@pure | 不读合约状态,也不读环境变量 |
@view | 可读状态,不改状态 |
@nonpayable(默认) | 可读写状态,但不能接收 ETH |
@payable | 可读写状态,可接收和访问 msg.value |
vyper
@view
@external
def readonly():
# 不能写状态
...
@payable
@external
def send_me_money():
# 可以接收 ETH
...@view 函数不能调用可变(@payable 或 @nonpayable)函数。所有外部调用使用 STATICCALL 操作码。@pure 函数不能调用非 @pure 函数。@nonpayable。内部函数的 nonpayable 行为
@nonpayable 在内部函数上不是严格强制的。外部 @payable 函数可以调用内部 @nonpayable 函数,
但该内部函数无法访问 msg.value。
@nonreentrant 装饰器为函数设置全局重入锁。当任何 @nonreentrant 函数正在执行时,
外部合约回调到同合约的任何其他 @nonreentrant 函数都会导致交易回滚。
vyper
@external
@nonreentrant
def make_a_call(_addr: address):
# 此函数受重入保护
...工作原理:在函数入口将特定存储槽设为"锁定"值,在出口设为"解锁"值。 入口检测到"锁定"状态时直接 revert。
使用限制:
@pure 函数上@view 函数上(仅检查锁状态,不修改)__default__ 函数上(但会导致合约拒绝来自回调的 ETH 转账)@nonreentrant 函数调用另一个 @nonreentrant 函数使用 vyper -f layout 可以查看重入锁在存储中的物理位置。默认分配在 slot 0。
重入锁的 gas 成本
解锁值为 3,锁定值为 2。使用非零值是为了利用 Berlin 硬分叉后的 net gas metering, 重入锁的净成本约为 2300 gas。0.3.4 之前的解锁/锁定值是 0 和 1。
从 0.4.2 起,#pragma nonreentrancy on 可以为文件中所有外部函数和公共 getter 自动启用重入保护
(constant 和 immutable 的 getter 除外)。
vyper
# pragma nonreentrancy on
x: public(uint256) # 受重入保护
y: public(reentrant(uint256)) # 不受保护
@external
def make_a_call(addr: address):
# 自动受重入保护
...
@external
@reentrant
def callback(addr: address):
# 显式允许重入
...#pragma nonreentrancy off| 装饰器 | 说明 |
|---|---|
@external | 函数可被外部调用,进入运行时选择器表 |
@internal | 函数只能在当前合约内调用 |
@deploy | 仅在部署时调用 |
@pure | 不读合约状态或环境变量 |
@view | 不修改合约状态 |
@payable | 可接收 ETH |
@nonreentrant | 不能在外部调用期间被回调 |
@raw_return | 返回原始字节,不做 ABI 编码(仅 @external 函数) |
@raw_return 装饰器让函数直接返回原始字节,跳过 ABI 编码。适用于代理合约和需要原样转发返回数据的场景。
vyper
@external
@payable
@raw_return
def forward_call(target: address) -> Bytes[1024]:
return raw_call(target, msg.data, max_outsize=1024, value=msg.value, is_delegate_call=True)限制:
@external 函数上Bytes[N]__default__() 上)@internal 函数上.vyi 文件)中使用注意
调用 @raw_return 函数时应使用 raw_call 而非接口调用,因为返回数据不是 ABI 编码的。
默认函数在没有匹配的函数选择器时执行(包括直接发送 ETH 的情况)。
相当于 Solidity 中的 fallback 和 receive 的组合。
vyper
event Payment:
amount: uint256
sender: indexed(address)
@external
@payable
def __default__():
log Payment(msg.value, msg.sender)注意事项:
@external,不能接受参数@payable,合约可以接收纯 ETH 转账__default__,编译器会生成一个 REVERT 版本send 调用只附带 2300 gas stipend,能做的事很有限(写存储、创建合约、外部调用都超过这个限额)msg.sender、msg.value、msg.gasvyper
if CONDITION:
...
elif OTHER_CONDITION:
...
else:
...与 Python 不同,Vyper 不允许非布尔类型在 if 条件中隐式转换。if 1: pass 会编译失败。
Vyper 的循环必须有编译期可知的上界:
vyper
foo: int128[3] = [4, 23, 42]
for i: int128 in foo:
...
# 也可以迭代字面量数组
for i: int128 in [4, 23, 42]:
...限制:不能迭代多维数组;迭代期间不能修改被迭代的数组。
vyper
# 固定上界
for i: uint256 in range(100):
...
# 变量上界 + 编译期 bound
for i: uint256 in range(stop, bound=100):
...
# 起止范围(固定字面量)
for i: uint256 in range(10, 20):
...
# 运行时起止 + 编译期 bound
for i: uint256 in range(start, end, bound=100):
...当 stop 可能小于 bound 时,使用 range(min(stop, N), bound=N) 来避免运行时 revert。
这对将大数组操作分块到多个交易中特别有用。
vyper
@external
@nonreentrant
def withdraw():
assert msg.sender == self.owner, "Not owner"
...
@external
def restricted():
raise "Operation not allowed"assert:条件为 false 时回滚,可附带错误消息raise:直接回滚vyper
x: uint256 = 42
print(x, "hello")print 通过静态调用 console 地址 (0x000000000000000000636F6E736F6C652E6C6F67) 实现。
默认模式与 titanoboa 兼容,使用 hardhat_compat=True 可适配 Hardhat。
控制流的审计视角
当你读一段 Vyper 代码时,优先找 @external、@payable、@nonreentrant 和 extcall。
这些标记几乎总能快速暴露资金流和风险边界。
本页目录