在组件第 1 部分中,我们学习了如何在合约中创建和使用组件,并演示了组件的行为类似于 Solidity 中的抽象合约(abstract contracts)。在组件第 2 部分中,我们学习了如何使用 OpenZeppelin 提供的预构建组件来创建代币合约。
到目前为止,我们一直在合约级别使用组件,由合约导入组件并调用它们的函数。但是,如果我们想构建一个使用其他组件功能的组件呢?
例如,考虑一个可以在多个合约中复用的 Staking 组件。当用户进行质押 (stake) 或解除质押 (unstake) 时,组件需要向他们的账户转入和转出代币。它还需要确保只有合约所有者才能更新奖励率。这两个操作都需要调用其他组件。组件到组件的交互使得 Staking 组件能够调用两个独立的组件来处理这些需求。
下图展示了集成了三个组件的 Staking 合约:Staking 组件处理质押逻辑,并依赖于 ERC20 组件(用于代币转移)和 Ownable 组件(用于确保只有合约所有者才能更新奖励率):

学完本章后,你将学会:
- 直接从其他组件调用组件
- 管理合约内的组件依赖
- 指定在合约的 ABI 中暴露哪些组件函数
- 在合约部署期间初始化组件
组件如何调用其他组件
一个组件应该专注于一个职责领域,例如代币管理、访问控制或质押逻辑。当一个组件需要使用其他组件的功能时,你需要在该组件的实现签名中声明这些依赖。声明后,组件便可以调用这些依赖项中的函数。例如,一个 Staking 组件可以调用 ERC20 组件中的转移函数和 Ownable 组件中的所有权检查函数,而无需合约来协调这些交互。
以下是当用户质押 100 个代币时,组件与组件交互的执行流程图:

在本教程中,我们将把 ERC20 组件和 Staking 组件嵌入到同一个合约中。这种嵌入式方法主要是为了演示组件间的交互。在生产环境中,Staking 合约通常接收外部代币地址并使用合约调度器(contract dispatchers)与那个独立的代币合约交互。我们将在文章末尾解释外部代币方法及其区别。
构建 Staking 组件
我们将构建一个依赖于 OpenZeppelin 的 ERC20 组件和 Ownable 组件的 Staking 组件,来看看组件间的交互在实践中是如何工作的,然后将其集成到一个 Staking 合约中。
我们的 Staking 组件将提供以下功能:
- 质押代币:用户可以质押代币以赚取奖励。当用户质押时,代币将从他们的余额转移到合约中。
- 解除质押代币:用户可以随时解除质押(没有锁定期)。解除质押时,质押的代币以及任何累积的奖励都会返还给用户。
- 设置奖励率:只有合约所有者可以更新奖励率,该比率决定了用户随着时间的推移,每个质押代币可以赚取多少奖励代币。
项目设置
创建一个新的 scarb 项目并导航到其目录:
scarb new component_component
cd component_component
添加组件依赖
要使用 OpenZeppelin 的 ERC20 和 Ownable 组件,请在 Scarb.toml 文件的 [dependencies] 下添加 OpenZeppelin 依赖:

