这篇指南详细介绍了如何使用ApeWorX框架在以太坊Sepolia测试网部署ERC4626合规的质押储蓄合约。文章深入讲解了创建和部署ERC20代币和铁定盈利的储蓄合约的整个过程,包括必要的依赖、代码实现以及如何进行智能合约测试和部署,适合有一定基础的智能合约开发者。
提示
虽然本指南演示了如何在以太坊 Sepolia 测试网中进行部署,但智能合约代码兼容任何基于 EVM 的区块链。欢迎根据其他网络(如 Base、Optimism、Avalanche 等)调整部署流程。请查看 QuickNode 支持的所有链 在这里。
请注意,本指南中展示的代码仅供教育用途,并作为开发的起点。它尚未经过彻底测试以用于生产环境。如果你对 Solidity 版本感兴趣,请查看此 指南。
ApeWorX,简称“ape”,是一个强大且灵活的开发框架,用于在 EVM 兼容区块链上进行智能合约开发。在本指南中,我们将创建和部署一个符合 ERC4626 标准的质押库智能合约,该合约使用 Vyper 编写,并使用 ApeWorX 框架进行测试。
让我们开始吧!
依赖项 | 版本 |
---|---|
Python | ^3.9.2 |
pip | 24.2 |
eth-ape | 0.8.10 |
Ape 是一个基于 Python 的开发框架和以太坊生态系统智能合约工具套件。它提供了一整套用于编写、测试和部署智能合约的工具。Ape 支持多种语言进行智能合约开发,包括 Solidity 和 Vyper。本指南将重点介绍使用 Vyper 创建智能合约,Vyper 是一种面向合约的 Python 风格的编程语言,旨在以安全性和审计的便利性为目标,优于 Solidity,因此对新开发者而言是一个极好的选择。
首先,让我们设置我们的 QuickNode 端点,以便我们能够与以太坊网络进行通信。然后,我们将初始化一个 Ape 项目目录,并安装必要的依赖项。
要与以太坊网络互动并部署本文所述的智能合约,我们需要一个 RPC 端点。请按照以下步骤使用 QuickNode 创建一个:
使用以下命令创建一个项目目录:
mkdir staking-vault-ape
cd staking-vault-ape
使用 ape 时,设置虚拟环境是一种良好的实践。使用以下命令设置一个虚拟环境:
python -m venv vyper-env
根据你的 Python 安装配置,你可能需要使用
python3
然后激活它:
source vyper-env/bin/activate
接下来,安装 Ape:
pip install eth-ape
然后,初始化一个新的 Ape 项目:
ape init
在系统提示项目名称时,输入“staking-vault-ape”。初始化后,你将看到以下目录结构:
├── contracts // 用于 Solidity 和 Vyper 智能合约
├── scripts // 部署和与智能合约互动的脚本
├── tests // 本地测试
├── vyper-env
然后,如果你尚未安装 Vyper,可以通过 ape 插件进行安装:
ape plugins install vyper
接下来,在代码编辑器中打开项目,并更新项目目录中的 ape-config.yaml
文件,以在 YOUR_QUICKNODE_RPC_URL
占位符中包含你的 QuickNode HTTP 提供程序 URL。
name: ape-staking-vault
node:
ethereum:
sepolia:
uri: YOUR_QUICKNODE_RPC_URL
记得保存文件。
接下来,让我们配置将用于智能合约部署的账户。在终端中运行以下命令,并按照提示操作(准备好你的私钥):
ape accounts import my_account
my_account
是一个别名,可以根据自己的喜好进行修改。你也可以在 这里 查看其他账户选项方法。
系统会提示你输入私钥和密码。
在下一部分中,我们将设置创建、部署和测试质押奖励智能合约所需的智能合约和脚本。
我们来创建一个简单的 ERC-20 代币,用于质押。该代币命名为 StakingToken
(ST),在合约部署时将铸造 1000000
代币至合约部署者。
创建一个新的文件 contracts/StakingToken.vy
并添加以下 Vyper 代码:
## @version 0.3.7
from vyper.interfaces import ERC20
implements: ERC20
## ERC20 代币元数据
NAME: constant(String[20]) = "StakingToken"
SYMBOL: constant(String[5]) = "ST"
DECIMALS: constant(uint8) = 18
## ERC20 状态变量
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
owner: public(address)
isMinter: public(HashMap[address, bool])
nonces: public(HashMap[address, uint256])
DOMAIN_SEPARATOR: public(bytes32)
DOMAIN_TYPE_HASH: constant(bytes32) = keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)')
PERMIT_TYPE_HASH: constant(bytes32) = keccak256('Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)')
@external
def __init__():
self.owner = msg.sender
self.totalSupply = 1000000
self.balanceOf[msg.sender] = 1000000
self.DOMAIN_SEPARATOR = keccak256(
concat(
DOMAIN_TYPE_HASH,
keccak256(NAME),
keccak256("1.0"),
_abi_encode(chain.id, self)
)
)
@pure
@external
def name() -> String[20]:
return NAME
@pure
@external
def symbol() -> String[5]:
return SYMBOL
@pure
@external
def decimals() -> uint8:
return DECIMALS
@external
def transfer(receiver: address, amount: uint256) -> bool:
assert receiver not in [empty(address), self]
self.balanceOf[msg.sender] -= amount
self.balanceOf[receiver] += amount
log Transfer(msg.sender, receiver, amount)
return True
@external
def transferFrom(sender: address, receiver: address, amount: uint256) -> bool:
assert receiver not in [empty(address), self]
self.allowance[sender][msg.sender] -= amount
self.balanceOf[sender] -= amount
self.balanceOf[receiver] += amount
log Transfer(sender, receiver, amount)
return True
@external
def approve(spender: address, amount: uint256) -> bool:
"""
@param spender 将代表所有者执行操作的地址。
@param amount 要转移的代币数量。
"""
self.allowance[msg.sender][spender] = amount
log Approval(msg.sender, spender, amount)
return True
@external
def burn(amount: uint256) -> bool:
"""
@notice 从发送者的钱包中销毁提供的代币数量。
@param amount 要销毁的代币数量。
"""
self.balanceOf[msg.sender] -= amount
self.totalSupply -= amount
log Transfer(msg.sender, empty(address), amount)
return True
@external
def mint(receiver: address, amount: uint256) -> bool:
"""
@notice 铸造代币的函数
@param receiver 接收铸造代币的地址。
@param amount 要铸造的代币数量。
@return 一个布尔值,指示操作是否成功。
"""
assert msg.sender == self.owner or self.isMinter[msg.sender], "访问被拒绝。"
assert receiver not in [empty(address), self]
self.totalSupply += amount
self.balanceOf[receiver] += amount
log Transfer(empty(address), receiver, amount)
return True
@external
def permit(owner: address, spender: address, amount: uint256, expiry: uint256, signature: Bytes[65]) -> bool:
"""
@notice
通过所有者的签名批准花费所有者的代币。
请参见https://eips.ethereum.org/EIPS/eip-2612。
@param owner 签署许可证的资金来源地址。
@param spender 被允许花费资金的地址。
@param amount 要花费的代币数量。
@param expiry 超过此时间戳后许可证将不再有效。
@param signature 有效 secp256k1 签名的许可证,由所有者编码为 r,s,v。
@return 如果事务成功完成,则返回 True
"""
assert owner != empty(address) # dev: 无效的所有者
assert expiry == 0 or expiry >= block.timestamp # dev: 许可证过期
nonce: uint256 = self.nonces[owner]
digest: bytes32 = keccak256(
concat(
b'\x19\x01',
self.DOMAIN_SEPARATOR,
keccak256(
_abi_encode(
PERMIT_TYPE_HASH,
owner,
spender,
amount,
nonce,
expiry,
)
)
)
)
# 注意:签名按 r, s, v 打包
r: uint256 = convert(slice(signature, 0, 32), uint256)
s: uint256 = convert(slice(signature, 32, 32), uint256)
v: uint256 = convert(slice(signature, 64, 1), uint256)
assert ecrecover(digest, v, r, s) == owner # dev: 签名无效
self.allowance[owner][spender] = amount
self.nonces[owner] = nonce + 1
log Approval(owner, spender, amount)
return True
上述代码是合规的 ERC-20 代币。你可以在 这里 查看 EIP 标准,或查看我们的指南以获取更多信息:如何创建和部署 ERC20 代币
在本指南中,我们将实现一个符合 ERC4626 标准的质押库。ERC4626 标准化了代币化库的接口,使其更容易与其他 DeFi 协议进行交互和集成。我们的合约将允许用户存入一个 ERC20 代币(资产),并获得份额作为回报,代表他们在库中的权益。
现在,创建一个名为 contracts/StakingVault.vy
的新文件,并输入以下代码:
## @version 0.3.7
from vyper.interfaces import ERC20
from vyper.interfaces import ERC4626
implements: ERC20
implements: ERC4626
###### ERC20 ###
totalSupply: public(uint256)
balanceOf: public(HashMap[address, uint256])
allowance: public(HashMap[address, HashMap[address, uint256]])
NAME: constant(String[10]) = "Test Vault"
SYMBOL: constant(String[5]) = "vTEST"
DECIMALS: constant(uint8) = 18
event Transfer:
sender: indexed(address)
receiver: indexed(address)
amount: uint256
event Approval:
owner: indexed(address)
spender: indexed(address)
allowance: uint256
###### ERC4626 ###
asset: public(ERC20)
event Deposit:
depositor: indexed(address)
receiver: indexed(address)
assets: uint256
shares: uint256
event Withdraw:
withdrawer: indexed(address)
receiver: indexed(address)
owner: indexed(address)
assets: uint256
shares: uint256
@external
def __init__(asset: ERC20):
self.asset = asset
@view
@external
def name() -> String[10]:
return NAME
@view
@external
def symbol() -> String[5]:
return SYMBOL
@view
@external
def decimals() -> uint8:
return DECIMALS
@external
def transfer(receiver: address, amount: uint256) -> bool:
self.balanceOf[msg.sender] -= amount
self.balanceOf[receiver] += amount
log Transfer(msg.sender, receiver, amount)
return True
@external
def approve(spender: address, amount: uint256) -> bool:
self.allowance[msg.sender][spender] = amount
log Approval(msg.sender, spender, amount)
return True
@external
def transferFrom(sender: address, receiver: address, amount: uint256) -> bool:
self.allowance[sender][msg.sender] -= amount
self.balanceOf[sender] -= amount
self.balanceOf[receiver] += amount
log Transfer(sender, receiver, amount)
return True
@view
@external
def totalAssets() -> uint256:
return self.asset.balanceOf(self)
@view
@internal
def _convertToAssets(shareAmount: uint256) -> uint256:
totalSupply: uint256 = self.totalSupply
if totalSupply == 0:
return 0
# 注意:`shareAmount = 0` 是极为罕见的情况,不做优化
# 注意:`totalAssets = 0` 是极为罕见的情况,不做优化
return shareAmount * self.asset.balanceOf(self) / totalSupply
@view
@external
def convertToAssets(shareAmount: uint256) -> uint256:
return self._convertToAssets(shareAmount)
@view
@internal
def _convertToShares(assetAmount: uint256) -> uint256:
totalSupply: uint256 = self.totalSupply
totalAssets: uint256 = self.asset.balanceOf(self)
if totalAssets == 0 or totalSupply == 0:
return assetAmount # 1:1 价格
# 注意:`assetAmount = 0` 是极为罕见的情况,不做优化
return assetAmount * totalSupply / totalAssets
@view
@external
def convertToShares(assetAmount: uint256) -> uint256:
return self._convertToShares(assetAmount)
@view
@external
def maxDeposit(owner: address) -> uint256:
return MAX_UINT256
@view
@external
def previewDeposit(assets: uint256) -> uint256:
return self._convertToShares(assets)
@external
def deposit(assets: uint256, receiver: address=msg.sender) -> uint256:
shares: uint256 = self._convertToShares(assets)
self.asset.transferFrom(msg.sender, self, assets)
self.totalSupply += shares
self.balanceOf[receiver] += shares
log Transfer(empty(address), receiver, shares)
log Deposit(msg.sender, receiver, assets, shares)
return shares
@view
@external
def maxMint(owner: address) -> uint256:
return MAX_UINT256
@view
@external
def previewMint(shares: uint256) -> uint256:
assets: uint256 = self._convertToAssets(shares)
# 注意:Vyper 会对 if 进行懒惰评估,因此大多数时候避免了 SLOAD
if assets == 0 and self.asset.balanceOf(self) == 0:
return shares # 注意:如果尚未存入,则假设 1:1 价格
return assets
@external
def mint(shares: uint256, receiver: address=msg.sender) -> uint256:
assets: uint256 = self._convertToAssets(shares)
if assets == 0 and self.asset.balanceOf(self) == 0:
assets = shares # 注意:如果尚未存入,则假设 1:1 价格
self.asset.transferFrom(msg.sender, self, assets)
self.totalSupply += shares
self.balanceOf[receiver] += shares
log Transfer(empty(address), receiver, shares)
log Deposit(msg.sender, receiver, assets, shares)
return assets
@view
@external
def maxWithdraw(owner: address) -> uint256:
return MAX_UINT256 # 实际最大值为 `self.asset.balanceOf(self)`
@view
@external
def previewWithdraw(assets: uint256) -> uint256:
shares: uint256 = self._convertToShares(assets)
# 注意:Vyper 会对 if 进行懒惰评估,因此大多数时候避免了 SLOAD
if shares == assets and self.totalSupply == 0:
return 0 # 注意:无可赎回的部分
return shares
@external
def withdraw(assets: uint256, receiver: address=msg.sender, owner: address=msg.sender) -> uint256:
shares: uint256 = self._convertToShares(assets)
# 注意:Vyper 会对 if 进行懒惰评估,因此大多数时候避免了 SLOAD
if shares == assets and self.totalSupply == 0:
raise # 无可赎回的部分
if owner != msg.sender:
self.allowance[owner][msg.sender] -= shares
self.totalSupply -= shares
self.balanceOf[owner] -= shares
self.asset.transfer(receiver, assets)
log Transfer(owner, empty(address), shares)
log Withdraw(msg.sender, receiver, owner, assets, shares)
return shares
@view
@external
def maxRedeem(owner: address) -> uint256:
return MAX_UINT256 # 实际最大值为 `self.totalSupply`
@view
@external
def previewRedeem(shares: uint256) -> uint256:
return self._convertToAssets(shares)
@external
def redeem(shares: uint256, receiver: address=msg.sender, owner: address=msg.sender) -> uint256:
if owner != msg.sender:
self.allowance[owner][msg.sender] -= shares
assets: uint256 = self._convertToAssets(shares)
self.totalSupply -= shares
self.balanceOf[owner] -= shares
self.asset.transfer(receiver, assets)
log Transfer(owner, empty(address), shares)
log Withdraw(msg.sender, receiver, owner, assets, shares)
return assets
上述代码遵循 ERC4626 标准,同时也继承 ERC-20 标准。实现的函数包括:
欲了解有关此标准的更多信息,请查看 EIP 在这里。另外,你还可以查看我们的指南:如何在你的智能合约中使用 ERC-4626
在下一节中,我们将测试从库合约中存款和提取代币,然后再部署到像 Sepolia 这样的公共测试网。
为测试我们的 ERC4626 库合约,我们将创建一个使用 Ape 测试框架的 Python 脚本。这将使我们能够在本地测试环境中部署我们的合约,存入代币(质押)和提取代币(解除质押),确保我们的实现符合 ERC4626 标准。
在项目目录中创建一个名为 tests/test_vault.py
的新文件,并添加以下代码:
import pytest
from ape import accounts, project
from ape.exceptions import VirtualMachineError
@pytest.fixture(scope="module")
def owner(accounts):
return accounts[0]
@pytest.fixture(scope="module")
def user1(accounts):
return accounts[1]
@pytest.fixture(scope="module")
def user2(accounts):
return accounts[2]
@pytest.fixture(scope="function")
def asset_token(owner, project):
# 部署一个模拟的 ERC20 代币作为资产
return owner.deploy(project.StakingToken)
@pytest.fixture(scope="function")
def staking_reward(owner, asset_token, project):
# 部署 StakingVault 合约
return owner.deploy(project.StakingVault, asset_token.address)
def test_initial_state(staking_reward, asset_token):
assert staking_reward.name() == "Test Vault"
assert staking_reward.symbol() == "vTEST"
assert staking_reward.decimals() == 18
assert staking_reward.totalSupply() == 0
assert staking_reward.totalAssets() == 0
assert staking_reward.asset() == asset_token.address
def test_deposit_and_withdraw(staking_reward, asset_token, owner, user1):
# 为测试铸造一些代币给 user1
asset_token.mint(user1, 1000, sender=owner)
# 批准质押合约支出 user1 的代币
asset_token.approve(staking_reward.address, 500, sender=user1)
# 存入代币
staking_reward.deposit(500, user1, sender=user1)
assert staking_reward.balanceOf(user1) == 500
assert asset_token.balanceOf(staking_reward.address) == 500
# 提取代币
staking_reward.withdraw(500, user1, sender=user1)
assert staking_reward.balanceOf(user1) == 0
assert asset_token.balanceOf(user1) == 1000
def test_conversion_functions(staking_reward, asset_token, owner, user1):
# 为转换测试铸造和存入代币
asset_token.mint(user1, 1000, sender=owner)
asset_token.approve(staking_reward.address, 500, sender=user1)
staking_reward.deposit(500, user1, sender=user1)
# 测试 convertToAssets
shares = staking_reward.convertToShares(100)
assert shares == 100
# 测试 convertToShares
assets = staking_reward.convertToAssets(100)
assert assets == 100
要运行这些测试,请在终端中使用以下命令:
ape test
你将看到类似如下的输出:
tests/test_staking.py ... [100%]
============================================================================= 3 passed in 2.68s ==============================================================================
现在我们的测试已完成。让我们将智能合约部署到测试网上。
在 scripts/deploy_token.py
文件中输入以下代码:
from ape import accounts, project, networks
def main():
# 连接到 Sepolia 网络
networks.parse_network_choice("ethereum:sepolia") # 使用你在 ape-config.yaml 中定义的网络
# 加载导入的账户
account = accounts.load("my_account")
print(f"使用账户: {account.address}")
# 部署 StakingToken 合约
staking_token = account.deploy(project.StakingToken)
print(f"StakingToken 部署在: {staking_token.address}")
return staking_token
if __name__ == "__main__":
main()
使用以下终端命令进行部署:
ape run deploy_token --network ethereum:sepolia:node
在此过程中,你将被提示签署交易并输入你之前设置的账户密码。
然后,在 scripts/deploy_staking_vault.py
文件中输入以下代码:
from ape import accounts, project, networks, Contract
def main():
# 使用我们的自定义配置连接到 Sepolia 网络
networks.parse_network_choice("ethereum:sepolia")
# 加载导入的账户
account = accounts.load("my_account")
print(f"使用账户: {account.address}")
# 指定在初始化期间要使用的资产代币地址
asset_token_address = "YOUR_ERC20_TOKEN_ADDRESS" # 用你的实际部署地址替换
# 加载现有资产代币合约
asset_token = Contract(asset_token_address)
print(f"Loaded AssetToken 在: {asset_token.address}")
# 打印可用合约
print("可用合约:", project.contracts)
# 使用资产代币地址作为参数部署 StakingVault 合约
vault = account.deploy(project.StakingVault, asset_token_address)
print(f"StakingVault 库部署在: {vault.address}")
return asset_token, vault
if __name__ == "__main__":
main()
记得将 YOUR_ERC20_TOKEN_ADDRESS
替换为之前步骤中输出的代币地址。上述脚本部署了我们的 ERC4626 合规质押库,将接受我们之前部署的资产代币的存款。
使用以下终端命令进行部署:
ape run deploy_staking_vault --network ethereum:sepolia:node
你将再次被提示签署交易。一旦交易得到确认,你将看到类似如下的输出:
INFO: 连接到现有Geth节点 https://dry-omniscient-ensemble.ethereum-sepolia.quiknode.pro/[hidden].
使用账户: 0x894bC5FB859CFD53E7E59E97a5CE3bf89D84fa04
Loaded AssetToken 在: 0xCBF509F9E7251B6217A700EA7816Ce1a10894e48
可用合约: <Contracts $HOME/Documents/Code/guide-tests/con-232-guide-idea-how-to-create-smart-contracts-with-apeworxape/staking-vault-ape/contracts>
DynamicFeeTransaction:
chainId: 11155111
from: 0x894bC5FB859CFD53E7E59E97a5CE3bf89D84fa04
gas: 930942
nonce: 44
value: 0
data: 0x307836...653438
type: 2
maxFeePerGas: 10571472034
maxPriorityFeePerGas: 420184473
accessList: []
签字: [y/N]: y
输入密码以解锁 'my_account' []:
保持 'my_account' 解锁吗? [y/N]: y
INFO: 提交到 https://sepolia.etherscan.io/tx/0xa3acdd62ba2d1f422663cdc617de541d19534cdbffd9f5dda6baf81eb602dc9c
确认 (2/2): 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 2/2 [00:42<00:00, 21.45s/it]
INFO: 确认 0xa3acdd62ba2d1f422663cdc617de541d19534cdbffd9f5dda6baf81eb602dc9c (总费用支付 = 7006047733668594)
INFO: 确认 0xa3acdd62ba2d1f422663cdc617de541d19534cdbffd9f5dda6baf81eb602dc9c (总费用支付 = 7006047733668594)
SUCCESS: 合约 'StakingVault' 部署到: 0x73e78463952960200322D004dCb13602eB2F8468
StakingVault 库部署在: 0x73e78463952960200322D004dCb13602eB2F8468
就这样!你的合约现在已部署在公共测试网上。在下一节中,我们将介绍如何使这些合约开源。然而,在继续之前,请检查 Etherscan 以确保它们尚未开源(如果开源了,你可以跳过下一节)。
要使你的智能合约开源,可以在 Etherscan 等区块浏览器上验证。请按照以下步骤进行:
在下一页中,输入合约代码。对于 StakingToken
,没有构造函数参数。
对于 StakingVault
合约,你需要编码构造函数参数。你可以在相同的项目目录中创建一个 encode.py
文件,包含以下代码。
from eth_abi import encode
## 来自你部署脚本输出的值
staking_token_address = "0x97a54ad6993f1fbcae9fa75357a81eb85160120a" # 更改为你的部署地址
reward_rate = 10**16
## 编码参数
encoded_args = encode(['address', 'uint256'], [staking_token_address, reward_rate])
## 转换为不带 '0x' 前缀的十六进制字符串
hex_encoded_args = encoded_args.hex()
print(hex_encoded_args)
然后执行该脚本:
python encode.py
输出结果将类似于(但合约地址应该是你部署的地址):
00000000000000000000000097a54ad6993f1fbcae9fa75357a81eb85160120a000000000000000000000000000000000000000000000000002386f26fc10000
将上述编码参数粘贴到部署 StakingVault 合约时的“构造函数参数 ABI 编码”字段中。验证合约后,你将在“合约”选项卡图标上看到一个勾号。
你已经成功部署了 ERC-20 代币以及 StakingVault 合约。为了挑战自己,请尝试做以下事情:
在本指南中,我们探索了如何使用 ApeWorX 和 Vyper 创建质押库合约。我们覆盖了从设置开发环境到在 Sepolia 测试网部署和验证我们的合约的整个过程。
我们鼓励你尝试代码,添加新功能,最重要的是,继续构建!区块链生态系统需要像你这样富有创意的开发者来推动创新并创建下一代的 web3 应用程序。
请通过关注我们的 Twitter (@QuickNode) 或加入我们的 Discord 社区,了解最新的区块链开发工具和见解。我们期待看到你接下来将构建的内容!
告诉我们 如果你有任何反馈或对新主题的请求。我们很想听听你的想法。
- 原文链接: quicknode.com/guides/eth...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!