接口

接口是一组用于实现智能合约之间通信的函数定义。合约接口定义了该合约所有对外可用的函数。通过导入接口,您的合约现在知道如何调用其他合约中的这些函数。

声明和使用接口

接口可以通过内联定义或从单独的文件导入添加到合约中。

interface 关键字用于定义内联外部接口

interface FooBar:
    def calculate() -> uint256: view
    def test1(): nonpayable

然后可以使用定义的接口来进行外部调用,前提是给定合约地址

@external
def test(foobar: FooBar):
    foobar.calculate()

接口名称也可以用作存储变量的类型注释。然后,您将地址值分配给变量以访问该接口。请注意,可以将地址强制转换为接口,例如 FooBar(<address_var>)

foobar_contract: FooBar

@external
def __init__(foobar_address: address):
    self.foobar_contract = FooBar(foobar_address)

@external
def test():
    self.foobar_contract.calculate()

指定 payablenonpayable 注释表示对外部合约进行的调用能够更改存储,而 view pure 调用将使用 STATICCALL,确保在执行期间不会更改存储。此外,payable 允许在调用时发送非零值。

interface FooBar:
    def calculate() -> uint256: pure
    def query() -> uint256: view
    def update(): nonpayable
    def pay(): payable

@external
def test(foobar: FooBar):
    foobar.calculate()  # cannot change storage
    foobar.query()  # cannot change storage, but reads itself
    foobar.update()  # storage can be altered
    foobar.pay(value=1)  # storage can be altered, and value can be sent

Vyper 提供了在进行外部调用时设置以下附加关键字参数的选项

关键字

描述

gas

指定调用的 gas 值

value

指定与调用一起发送的以太坊数量

skip_contract_check

放弃 EXTCODESIZERETURNDATASIZE 检查

default_return_value

如果未返回值,则指定默认返回值

default_return_value 参数可用于处理受 Solidity 中缺少返回值错误影响的 ERC20 代币,方法类似于 OpenZeppelin 的 safeTransfer

ERC20(USDT).transfer(msg.sender, 1, default_return_value=True) # returns True
ERC20(USDT).transfer(msg.sender, 1) # reverts because nothing returned

警告

当使用 skip_contract_check=True 并且被调用的函数返回数据时(例如:x: uint256 = SomeContract.foo(skip_contract_check=True)),编译器不提供关于返回值有效性的保证。换句话说,如果被调用的合约不存在,会发生什么是不确定的行为。特别是,返回值可能指向垃圾内存。因此,建议仅在已手动确保被调用合约在调用时存在的调用中使用 skip_contract_check=True

导入接口

接口通过 importfrom ... import 语句导入。

导入的接口使用标准 Vyper 语法编写。导入接口时,每个函数的主体都会被忽略。如果您正在定义一个独立的接口,通常通过使用 pass 语句来指定它

@external
def test1():
    pass

@external
def calculate() -> uint256:
    pass

您还可以导入一个完全实现的合约,Vyper 会自动将其转换为接口。合约甚至可以导入自身以访问其自身的接口。

import greeter as Greeter

name: public(String[10])

@external
def __init__(_name: String[10]):
    self.name = _name

@view
@external
def greet() -> String[16]:
    return concat("Hello ", Greeter(msg.sender).name())

通过 import 导入

使用绝对 import 语句,您 **必须** 包含一个别名作为导入包的名称。在以下示例中,如果不包含 as Foo,将引发编译错误

import contract.foo as Foo

通过 from ... import 导入

使用 from,您可以执行绝对和相对导入。您可以选择性地包含一个别名 - 如果您不包含,接口的名称将与文件相同。

# without an alias
from contract import foo

# with an alias
from contract import foo as Foo

通过在合约名称前添加点可以进行相对导入。一个前导点表示从当前包开始的相对导入。两个前导点表示从当前包的父级开始的相对导入

from . import foo
from ..interfaces import baz

搜索接口文件

查找要导入的文件时,Vyper 将首先搜索与正在编译的合约相同的文件夹。对于绝对导入,它还会搜索相对于项目的根路径。Vyper 首先检查带有 .vy 后缀的文件名,然后检查 .json

使用命令行编译器时,根路径默认为当前工作目录。您可以使用 -p 标志更改它

$ vyper my_project/contracts/my_contract.vy -p my_project

在上面的示例中,my_project 文件夹被设置为根路径。合约不能执行超出顶层文件夹的相对导入。

内置接口

Vyper 包含常见的内置接口,例如 ERC20ERC721。这些从 vyper.interfaces 导入

from vyper.interfaces import ERC20

implements: ERC20

您可以在 Vyper GitHub 存储库中查看所有可用的内置接口。

实现接口

您可以使用 implements 语句为您的合约定义接口

import an_interface as FooBarInterface

implements: FooBarInterface

这会从 an_interface.vy(或如果使用 ABI json 接口类型,则为 an_interface.json)中的 vyper 文件导入定义的接口,并确保您当前的合约实现了所有必要的外部函数。如果合约中不包含任何接口函数,它将无法编译。这在开发围绕 ERC20 等定义良好的标准的合约时特别有用。

注意

实现具有需要上限的返回值的函数(例如 BytesDynArrayString)的接口,接口中定义的上限表示实现的下限。假设函数 my_func 在接口中返回一个值 String[1],这意味着对于 my_func 的实现函数,返回值必须至少具有长度 1。这种行为在未来可能会改变。

提取接口

Vyper 具有内置的格式选项,允许您轻松创建自己的 Vyper 接口。

$ vyper -f interface examples/voting/ballot.vy

# Functions

@view
@external
def delegated(addr: address) -> bool:
    pass

# ...

如果您想对另一个合约进行外部调用,Vyper 还提供了一个外部接口提取实用程序。

$ vyper -f external_interface examples/voting/ballot.vy

# External Contracts
interface Ballot:
    def delegated(addr: address) -> bool: view
    def directlyVoted(addr: address) -> bool: view
    def giveRightToVote(voter: address): nonpayable
    def forwardWeight(delegate_with_weight_to_forward: address): nonpayable
    # ...

然后可以轻松地将输出复制粘贴到目标位置。