Staking 接口
以下接口定义了 Staking 组件将实现的特定于质押的函数:
use starknet::ContractAddress;
#[starknet::interface]
pub trait IStaking<TContractState> {
// User functions
fn stake(ref self: TContractState, amount: u256);
fn unstake(ref self: TContractState, amount: u256);
fn claim_rewards(ref self: TContractState);
// View functions
fn get_staked_balance(self: @TContractState, user: ContractAddress) -> u256;
fn get_total_staked(self: @TContractState) -> u256;
fn calculate_rewards(self: @TContractState, user: ContractAddress) -> u256;
fn get_reward_rate(self: @TContractState) -> u256;
// Admin function
fn set_reward_rate(ref self: TContractState, rate: u256);
}
将该接口添加到你的 src/lib.cairo 文件中,然后在下方添加以下组件结构:
#[starknet::component]
pub mod StakingComponent {
// Component implementation will go here
}
存储 (Storage) 设置
每个组件都定义了自己的存储结构来跟踪其状态。对于 StakingComponent,我们需要跟踪每个用户质押了多少、他们的奖励最后一次计算的时间、合约中质押的总额、用户累积的奖励以及奖励率。让我们在 StakingComponent 内部定义存储结构:
#[starknet::component]
pub mod StakingComponent {
use starknet::ContractAddress;
use starknet::storage::Map;
#[storage]
pub struct Storage {
staked_balances: Map<ContractAddress, u256>,
total_staked: u256,
reward_rate: u256,
last_update_time: Map<ContractAddress, u64>,
accumulated_rewards: Map<ContractAddress, u256>,
}
}
以下是每个存储字段代表的含义:
staked_balances:一个映射 (mapping),跟踪每个用户质押了多少代币。键 (key) 是用户的地址,值 (value) 是他们的质押数量。total_staked:合约中所有用户质押的代币总额。reward_rate:每个质押代币每秒产生的奖励代币数量(放大 1,000,000 倍)。可以由合约所有者更新。last_update_time:从用户地址到他们最后一次奖励更新的时间戳映射。accumulated_rewards:一个映射,跟踪每个用户已经累积但尚未领取的总奖励。
关于组件存储和变量命名的说明
回顾组件第 1 部分,#[substorage(v0)] 属性允许使用该组件的合约访问该组件的状态。
当集成多个组件时,如果两个组件定义了具有相同名称的存储变量,Cairo 编译器将发出关于潜在冲突的警告:
warn: The path `component_a.variable_name` collides with existing path `component_b.variable_name`.
你可以使用 #[allow(starknet::colliding_storage_paths)] 来抑制此警告,但这并不能防止冲突;它只是让警告静音。这两个变量仍将指向同一个存储位置。
这就是为什么 OpenZeppelin 在其组件中为存储变量添加前缀的原因(ERC20_total_supply、Ownable_owner 等)。这些前缀确保即使一起使用多个组件,它们的存储变量也有唯一的名称且不会发生冲突。
因此,在构建组件时,请使用描述性的前缀或不太可能与其他组件存储变量冲突的名称。
事件声明
为了跟踪用户何时质押和解除质押代币,请将以下事件定义添加到 StakingComponent 中:
#[event]
#[derive(Drop, starknet::Event)]
pub enum Event {
Staked: Staked,
Unstaked: Unstaked
}
#[derive(Drop, starknet::Event)]
pub struct Staked {
pub user: ContractAddress,
pub amount: u256,
}
#[derive(Drop, starknet::Event)]
pub struct Unstaked {
pub user: ContractAddress,
pub amount: u256,
}
Staked 会在用户质押代币时记录其地址和数量。当用户解除质押时,Unstaked 也会执行相同的操作。
实现 Staking 接口
有了状态变量和事件,我们现在就可以实现之前定义的 IStaking 接口了。我们将首先创建空的函数存根 (stubs),以使我们的代码能够编译,然后我们将逐个实现每个函数。
将以下实现块添加到 StakingComponent:
#[embeddable_as(StakingImpl)]
impl StakingImplImpl<
TContractState,
+HasComponent<TContractState>,
+Drop<TContractState>,
> of super::IStaking<ComponentState<TContractState>> {
fn stake(ref self: ComponentState<TContractState>, amount: u256) {
// Implementation will go here
}
fn unstake(ref self: ComponentState<TContractState>, amount: u256) {
// Implementation will go here
}
fn claim_rewards(ref self: ComponentState<TContractState>) {
// Implementation will go here
}
fn get_staked_balance(
self: @ComponentState<TContractState>, user: ContractAddress
) -> u256 {
0 // Placeholder
}
fn get_total_staked(self: @ComponentState<TContractState>) -> u256 {
0 // Placeholder
}
fn calculate_rewards(
self: @ComponentState<TContractState>, user: ContractAddress
) -> u256 {
0 // Placeholder
}
fn set_reward_rate(ref self: ComponentState<TContractState>, rate: u256) {
// Implementation will go here
}
fn get_reward_rate(self: @ComponentState<TContractState>) -> u256 {
0 // Placeholder
}
}
#[embeddable_as(StakingImpl)] 属性告诉 Cairo 此实现应该可用于嵌入到合约中。
声明组件依赖
如前所述,StakingComponent 必须在其实现签名中将 ERC20Component 和 OwnableComponent 声明为依赖项,以便直接调用它们的函数。
将 ERC20Component、OwnableComponent 和 starknet 的导入添加到 StakingComponent 模块中:
use openzeppelin::access::ownable::OwnableComponent;
use openzeppelin::token::erc20::ERC20Component;
use starknet::{get_caller_address, get_contract_address};
接下来,在实现签名中将这些组件声明为依赖项:
#[embeddable_as(StakingImpl)]
impl StakingImplImpl
TContractState,
+HasComponent<TContractState>,
+Drop<TContractState>,
impl ERC20: ERC20Component::HasComponent<TContractState>,//ADD THIS LINE
impl Ownable: OwnableComponent::HasComponent<TContractState>,//ADD THIS LINE
> of super::IStaking<ComponentState<TContractState>> {
// Functions here
}
impl ERC20: ERC20Component::HasComponent<TContractState> 和 impl Ownable: OwnableComponent::HasComponent<TContractState> 这两行代码告诉 Cairo,任何使用 StakingComponent 的合约也必须包含这些组件。
以下是到目前为止完整的 StakingComponent 代码:
use starknet::ContractAddress;
#[starknet::interface]
pub trait IStaking<TContractState> {
// User functions
fn stake(ref self: TContractState, amount: u256);
fn unstake(ref self: TContractState, amount: u256);
fn claim_rewards(ref self: TContractState);
// View functions
fn get_staked_balance(self: @TContractState, user: ContractAddress) -> u256;
fn get_total_staked(self: @TContractState) -> u256;
fn calculate_rewards(self: @TContractState, user: ContractAddress) -> u256;
fn get_reward_rate(self: @TContractState) -> u256;
// Admin function
fn set_reward_rate(ref self: TContractState, rate: u256);
}
#[starknet::component]
pub mod StakingComponent {
use openzeppelin::access::ownable::OwnableComponent;
use openzeppelin::token::erc20::ERC20Component;
use starknet::storage::{
Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess,
StoragePointerWriteAccess,
};
use starknet::{ContractAddress, get_caller_address, get_contract_address};
#[storage]
pub struct Storage {
staked_balances: Map<ContractAddress, u256>,
total_staked: u256,
reward_rate: u256,
last_update_time: Map<ContractAddress, u64>,
accumulated_rewards: Map<ContractAddress, u256>,
}
#[event]
#[derive(Drop, starknet::Event)]
pub enum Event {
Staked: Staked,
Unstaked: Unstaked,
}
#[derive(Drop, starknet::Event)]
pub struct Staked {
pub user: ContractAddress,
pub amount: u256,
}
#[derive(Drop, starknet::Event)]
pub struct Unstaked {
pub user: ContractAddress,
pub amount: u256,
}
#[embeddable_as(StakingImpl)]
impl StakingImplImpl<
TContractState,
+HasComponent<TContractState>,
+Drop<TContractState>,
impl ERC20: ERC20Component::HasComponent<TContractState>,
impl Ownable: OwnableComponent::HasComponent<TContractState>,
> of super::IStaking<ComponentState<TContractState>> {
fn stake(
ref self: ComponentState<TContractState>, amount: u256,
) { // Implementation will go here
}
fn unstake(
ref self: ComponentState<TContractState>, amount: u256,
) { // Implementation will go here
}
fn claim_rewards(ref self: ComponentState<TContractState>) {// Implementation will go here
}
fn get_staked_balance(
self: @ComponentState<TContractState>, user: ContractAddress,
) -> u256 {
0 // Placeholder
}
fn get_total_staked(self: @ComponentState<TContractState>) -> u256 {
0 // Placeholder
}
fn calculate_rewards(self: @ComponentState<TContractState>, user: ContractAddress) -> u256 {
0 // Placeholder
}
fn set_reward_rate(
ref self: ComponentState<TContractState>, rate: u256,
) { // Implementation will go here
}
fn get_reward_rate(self: @ComponentState<TContractState>) -> u256 {
0 // Placeholder
}
}
}
实现 stake 函数
stake 函数将代币从用户转移到合约,更新其质押余额,并记录奖励计算的时间戳:
fn stake(ref self: ComponentState<TContractState>, amount: u256) {
assert(amount > 0, 'Amount must be greater than 0');
let caller = get_caller_address();
let contract_address = get_contract_address();
// Transfer tokens from caller to contract
let mut erc20 = get_dep_component_mut!(ref self, ERC20);
erc20._transfer(caller, contract_address, amount);
// Update staking balances
let current_stake = self.staked_balances.read(caller);
self.staked_balances.write(caller, current_stake + amount);
let total = self.total_staked.read();
self.total_staked.write(total + amount);
self.emit(Staked { user: caller, amount });
}
该函数首先验证质押金额是否大于零,然后检索调用者的地址和合约地址。它将代币从调用者转移到合约,更新调用者的质押余额和总质押金额,并发出一个 Staked 事件。
组件间交互发生的位置
注意这几行代码:
let mut erc20 = get_dep_component_mut!(ref self, ERC20);
erc20._transfer(caller, contract_address, amount);
这就是组件与组件发生交互的地方。get_dep_component_mut! 宏检索对 ERC20Component 的可变引用,允许我们调用其 _transfer 函数将代币从用户移动到合约。让我们详细分析一下这是如何工作的:
- 宏:
get_dep_component_mut!为我们提供了一个对我们依赖的组件的可变引用。这使我们能够调用其内部函数。 - 参数:
ref self指的是组件的状态ERC20是我们在实现签名中声明的依赖项名称
- 为什么我们需要这个宏:在
StakingComponent内部,我们不能直接调用self.erc20._transfer(...),因为在合约中每个组件的存储都是保持隔离的。get_dep_component_mut!宏为我们获取对ERC20Component的引用,以便我们可以调用其函数。
你可能想知道为什么我们使用 _transfer 而不是传统的 transfer_from 函数。正如在引言中提到的,本教程使用的是嵌入式代币架构,其中代币和质押逻辑属于同一个合约。这会影响我们使用的转移方法。我们将在文章末尾更详细地解释它,并将其与质押外部代币进行比较。
转账后,我们通过读取用户当前的质押金额来更新其质押余额,加上新的金额,然后再将其写回。我们还更新总质押金额并发出一个 Staked 事件来记录此操作。
如果此时尝试编译代码,你会注意到 _transfer 抛出了一个错误。当你在上面悬停时,你会看到:
Method `_transfer` not found on type `openzeppelin_token::erc20::erc20::ERC20Comp
onent::ComponentState::<TContractState>`. Did you import the correct trait and impl?
发生此错误是因为 _transfer 是 ERC20Component 的内部函数,而我们尚未导入实现它的 Trait。为了解决这个问题,在 StakingComponent 模块顶部添加以下导入:
use openzeppelin::token::erc20::ERC20Component::InternalTrait as ERC20InternalTrait;
此导入使我们能够访问 ERC20Component 的内部函数,如 _transfer 和 _mint。如果你再次编译,你会遇到另一个错误,提示:
Trait has no implementation in context: openzeppelin_token::erc20::erc20::ERC20Co
mponent::InternalTrait::<TContractState, ImplVarId(34881), ImplVarId(34882)> ….
发生这种情况是因为 ERC20Component 需要其 ERC20HooksTrait 的实现。这个 Trait 定义了可以在代币转移前后运行的钩子 (hooks)。由于我们的 Staking 合约不需要自定义钩子,我们将使用 OpenZeppelin 提供的空实现。
更新 ERC20 的导入以包含 ERC20HooksEmptyImpl:
use openzeppelin::token::erc20::{ERC20Component, ERC20HooksEmptyImpl};
现在代码应该成功编译,并且 _transfer 函数将按预期工作。
但是,stake 函数的实现是不完整的。在更新用户的质押之前,我们需要首先计算其累积的奖励。让我们创建内部辅助函数来处理奖励计算。
用于计算奖励的内部辅助函数
组件可以具有仅在组件内部或由使用它的合约访问的内部函数,就像 ERC20Component 中的 _transfer 一样。这些函数不是公共接口的一部分,也不会出现在合约的 ABI 中。
我们使用 #[generate_trait] 属性来定义它们,它会自动生成一个 Trait 来保存内部函数。将以下内部实现块添加到主实现下方,以处理奖励的计算和更新:
#[generate_trait]
pub impl InternalImpl<
TContractState,
+HasComponent<TContractState>,
+Drop<TContractState>,
impl ERC20: ERC20Component::HasComponent<TContractState>,
impl Ownable: OwnableComponent::HasComponent<TContractState>,
> of InternalTrait<TContractState> {
// Internal functions will go here
}
在实现奖励计算辅助函数之前,我们需要一个初始化器 (initializer) 函数,以便在部署合约时使用初始奖励率来设置组件的初始状态:
#[generate_trait]
pub impl InternalImpl<
TContractState,
+HasComponent<TContractState>,
+Drop<TContractState>,
impl ERC20: ERC20Component::HasComponent<TContractState>,
impl Ownable: OwnableComponent::HasComponent<TContractState>,
> of InternalTrait<TContractState> {
//NEWLY ADDED BELOW //
fn initializer(ref self: ComponentState<TContractState>, initial_reward_rate: u256) {
self.reward_rate.write(initial_reward_rate);
}
//other internal function will go here
}
理解组件初始化器
有时组件需要只运行一次初始化逻辑。在 Solidity 中,这可以通过构造函数来实现。虽然 Cairo 合约也支持构造函数,但组件不支持。
相反,组件使用初始化器:在合约部署时处理设置的常规函数。框架并不强制执行单次执行,因此惯例是仅从合约的构造函数中调用初始化器。 由于构造函数仅在部署期间运行一次,因此这也确保了初始化器也只会被调用一次。
在本例中,OpenZeppelin 的 Ownable 和 ERC20 组件,以及我们自定义的 StakingComponent,都提供了名为 initializer 的初始化器函数。因为它是一个常规函数,所以它的名字可以是任意的。名称 initializer 是一种惯例。
当我们在本文后面构建 StakingContract 时,这些初始化器将从合约的构造函数中被调用。忘记调用组件的初始化器将导致其状态未初始化,从而破坏合约逻辑或产生安全漏洞。
OpenZeppelin 中 Ownable 组件初始化器的代码片段如下所示:

