本文详细介绍如何在Starknet上使用Cairo语言集成Pragma预言机,实现一个基于STRK价格条件的简单金库合约。文章从Pragma的两层数据聚合机制(数据源与发布者)讲起,逐步构建合约:定义接口、处理ERC20转账、使用Pragma的get_data_median获取中位价格,并通过价格阈值控制提款。最后展示合约部署、存款、查询价格和提款的全流程交互,并指出可扩展至动态NFT、借贷协议等场景。
Pragma 是一个为 Starknet 构建的预言机协议,它将链下价格数据带到链上。它提供价格馈送和计算馈送(例如收益率曲线和波动率数据)。
在本文中,你将学习如何将 Pragma 的价格馈送预言机集成到你的 Cairo 合约中。
在集成价格馈送之前,我们先从高层解释一下 Pragma 的工作原理。
Pragma 从多个独立来源收集价格数据,并在链上聚合,以提供可靠、抗操纵的价格馈送。关键区别在于 Pragma 直接在 Starknet 上执行所有聚合,使整个过程透明且可验证。
Pragma 通过一个两层系统聚合数据:
来源 是价格产生的实际交易所、数据提供商和市场,例如币安、Coinbase、OKX、Ekubo、Chainlink 等。
发布者 是由 Pragma 管理员控制的注册表列入白名单的实体。它们监控来自各个来源的价格,并将这些数据提交给预言机合约。你可以在 Pragma 的发布者注册合约中找到完整的发布者列表。截至撰写本文时,当前的发布者如下:

每个发布者独立监控多个来源,并将价格数据提交给预言机合约。然后,Pragma 的预言机聚合所有发布者的提交。如果某个发布者报告了异常价格,Pragma 的聚合会将其排除。
例如,以下是 ARGENT 发布者监控的来源:

因此,当 ARGENT(一个发布者)想要报告价格时,它会监控上方红框中高亮显示的多个来源,并将观察到的价格数据提交给 Pragma 预言机合约。
发布者监控其指定的来源(交易所、预言机、数据提供商)并收集当前价格观测值。
发布者为收集到的数据添加时间戳,然后直接提交到 Starknet 上的 Pragma 预言机合约。没有集中的链下聚合层;每个发布者独立地在链上提交其数据,数据在那里可以被公开验证。例如,如果 10 个发布者提交 ETH 价格,Pragma 就有 10 个链上数据点可以使用。
Pragma 使用两步聚合过程来计算最终价格:
你的合约向 Pragma 的预言机查询当前的 ETH/USD 价格,并收到带有时间戳的聚合价格。
以下是整个流程的总结:

让我们构建一个基础的金库合约,用户可以在其中存入 STRK 代币,但只有当 STRK 价格达到某个阈值时才能提取。
以下是该金库中存款和提现操作的流程:

创建一个新的 scarb 项目并导航到目录:
scarb new simple_vault
cd simple_vault
打开 Scarb.toml 并在 [dependencies] 部分下添加 Pragma 预言机库作为依赖:
[dependencies]
pragma_lib = { git = "https://github.com/astraly-labs/pragma-lib" }