Ownable 初始化器设置初始所有者地址,因此我们的构造函数必须接受一个所有者地址作为参数。
计算待定奖励 (Pending Rewards)
有了初始化器,我们就可以实现奖励计算辅助函数了。_calculate_pending_rewards 函数根据用户的质押金额和自上次更新以来的运行时间来计算用户获得了多少奖励:
fn _calculate_pending_rewards(
self: @ComponentState<TContractState>, user: ContractAddress,
) -> u256 {
let staked = self.staked_balances.read(user);
if staked == 0 {
return self.accumulated_rewards.read(user);
}
let last_update = self.last_update_time.read(user);
let current_time = get_block_timestamp();
if last_update == 0 {
return 0;
}
let time_elapsed = current_time - last_update;
let reward_rate = self.reward_rate.read();
// Calculate new rewards: staked_amount * reward_rate * time_elapsed
let new_rewards = (staked * reward_rate * time_elapsed.into()) / 1000000;
let accumulated = self.accumulated_rewards.read(user);
accumulated + new_rewards
}
该函数首先检查用户是否有任何质押的代币。如果没有,它将返回用户从之前的质押中累积的奖励。
然后它检索用户奖励最后一次更新的时间和当前区块时间戳。如果用户以前从未质押过(他们的 last_update_time 为 0),则函数返回 0,因为目前还没有要计算的奖励。
该函数计算自上次更新以来流逝的时间并检索当前的奖励率。奖励计算使用此公式:staked_amount * reward_rate * time_elapsed / 1000000。我们除以 1,000,000,因为 Cairo 没有浮点数。奖励率被放大 1,000,000 倍,以用整数表示小数部分。
然后该函数将任何先前累积的奖励添加到新计算的奖励中,并返回总额。
我们需要在模块顶部现有的导入项旁边导入 get_block_timestamp:
use starknet::{ContractAddress, get_block_timestamp, get_caller_address, get_contract_address};
现在让我们实现 update_rewards 函数,该函数使用 _calculate_pending_rewards 为用户计算任何待定奖励,更新他们累积的奖励和最后更新的时间戳:
fn update_rewards(ref self: ComponentState<TContractState>, user: ContractAddress) {
let pending = self._calculate_pending_rewards(user);
self.accumulated_rewards.write(user, pending);
self.last_update_time.write(user, get_block_timestamp());
}
实现了 update_rewards 之后,我们可以回过头去完善 stake 函数,在更改用户质押额之前添加奖励更新操作。
完成 stake() 函数
更新 stake 函数以包含 update_rewards 调用:
fn stake(ref self: ComponentState<TContractState>, amount: u256) {
assert(amount > 0, 'Amount must be greater than 0');
let caller = get_caller_address();
let contract_address = get_contract_address();
// Update rewards before changing stake
self.update_rewards(caller); //ADD THIS LINE
// Transfer tokens from caller to contract
let mut erc20 = get_dep_component_mut!(ref self, ERC20);
erc20._transfer(caller, contract_address, amount);
// Update staking balances
let current_stake = self.staked_balances.read(caller);
self.staked_balances.write(caller, current_stake + amount);
let total = self.total_staked.read();
self.total_staked.write(total + amount);
self.emit(Staked { user: caller, amount });
}
增加的部分是 self.update_rewards(caller),它在我们更改用户的质押余额之前被调用。这确保了在添加新代币之前,根据用户之前的质押来计算奖励。如果没有这个,用户将失去在其之前的质押上赚取的奖励。
以下是到目前为止完整的 StakingComponent 代码:
use starknet::ContractAddress;
#[starknet::interface]
pub trait IStaking<TContractState> {
// User functions
fn stake(ref self: TContractState, amount: u256);
fn unstake(ref self: TContractState, amount: u256);
fn claim_rewards(ref self: TContractState);
// View functions
fn get_staked_balance(self: @TContractState, user: ContractAddress) -> u256;
fn get_total_staked(self: @TContractState) -> u256;
fn calculate_rewards(self: @TContractState, user: ContractAddress) -> u256;
fn get_reward_rate(self: @TContractState) -> u256;
// Admin function
fn set_reward_rate(ref self: TContractState, rate: u256);
}
#[starknet::component]
pub mod StakingComponent {
use openzeppelin::access::ownable::OwnableComponent;
use openzeppelin::token::erc20::ERC20Component::InternalTrait as ERC20InternalTrait;
use openzeppelin::token::erc20::{ERC20Component, ERC20HooksEmptyImpl};
use starknet::storage::{
Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess,
StoragePointerWriteAccess,
};
use starknet::{ContractAddress, get_block_timestamp, get_caller_address, get_contract_address};
#[storage]
pub struct Storage {
staked_balances: Map<ContractAddress, u256>,
total_staked: u256,
reward_rate: u256,
last_update_time: Map<ContractAddress, u64>,
accumulated_rewards: Map<ContractAddress, u256>,
}
#[event]
#[derive(Drop, starknet::Event)]
pub enum Event {
Staked: Staked,
Unstaked: Unstaked,
}
#[derive(Drop, starknet::Event)]
pub struct Staked {
pub user: ContractAddress,
pub amount: u256,
}
#[derive(Drop, starknet::Event)]
pub struct Unstaked {
pub user: ContractAddress,
pub amount: u256,
}
#[embeddable_as(StakingImpl)]
impl StakingImplImpl<
TContractState,
+HasComponent<TContractState>,
+Drop<TContractState>,
impl ERC20: ERC20Component::HasComponent<TContractState>,
impl Ownable: OwnableComponent::HasComponent<TContractState>,
> of super::IStaking<ComponentState<TContractState>> {
/// @notice Stakes tokens into the contract and update rewards
/// @param amount The amount of tokens to stake
fn stake(ref self: ComponentState<TContractState>, amount: u256) {
assert(amount > 0, 'Amount must be greater than 0');
let caller = get_caller_address();
let contract_address = get_contract_address();
// Update rewards before changing stake
self.update_rewards(caller);
// This transfers from caller to contract without needing approval
let mut erc20 = get_dep_component_mut!(ref self, ERC20);
erc20._transfer(caller, contract_address, amount);
// Update staking balances
let current_stake = self.staked_balances.read(caller);
self.staked_balances.write(caller, current_stake + amount);
let total = self.total_staked.read();
self.total_staked.write(total + amount);
self.emit(Staked { user: caller, amount });
}
fn unstake(
ref self: ComponentState<TContractState>, amount: u256,
) { // Implementation will go here
}
fn claim_rewards(ref self: ComponentState<TContractState>) { // Implementation will go here
}
fn get_staked_balance(
self: @ComponentState<TContractState>, user: ContractAddress,
) -> u256 {
0 // Placeholder
}
fn get_total_staked(self: @ComponentState<TContractState>) -> u256 {
0 // Placeholder
}
fn calculate_rewards(self: @ComponentState<TContractState>, user: ContractAddress) -> u256 {
0 // Placeholder
}
fn set_reward_rate(
ref self: ComponentState<TContractState>, rate: u256,
) { // Implementation will go here
}
fn get_reward_rate(self: @ComponentState<TContractState>) -> u256 {
0 // Placeholder
}
}
#[generate_trait]
pub impl InternalImpl<
TContractState,
+HasComponent<TContractState>,
+Drop<TContractState>,
impl ERC20: ERC20Component::HasComponent<TContractState>,
impl Ownable: OwnableComponent::HasComponent<TContractState>,
> of InternalTrait<TContractState> {
//NEWLY ADDED BELOW //
fn initializer(ref self: ComponentState<TContractState>, initial_reward_rate: u256) {
self.reward_rate.write(initial_reward_rate);
}
fn _calculate_pending_rewards(
self: @ComponentState<TContractState>, user: ContractAddress,
) -> u256 {
let staked = self.staked_balances.read(user);
if staked == 0 {
return self.accumulated_rewards.read(user);
}
let last_update = self.last_update_time.read(user);
let current_time = get_block_timestamp();
if last_update == 0 {
return 0;
}
let time_elapsed = current_time - last_update;
let reward_rate = self.reward_rate.read();
// Calculate new rewards: staked_amount * reward_rate * time_elapsed
let new_rewards = (staked * reward_rate * time_elapsed.into()) / 1000000;
let accumulated = self.accumulated_rewards.read(user);
accumulated + new_rewards
}
fn update_rewards(ref self: ComponentState<TContractState>, user: ContractAddress) {
let pending = self._calculate_pending_rewards(user);
self.accumulated_rewards.write(user, pending);
self.last_update_time.write(user, get_block_timestamp());
}
}
}
实现 unstake 函数
unstake 函数允许用户从合约中提取他们质押的代币。让我们实现它:
fn unstake(ref self: ComponentState<TContractState>, amount: u256) {
let caller = get_caller_address();
let current_stake = self.staked_balances.read(caller);
assert(amount > 0, 'Amount must be greater than 0');
assert(current_stake >= amount, 'Insufficient staked balance');
// Update rewards before changing stake
self.update_rewards(caller);
// Update staking balances
self.staked_balances.write(caller, current_stake - amount);
let total = self.total_staked.read();
self.total_staked.write(total - amount);
// Transfer tokens back to user using ERC20Component
let mut erc20 = get_dep_component_mut!(ref self, ERC20);
let contract_address = get_contract_address();
erc20._transfer(contract_address, caller, amount);
self.emit(Unstaked { user: caller, amount });
}
该函数首先检索调用者的地址及其当前的质押余额。然后它验证金额,并确认用户有足够的质押代币。
就像在 stake 函数中一样,在修改用户的质押余额之前,我们调用了 self.update_rewards(caller)。这确保了在提款之前根据其质押金额计算出奖励。
更新奖励后,该函数会减少用户的质押余额和总质押金额。然后它使用 ERC20Component 的 _transfer 函数将代币从合约转移回用户,并发出 Unstaked 事件。
实现 claim_rewards 函数
claim_rewards 函数允许用户在保持其代币在合约中质押的同时,领取其累积的奖励:
fn claim_rewards(ref self: ComponentState<TContractState>) {
let caller = get_caller_address();
self.update_rewards(caller);
let rewards = self.accumulated_rewards.read(caller);
assert(rewards > 0, 'No rewards to claim');
// Reset accumulated rewards
self.accumulated_rewards.write(caller, 0);
// Transfer reward tokens to user
let mut erc20 = get_dep_component_mut!(ref self, ERC20);
let contract_address = get_contract_address();
erc20._transfer(contract_address, caller, rewards);
}
claim_rewards 函数首先更新用户的奖励,以确保计算所有待定奖励。然后读取用户的总累积奖励。
该函数验证用户是否有要领取的奖励,将其累积奖励重置为零,然后使用 _transfer 将奖励代币转移给用户。
实现 set_reward_rate 函数
set_reward_rate 函数允许合约所有者更新奖励率:
fn set_reward_rate(ref self: ComponentState<TContractState>, rate: u256) {
// Check ownership using OwnableComponent
let ownable = get_dep_component!(@self, Ownable);
ownable.assert_only_owner();
self.reward_rate.write(rate);
}
此函数还演示了另一个组件到组件的交互。我们使用 get_dep_component!(@self, Ownable) 来访问 OwnableComponent 并调用其 assert_only_owner 函数。如果调用者不是合约所有者,此函数将会 panic,从而防止未经授权的用户更改奖励率。
请注意,我们在这里使用 get_dep_component!(没有 _mut)而不是我们与 ERC20Component 一起使用的 get_dep_component_mut!。区别在于:
get_dep_component_mut!提供了一个可变引用——当需要修改组件状态(如转移代币)时使用它get_dep_component!提供了一个不可变引用——当只需要读取数据或调用不修改状态的函数(如检查所有权)时使用它
由于 assert_only_owner 只读取所有者地址而不修改 OwnableComponent 的状态,因此我们使用了不可变版本。
如果现在编译代码,你会得到一个错误:
Method 'assert_only_owner' not found on type '@openzeppelin_access::ownable::owna
ble::OwnableComponent::ComponentState::<TContractState>'. Consider importing one
of the following traits: 'OwnableComponent::InternalTrait'
这意味着我们需要在模块顶部导入 OwnableComponent 的内部 Trait:
use openzeppelin::access::ownable::OwnableComponent::InternalTrait as OwnableInternalTrait;
在所有权检查通过后,该函数会在存储中更新奖励率。
实现视图 (view) 函数
我们需要实现四个视图函数:
get_staked_balance返回特定用户质押的代币数量,get_total_staked返回所有用户质押的总额,calculate_rewards显示用户可以领取多少奖励,以及get_reward_rate返回当前的奖励率。
fn get_staked_balance(
self: @ComponentState<TContractState>, user: ContractAddress
) -> u256 {
self.staked_balances.read(user)
}
fn get_total_staked(self: @ComponentState<TContractState>) -> u256 {
self.total_staked.read()
}
fn calculate_rewards(
self: @ComponentState<TContractState>, user: ContractAddress
) -> u256 {
self._calculate_pending_rewards(user)
}
fn get_reward_rate(self: @ComponentState<TContractState>) -> u256 {
self.reward_rate.read()
}
这些实现从存储中读取并返回请求的值。calculate_rewards 函数使用内部的 _calculate_pending_rewards 函数来计算奖励。
以下是完整的 StakingComponent 代码:
use starknet::ContractAddress;
#[starknet::interface]
pub trait IStaking<TContractState> {
// User functions
fn stake(ref self: TContractState, amount: u256);
fn unstake(ref self: TContractState, amount: u256);
fn claim_rewards(ref self: TContractState);
// View functions
fn get_staked_balance(self: @TContractState, user: ContractAddress) -> u256;
fn get_total_staked(self: @TContractState) -> u256;
fn calculate_rewards(self: @TContractState, user: ContractAddress) -> u256;
fn get_reward_rate(self: @TContractState) -> u256;
// Admin function
fn set_reward_rate(ref self: TContractState, rate: u256);
}
#[starknet::component]
pub mod StakingComponent {
use openzeppelin::access::ownable::OwnableComponent;
use openzeppelin::access::ownable::OwnableComponent::InternalTrait as OwnableInternalTrait;
use openzeppelin::token::erc20::ERC20Component::InternalTrait as ERC20InternalTrait;
use openzeppelin::token::erc20::{ERC20Component, ERC20HooksEmptyImpl};
use starknet::storage::{
Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess,
StoragePointerWriteAccess,
};
use starknet::{ContractAddress, get_block_timestamp, get_caller_address, get_contract_address};
#[storage]
pub struct Storage {
staked_balances: Map<ContractAddress, u256>,
total_staked: u256,
reward_rate: u256,
last_update_time: Map<ContractAddress, u64>,
accumulated_rewards: Map<ContractAddress, u256>,
}
#[event]
#[derive(Drop, starknet::Event)]
pub enum Event {
Staked: Staked,
Unstaked: Unstaked
}
#[derive(Drop, starknet::Event)]
pub struct Staked {
pub user: ContractAddress,
pub amount: u256,
}
#[derive(Drop, starknet::Event)]
pub struct Unstaked {
pub user: ContractAddress,
pub amount: u256,
}
#[embeddable_as(StakingImpl)]
impl StakingImplImpl<
TContractState,
+HasComponent<TContractState>,
+Drop<TContractState>,
impl ERC20: ERC20Component::HasComponent<TContractState>,
impl Ownable: OwnableComponent::HasComponent<TContractState>,
> of super::IStaking<ComponentState<TContractState>> {
/// @notice Stakes tokens into the contract and update rewards
/// @param amount The amount of tokens to stake
fn stake(ref self: ComponentState<TContractState>, amount: u256) {
assert(amount > 0, 'Amount must be greater than 0');
let caller = get_caller_address();
let contract_address = get_contract_address();
// Update rewards before changing stake
self.update_rewards(caller);
// This transfers from caller to contract without needing approval
let mut erc20 = get_dep_component_mut!(ref self, ERC20);
erc20._transfer(caller, contract_address, amount);
// Update staking balances
let current_stake = self.staked_balances.read(caller);
self.staked_balances.write(caller, current_stake + amount);
let total = self.total_staked.read();
self.total_staked.write(total + amount);
self.emit(Staked { user: caller, amount });
}
/// @notice Unstakes tokens and transfers them back to user
/// @param amount The amount of tokens to unstake
fn unstake(ref self: ComponentState<TContractState>, amount: u256) {
let caller = get_caller_address();
let current_stake = self.staked_balances.read(caller);
assert(amount > 0, 'Amount must be greater than 0');
assert(current_stake >= amount, 'Insufficient staked balance');
// Update rewards before changing stake
self.update_rewards(caller);
// Update staking balances
self.staked_balances.write(caller, current_stake - amount);
let total = self.total_staked.read();
self.total_staked.write(total - amount);
// Transfer tokens back to user using ERC20Component
let mut erc20 = get_dep_component_mut!(ref self, ERC20);
let contract_address = get_contract_address();
erc20._transfer(contract_address, caller, amount);
self.emit(Unstaked { user: caller, amount });
}
/// @notice Claims accumulated rewards for the caller
fn claim_rewards(ref self: ComponentState<TContractState>) {
let caller = get_caller_address();
self.update_rewards(caller);
let rewards = self.accumulated_rewards.read(caller);
assert(rewards > 0, 'No rewards to claim');
// Reset accumulated rewards
self.accumulated_rewards.write(caller, 0);
// Transfer reward tokens to user
let mut erc20 = get_dep_component_mut!(ref self, ERC20);
let contract_address = get_contract_address();
erc20._transfer(contract_address, caller, rewards);
}
/// @notice Returns the staked balance of a specific user
/// @param user The address of the user
/// @return The amount of tokens staked by the user
fn get_staked_balance(
self: @ComponentState<TContractState>, user: ContractAddress,
) -> u256 {
self.staked_balances.read(user)
}
/// @notice Returns the total amount of tokens staked in the contract
/// @return The total staked amount
fn get_total_staked(self: @ComponentState<TContractState>) -> u256 {
self.total_staked.read()
}
/// @notice Calculates the pending rewards for a user
/// @param user The address of the user
/// @return The amount of pending rewards
fn calculate_rewards(self: @ComponentState<TContractState>, user: ContractAddress) -> u256 {
self._calculate_pending_rewards(user)
}
/// @notice Sets the reward rate (only callable by owner)
/// @param rate The new reward rate per second
fn set_reward_rate(ref self: ComponentState<TContractState>, rate: u256) {
// Check ownership using OwnableComponent
let ownable = get_dep_component!(@self, Ownable);
ownable.assert_only_owner();
self.reward_rate.write(rate);
}
/// @notice Returns the current reward rate
/// @return The reward rate per second
fn get_reward_rate(self: @ComponentState<TContractState>) -> u256 {
self.reward_rate.read()
}
}
#[generate_trait]
pub impl InternalImpl<
TContractState,
+HasComponent<TContractState>,
+Drop<TContractState>,
impl ERC20: ERC20Component::HasComponent<TContractState>,
impl Ownable: OwnableComponent::HasComponent<TContractState>,
> of InternalTrait<TContractState> {
// Initializes the staking component with an initial reward rate
fn initializer(ref self: ComponentState<TContractState>, initial_reward_rate: u256) {
self.reward_rate.write(initial_reward_rate);
}
// Updates the accumulated rewards for a user
fn update_rewards(ref self: ComponentState<TContractState>, user: ContractAddress) {
let pending = self._calculate_pending_rewards(user);
self.accumulated_rewards.write(user, pending);
self.last_update_time.write(user, get_block_timestamp());
}
// Calculates pending rewards based on staked amount and time elapsed
fn _calculate_pending_rewards(
self: @ComponentState<TContractState>, user: ContractAddress,
) -> u256 {
let staked = self.staked_balances.read(user);
if staked == 0 {
return self.accumulated_rewards.read(user);
}
let last_update = self.last_update_time.read(user);
let current_time = get_block_timestamp();
if last_update == 0 {
return 0;
}
let time_elapsed = current_time - last_update;
let reward_rate = self.reward_rate.read();
// Calculate new rewards: staked_amount * reward_rate * time_elapsed
let new_rewards = (staked * reward_rate * time_elapsed.into()) / 1000000;
let accumulated = self.accumulated_rewards.read(user);
accumulated + new_rewards
}
}
}
我们现在已经完成了 StakingComponent,它包含了我们需要的所有质押逻辑。该组件处理质押和解除质押代币,计算和领取奖励,以及管理奖励率。
然而,组件本身无法部署。我们需要创建一个集成了 StakingComponent 以及 ERC20Component 和 OwnableComponent 的合约。此合约将充当用户与之交互的、可部署的智能合约。
构建 Staking 合约
StakingContract 将:
- 包含我们刚刚构建的
StakingComponent - 包含
ERC20Component(用于质押代币) - 包含
OwnableComponent(用于访问控制) - 使用必要的参数初始化这三个组件
- 暴露我们希望用户能够调用的函数
让我们构建将所有这些组件汇集在一起的 StakingContract。
导入依赖项
注意: 在本演练中,我们将在同一个 lib.cairo 文件中构建 StakingComponent 和 StakingContract。在较大的项目中,您可以将它们组织到单独的文件中(例如 staking_component.cairo 和 staking_contract.cairo),但将所有内容保留在一个文件中能让学习过程更容易跟上。
首先,我们需要导入所有将要使用的组件:
#[starknet::contract]
mod StakingContract {
use openzeppelin::access::ownable::OwnableComponent;
use openzeppelin::token::erc20::{ERC20Component, ERC20HooksEmptyImpl};
use starknet::{ContractAddress, get_contract_address};
use super::StakingComponent;
}
我们从 OpenZeppelin 导入了 OwnableComponent 和 ERC20Component,以及我们刚刚创建的 StakingComponent。
声明组件
接下来,我们声明我们的合约将要使用的三个组件:
// Component declarations
component!(path: StakingComponent, storage: staking, event: StakingEvent);
component!(path: ERC20Component, storage: erc20, event: ERC20Event);
component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);
每个 component! 宏都会声明一个组件并指定:
- 组件路径(要使用的组件)
- 存储名称(该组件的存储将保存在何处)
- 事件名称(该组件事件的名称)
配置 ERC20Component
ERC20Component 要求我们实现它的 ImmutableConfig Trait。此 Trait 配置了在编译时固定的值,而不是保存在合约存储中的值。ERC20 的 DECIMALS 值在部署后永远不会改变,因此非常适合这种模式。
// ERC20 Mixin configuration
impl ERC20ImmutableConfig of ERC20Component::ImmutableConfig {
const DECIMALS: u8 = 18;
}
这将代币设置为使用 18 位小数,这是大多数代币的标准。
暴露组件函数
现在我们需要决定应该公开访问每个组件中的哪些函数。我们使用 #[abi(embed_v0)] 属性来暴露实现:
// Expose staking functions publicly
#[abi(embed_v0)]
impl StakingImpl = StakingComponent::StakingImpl<ContractState>;
// Expose ERC20 functions publicly
#[abi(embed_v0)]
impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl<ContractState>;
// Expose Ownable functions publicly
#[abi(embed_v0)]
impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl<ContractState>;
这些代码行使所有 Staking 函数、ERC-20 函数(如 transfer、balance_of)以及所有权函数(如 transfer_ownership)在合约的公共接口 (ABI) 中可用。
我们还需要提供内部实现,以便组件可以调用彼此的内部函数:
// Internal implementations
impl StakingInternalImpl = StakingComponent::InternalImpl<ContractState>;
impl ERC20InternalImpl = ERC20Component::InternalImpl<ContractState>;
impl OwnableInternalImpl = OwnableComponent::InternalImpl<ContractState>;
存储结构
每个组件都需要自己的存储空间。我们为所有三个组件声明存储:
#[storage]
struct Storage {
#[substorage(v0)]
staking: StakingComponent::Storage,
#[substorage(v0)]
erc20: ERC20Component::Storage,
#[substorage(v0)]
ownable: OwnableComponent::Storage,
}
#[substorage(v0)] 属性告诉 Cairo 每个字段都包含一个组件的存储结构。
事件
同样,我们需要聚合来自所有组件的事件:
#[event]
#[derive(Drop, starknet::Event)]
pub enum Event {
#[flat]
StakingEvent: StakingComponent::Event,
#[flat]
ERC20Event: ERC20Component::Event,
#[flat]
OwnableEvent: OwnableComponent::Event,
}
构造函数 (Constructor)
构造函数在合约部署时初始化所有这三个组件:
#[constructor]
fn constructor(
ref self: ContractState,
owner: ContractAddress,
token_name: ByteArray,
token_symbol: ByteArray,
initial_reward_rate: u256,
initial_supply: u256,
) {
// Initialize ownership
self.ownable.initializer(owner);
// Initialize the ERC20 token
self.erc20.initializer(token_name, token_symbol);
// Mint initial supply to the contract for rewards
self.erc20.mint(get_contract_address(), initial_supply);
// Initialize staking with reward rate
self.staking.initializer(initial_reward_rate);
}
构造函数接收所有三个组件所需的参数:所有者的地址、代币名称和符号、初始奖励率以及初始代币供应量。它初始化每个组件,并将初始供应量铸造给合约本身。这确保了当用户质押和领取奖励时,合约有可用的代币来支付。
添加 mint 函数
我们再添加一个函数,以允许所有者铸造 (mint) 代币:
#[generate_trait]
#[abi(per_item)]
impl ExternalImpl of ExternalTrait {
#[external(v0)]
fn mint(ref self: ContractState, recipient: ContractAddress, amount: u256) {
self.ownable.assert_only_owner();
self.erc20.mint(recipient, amount);
}
}
此函数检查调用者是否为所有者,然后将代币铸造给指定的接收者。
以下是完整的 StakingContract:
#[starknet::contract]
mod StakingContract {
use openzeppelin::access::ownable::OwnableComponent;
use openzeppelin::token::erc20::{ERC20Component, ERC20HooksEmptyImpl};
use starknet::{ContractAddress, get_contract_address};
use super::StakingComponent;
// Component declarations
component!(path: StakingComponent, storage: staking, event: StakingEvent);
component!(path: ERC20Component, storage: erc20, event: ERC20Event);
component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);
// ERC20 Mixin configuration
impl ERC20ImmutableConfig of ERC20Component::ImmutableConfig {
const DECIMALS: u8 = 18;
}
// Expose staking functions publicly
#[abi(embed_v0)]
impl StakingImpl = StakingComponent::StakingImpl<ContractState>;
// Expose ERC20 functions publicly
#[abi(embed_v0)]
impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl<ContractState>;
// Expose Ownable functions publicly
#[abi(embed_v0)]
impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl<ContractState>;
// Internal implementations
impl StakingInternalImpl = StakingComponent::InternalImpl<ContractState>;
impl ERC20InternalImpl = ERC20Component::InternalImpl<ContractState>;
impl OwnableInternalImpl = OwnableComponent::InternalImpl<ContractState>;
#[storage]
struct Storage {
#[substorage(v0)]
staking: StakingComponent::Storage,
#[substorage(v0)]
erc20: ERC20Component::Storage,
#[substorage(v0)]
ownable: OwnableComponent::Storage,
}
#[event]
#[derive(Drop, starknet::Event)]
pub enum Event {
#[flat]
StakingEvent: StakingComponent::Event,
#[flat]
ERC20Event: ERC20Component::Event,
#[flat]
OwnableEvent: OwnableComponent::Event,
}
#[constructor]
fn constructor(
ref self: ContractState,
owner: ContractAddress,
token_name: ByteArray,
token_symbol: ByteArray,
initial_reward_rate: u256,
initial_supply: u256,
) {
// Initialize ownership
self.ownable.initializer(owner);
// Initialize the ERC20 token
self.erc20.initializer(token_name, token_symbol);
// Mint initial supply to the contract for rewards
self.erc20.mint(get_contract_address(), initial_supply);
// Initialize staking with reward rate
self.staking.initializer(initial_reward_rate);
}
#[generate_trait]
#[abi(per_item)]
impl ExternalImpl of ExternalTrait {
#[external(v0)]
fn mint(ref self: ContractState, recipient: ContractAddress, amount: u256) {
self.ownable.assert_only_owner();
self.erc20.mint(recipient, amount);
}
}
}
为什么我们使用 ERC20Component 的 _transfer 而不是 transfer_from
虽然 transfer_from 是用于授权代币转移的标准 ERC-20 函数,但它在同一个合约内的组件到组件的调用中无法正常工作。
在典型的 Staking 合约中,Staking 合约和代币是两个独立的合约。当用户质押 STRK 代币时:
- 用户授权 (approve) Staking 合约:
token.approve(staking_contract, amount) - Staking 合约调用:
token.transfer_from(user, staking_contract, amount) - 在代币合约内部,
transfer_from调用get_caller_address(),它会返回 Staking 合约的地址 - 代币检查:
allowances[user][staking_contract] >= amount - 如果已授权,代币将被转移
这样有效是因为在代币合约中,get_caller_address() 返回的是 Staking 合约(外部调用者),它与授权相匹配。
组件间调用时会发生什么
在我们的嵌入式架构中,StakingComponent 和 ERC20Component 属于同一个合约的一部分。当 StakingComponent 调用 ERC20Component 时,这是一个内部调用,而不是外部调用。
需要注意的重要行为是,即使在组件内部被调用,get_caller_address() 也依然保持原始的外部调用者地址不变。当用户调用 stake() 时:
- 外部调用进入合约:
get_caller_address()返回 User(用户) - StakingComponent.stake() 执行:
get_caller_address()返回 User - ERC20Component.transfer_from() 执行:
get_caller_address()依然返回 User
所以 transfer_from 会检查 allowances[user][user] 而不是 allowances[user][contract],因此这将无法工作。
组件调用是内部调度,而不是外部合约调用,因此调用链中不存在中间合约地址。
在组件调用中使用 _transfer
_transfer 内部函数绕过了授权 (allowance) 机制,直接转移代币:
fn stake(ref self: ComponentState<TContractState>, amount: u256) {
let mut erc20 = get_dep_component_mut!(ref self, ERC20);
erc20._transfer(caller, contract_address, amount);
// No allowance check needed
}
这是可行的,正如前面提到的,因为我们的合约就是代币合约本身;同一个合约既控制了代币余额和转账 (ERC20Component),也控制了 Staking 逻辑 (StakingComponent)。_transfer 函数完全绕过了授权机制,因此用户在质押前无需调用 approve()。
质押外部代币
在典型的 Staking 合约中,您通常是质押外部代币,而不是使用嵌入式的 ERC20Component。采用这种标准模式,你会在 stake() 函数中使用外部代币的调度器 (IERC20Dispatcher { contract_address: token_address }),对外部合约调用 transfer_from。授权机制将会正常工作,因为它是一个真实的外部调用。
transfer_from 适用于外部的合约间调用,而不是同一个合约内部的组件间调用。当同一个合约内的组件相互交互时,应使用不依赖于 get_caller_address() 进行授权检查的内部函数(如 _transfer)。即使它们实现了相同的接口,外部合约交互和内部组件交互之间的执行上下文和调用者语义也是不同的。
结论
组件到组件的交互使我们能够构建依赖于其他组件的可复用逻辑。通过显式声明依赖,一个组件可以直接调用另一个组件的函数,而无需通过合约路由调用。
在本教程中,我们构建了一个显式依赖于 ERC20Component 和 OwnableComponent 的 StakingComponent。这种声明和调用依赖组件的模式是构建模块化、可组合智能合约的基础。
如前所述,我们在本教程中使用的嵌入式代币方法主要是为了演示目的;在生产环境中,Staking 合约通常使用外部代币模式。
当每个组件都处理某个特定关注点时,它们能发挥最大的效用。当逻辑需要与多个组件交互时,显式地声明这些依赖以保持代码井然有序且可复用。