现在运行 scarb build 来下载依赖并验证项目设置正确。首次运行可能需要一分钟来下载依赖。构建成功后,我们就可以开始编写金库合约了。
首先,我们为金库合约定义接口。这指定了我们的金库将公开的所有函数:
deposit(amount):在金库中锁定 STRK 代币withdraw():取回 STRK 代币(仅在满足价格条件时)get_balance(user):检查特定用户已存入多少 STRKget_strk_price():从 Pragma 预言机查询当前 STRK/USD 价格use starknet::ContractAddress;
#[starknet::interface]
trait ISimpleVault<TContractState> {
fn deposit(ref self: TContractState, amount: u256);
fn withdraw(ref self: TContractState);
fn get_balance(self: @TContractState, user: ContractAddress) -> u256;
fn get_strk_price(self: @TContractState) -> u128;
}
由于金库需要在存款和提现期间转移 STRK 代币,我们需要定义一个 ERC-20 接口,其中包含用于将代币从用户转移到金库的 transfer_from 和用于将代币从金库返回给用户的 transfer:
#[starknet::interface]
trait IERC20<TContractState> {
fn transfer_from(
ref self: TContractState,
sender: ContractAddress,
recipient: ContractAddress,
amount: u256
) -> bool;
fn transfer(ref self: TContractState, recipient: ContractAddress, amount: u256) -> bool;
}
首先声明合约模块并导入所需的类型、特性和函数:
#[starknet::contract]
mod SimpleVault {
use starknet::{ContractAddress, get_caller_address};
use pragma_lib::abi::{IPragmaABIDispatcher, IPragmaABIDispatcherTrait};
use pragma_lib::types::{DataType, PragmaPricesResponse};
use starknet::storage::{Map, StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess};
use super::{IERC20Dispatcher, IERC20DispatcherTrait};
需要注意的关键导入来自 pragma_lib:
IPragmaABIDispatcher 和 IPragmaABIDispatcherTrait:用于调用 Pragma 预言机合约上函数的合约调度器和特性DataType:指定我们想要的数据类型(当前市场价格的现货价格)PragmaPricesResponse:保存 Pragma 返回的价格信息我们定义两个关键常量:
// 常量
const STRK_USD_PAIR_ID: felt252 = 6004514686061859652; // 来自 Pragma 的 STRK/USD 交易对 ID
const PRICE_THRESHOLD: u128 = 16000000; // 8 位小数的 $0.16 (0.16 * 10^8)
STRK_USD_PAIR_ID 标识要查询哪个 Pragma 价格馈送。在这个例子中,我们查询的是 STRK/USD 馈送。交易对 ID 6004514686061859652 对应于大写 ticker 字符串 'STRK/USD' 的 UTF-8 编码。这意味着交易对 ID 常量可以更易读地写成:
const STRK_USD_PAIR_ID: felt252 = 'STRK/USD';
相同的模式适用于其他价格对。每个交易对 ID 就是其大写 ticker 字符串的 UTF-8 编码。例如,'ETH/USD' 可以写成字符串 'ETH/USD' 或其 felt252 数值 19514442401534788,两者在 Cairo 中是等价的。其他资产交易对 ID 可从 Pragma 的价格馈送文档中获得。每个资产对都有自己独特的 ID 和小数精度;在选择不同交易对时,请务必检查 Decimals 列。相同的交易对 ID 用于 Sepolia 测试网和主网。

PRICE_THRESHOLD 设置提现所需的最低 STRK 价格($0.16)。由于 Pragma 以 8 位小数精度返回 STRK 价格,我们将 $0.16 表示为 16,000,000 (0.16 × 10^8)。
存储结构体定义了我们的合约存储哪些数据:
#[storage]
struct Storage {
pragma_oracle: ContractAddress, // pragma_oracle 合约地址
strk_token: ContractAddress, // strk 代币合约地址
balances: Map<ContractAddress, u256>, // 用户余额
}
我们存储的内容:
pragma_oracle:Starknet 上 Pragma 预言机合约的地址,用于获取实时资产价格strk_token:STRK 代币合约的地址,用于处理存款和提现balances:从用户地址到其存入的 STRK 数量的映射定义了存储之后,我们在部署期间通过将 Pragma 预言机和 STRK 代币地址传递给构造函数来初始化合约:
#[constructor]
fn constructor(
ref self: ContractState,
pragma_oracle: ContractAddress,
strk_token: ContractAddress
) {
self.pragma_oracle.write(pragma_oracle);
self.strk_token.write(strk_token);
}
这些地址允许金库合约从 Pragma 的预言机查询价格并处理 STRK 代币转账,正如我们在讨论存款和提现函数时将会看到的那样。
接下来,我们定义事件来追踪金库中的存款(Deposit)和提现(Withdrawal):
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
Deposit: Deposit,
Withdrawal: Withdrawal,
}
#[derive(Drop, starknet::Event)]
struct Deposit {
user: ContractAddress,
amount: u256,
}
#[derive(Drop, starknet::Event)]
struct Withdrawal {
user: ContractAddress,
amount: u256,
}
每个事件记录用户的地址以及存入或提取的 STRK 数量。
现在,让我们实现之前定义的金库接口函数:
deposit 函数当用户调用 deposit(amount) 时,该函数验证金额大于零,然后使用 transfer_from 将 STRK 代币从用户账户转移到金库。这要求用户事先批准金库合约。转移成功后,用户的余额在存储中更新,并发出 Deposit 事件。
fn deposit(ref self: ContractState, amount: u256) {
let caller = get_caller_address();
assert(amount > 0, '金额必须大于 0');
// 将 STRK 从用户转移到此合约
let strk = IERC20Dispatcher { contract_address: self.strk_token.read() };
let success = strk.transfer_from(caller, starknet::get_contract_address(), amount);
assert(success, '转移失败');
// 更新用户余额
let current_balance = self.balances.entry(caller).read();
self.balances.entry(caller).write(current_balance + amount);
// 发出存款事件
self.emit(Deposit { user: caller, amount });
}
withdraw 函数提现函数确保用户有非零余额,然后从 Pragma 获取当前 STRK 价格。如果价格达到或超过 $0.16,它会重置用户余额(遵循检查-效果-交互模式),将他们的 STRK 代币转回他们的账户,并发出 Withdrawal 事件。
fn withdraw(ref self: ContractState) {
let caller = get_caller_address();
let balance = self.balances.entry(caller).read();
assert(balance > 0, '没有余额可提现');
// 获取当前 STRK 价格
let current_price = self.get_strk_price();
// 检查价格阈值是否满足
assert(current_price >= PRICE_THRESHOLD, '价格阈值未满足');
// 在转移前重置余额(CEI 模式)
self.balances.entry(caller).write(0);
// 将 STRK 转回用户
let strk = IERC20Dispatcher { contract_address: self.strk_token.read() };
let success = strk.transfer(caller, balance);
assert(success, '转移失败');
// 发出提现事件
self.emit(Withdrawal { user: caller, amount: balance });
}
get_balance 函数返回特定用户已存入金库的 STRK 数量:
fn get_balance(self: @ContractState, user: ContractAddress) -> u256 {
self.balances.entry(user).read()
}
get_strk_price 函数从 Pragma 预言机获取当前 STRK/USD 价格:
fn get_strk_price(self: @ContractState) -> u128 {
let oracle = IPragmaABIDispatcher { contract_address: self.pragma_oracle.read() };
let response: PragmaPricesResponse = oracle.get_data_median(DataType::SpotEntry(STRK_USD_PAIR_ID));
response.price
}
首先,它创建一个合约调度器,使用存储的地址与 Pragma 预言机合约交互:
let oracle = IPragmaABIDispatcher { contract_address: self.pragma_oracle.read() };
然后调用 get_data_median(),传递 DataType::SpotEntry(STRK_USD_PAIR_ID) 以请求 STRK/USD 的当前现货价格。该方法聚合来自多个发布者的数据并返回中位数,这可以防止任何单个来源操纵结果:
let response: PragmaPricesResponse = oracle.get_data_median(DataType::SpotEntry(STRK_USD_PAIR_ID));
get_data_median() 返回一个 PragmaPricesResponse 结构体,包含以下字段:
struct PragmaPricesResponse {
price: u128, // 聚合价格
decimals: u32, // 小数位数
last_updated_timestamp: u64, // 价格最后更新时间
num_sources_aggregated: u32, // 聚合中使用的来源数量
expiration_timestamp: Option<u64>, // 可选过期时间
}
get_strk_price 仅返回该结构体中的 price 字段:
response.price
价格以 u128 返回,精度为 8 位小数。例如,如果 STRK 交易价格为 $0.15,则返回 15000000(因为 Pragma 使用 8 位小数:0.15 × 10^8)。
通过使用
get_data_median(),我们接受 Pragma 的默认初始聚合(所有发布者提交价格的中位数)。由于我们查询的是单个交易对,所有发布者对 STRK/USD 的中位数就是我们的最终价格。
将完整的金库合约复制到 lib.cairo 中,并使用 scarb build 编译:
use starknet::ContractAddress;
#[starknet::interface]
trait IERC20<TContractState> {
fn transfer_from(
ref self: TContractState,
sender: ContractAddress,
recipient: ContractAddress,
amount: u256
) -> bool;
fn transfer(ref self: TContractState, recipient: ContractAddress, amount: u256) -> bool;
}
#[starknet::interface]
trait ISimpleVault<TContractState> {
fn deposit(ref self: TContractState, amount: u256);
fn withdraw(ref self: TContractState);
fn get_balance(self: @TContractState, user: ContractAddress) -> u256;
fn get_strk_price(self: @TContractState) -> u128;
}
#[starknet::contract]
mod SimpleVault {
use starknet::{ContractAddress, get_caller_address};
use pragma_lib::abi::{IPragmaABIDispatcher, IPragmaABIDispatcherTrait};
use pragma_lib::types::{DataType, PragmaPricesResponse};
use starknet::storage::{Map, StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess};
// 为此 ERC20 调度器添加导入
use super::{IERC20Dispatcher, IERC20DispatcherTrait};
// 常量
const STRK_USD_PAIR_ID: felt252 = 6004514686061859652; // STRK/USD 交易对 ID
const PRICE_THRESHOLD: u128 = 16000000; // 8 位小数的 $0.16 (0.16 * 10^8)
#[storage]
struct Storage {
pragma_oracle: ContractAddress,
strk_token: ContractAddress,
balances: Map<ContractAddress, u256>,
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
Deposit: Deposit,
Withdrawal: Withdrawal,
}
#[derive(Drop, starknet::Event)]
struct Deposit {
user: ContractAddress,
amount: u256,
}
#[derive(Drop, starknet::Event)]
struct Withdrawal {
user: ContractAddress,
amount: u256,
}
#[constructor]
fn constructor(
ref self: ContractState,
pragma_oracle: ContractAddress,
strk_token: ContractAddress
) {
self.pragma_oracle.write(pragma_oracle);
self.strk_token.write(strk_token);
}
#[abi(embed_v0)]
impl SimpleVaultImpl of super::ISimpleVault<ContractState> {
fn deposit(ref self: ContractState, amount: u256) {
let caller = get_caller_address();
assert(amount > 0, '金额必须大于 0');
// 将 STRK 从用户转移到此合约
let strk = IERC20Dispatcher { contract_address: self.strk_token.read() };
let success = strk.transfer_from(caller, starknet::get_contract_address(), amount);
assert(success, '转移失败');
// 更新用户余额
let current_balance = self.balances.entry(caller).read();
self.balances.entry(caller).write(current_balance + amount);
// 发出存款事件
self.emit(Deposit { user: caller, amount });
}
fn withdraw(ref self: ContractState) {
let caller = get_caller_address();
let balance = self.balances.entry(caller).read();
assert(balance > 0, '没有余额可提现');
// 获取当前 STRK 价格
let current_price = self.get_strk_price();
// 检查价格阈值是否满足
assert(current_price >= PRICE_THRESHOLD, '价格阈值未满足');
// 在转移前重置余额(CEI 模式)
self.balances.entry(caller).write(0);
// 将 STRK 转回用户
let strk = IERC20Dispatcher { contract_address: self.strk_token.read() };
let success = strk.transfer(caller, balance);
assert(success, '转移失败');
// 发出提现事件
self.emit(Withdrawal { user: caller, amount: balance });
}
fn get_balance(self: @ContractState, user: ContractAddress) -> u256 {
self.balances.entry(user).read()
}
fn get_strk_price(self: @ContractState) -> u128 {
let oracle = IPragmaABIDispatcher { contract_address: self.pragma_oracle.read() };
let response: PragmaPricesResponse = oracle.get_data_median(DataType::SpotEntry(STRK_USD_PAIR_ID));
response.price
}
}
}
首先,使用 sncast 声明合约:
sncast --account <YOUR_ACCOUNT_NAME> \
declare \
--url https://starknet-sepolia.g.alchemy.com/starknet/version/rpc/v0_10/<YOUR_API_KEY> \
--contract-name SimpleVault

Pragma 提供了部署所需的两个地址。我们将它们作为参数传递给构造函数:
0x036031daa264c24520b11d93af622c848b2499b66b41d611bac95e13cfca131a0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d你可以在 Pragma 的 GitHub 仓库 中找到最新的部署地址。
使用构造函数参数部署合约:
sncast \
--account new_account1 \
deploy \
--url https://starknet-sepolia.g.alchemy.com/starknet/version/rpc/v0_10/<YOUR_API_KEY> \
--class-hash <CLASS_HASH> \
--constructor-calldata "0x036031daa264c24520b11d93af622c848b2499b66b41d611bac95e13cfca131a" "0x04718f5a0Fc34cC1AF16A1cdee98fFB20C31f5cD61D6Ab07201858f4287c938D"
从 declare 输出中复制 <CLASS_HASH> 并粘贴到 deploy 命令中。终端将显示包含已部署金库合约地址的交易收据。

保存地址以供后续步骤使用。
部署好金库后,让我们通过 Voyager 逐步完成 STRK 代币的存款和提现。
在存入之前,必须批准金库转移 STRK 代币。导航到 Voyager 上 STRK 代币合约的“Write Contract”选项卡,并调用 approve 函数,将金库合约地址作为 spender,并输入你要存入的 STRK 数量。完成后,执行交易并等待确认。

获得批准后,现在可以存入代币。在金库合约的“Write Contract”选项卡上,使用你之前批准的金额调用 deposit 函数。执行此交易后,STRK 代币被锁定在金库中。

要验证存款是否成功,导航到“Read Contract”选项卡并使用钱包地址调用 get_balance。它应该返回余额,确认 STRK 存款。

在“Read Contract”选项卡中调用 get_strk_price 以检查来自 Pragma 的当前 STRK 价格。该函数返回一个具有 8 位小数的整数价格。要将其转换为美元价值,请将返回的数字除以 100,000,000。

例如,这里函数返回 14716561。除以 100,000,000:
14716561 ÷ 100,000,000 = 0.14716561
因此,此查询时的当前 STRK 价格为 $0.147。
现在尝试提取已存入的 STRK。在“Write Contract”选项卡中,调用 withdraw 函数并执行交易。
结果取决于当前价格。如果 STRK 低于 $0.16,交易将失败并显示错误消息“价格阈值未满足”。如果 STRK 已达到 $0.16 或更高,代币将返回给钱包。

要立即测试成功的提现,可以重新声明并重新部署合约,使用更低的阈值,例如 10000000(代表 $0.10),或者等待 STRK 价格自然上涨至 $0.16 以上。
集成 Pragma 的价格馈送使借贷协议、预测市场、动态 NFT 和其他应用程序能够在链上访问经过验证的价格数据。目前,金库仅根据价格锁定和释放资金。除了价格条件之外,没有收益或奖励机制。合约可以通过添加适合特定用例的功能进行扩展:为存款人提供质押奖励、为不同用户层级提供多个价格阈值,或者时间加权解锁。
Pragma 的计算馈送提供的不仅仅是现货价格。波动率馈送可衡量市场波动,适用于风险调整协议。TWAP 提供时间平均价格,可抵抗闪电贷攻击的操纵。探索这些馈送、尝试不同的资产对以及组合条件,可以揭示预言机如何转换合约以动态响应现实世界的条件。
- 原文链接: rareskills.io/post/cairo...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!
作者暂未设置收款二维码