Pragma 是一个为 Starknet 构建的预言机协议,可将链下价格数据引入链上。它提供喂价和计算数据馈送(例如收益率曲线和波动率数据)。
在本文中,你将学习如何将 Pragma 的喂价预言机集成到你的 Cairo 合约中。
Pragma 工作原理概述
在集成喂价之前,我们先从宏观层面解释一下 Pragma 的工作原理。
Pragma 从多个独立的数据源收集价格数据,并在链上对其进行聚合,以提供可靠、抗操纵的喂价。关键的区别在于,Pragma 直接在 Starknet 上执行所有聚合,使整个过程透明且可验证。
Pragma 通过一个双层系统来聚合数据:
-
Sources(数据源)是价格产生的实际交易所、数据提供商和市场,例如 Binance、Coinbase、OKX、Ekubo、Chainlink 等。
-
Publishers(发布者)是由 Pragma 管理员控制的注册表列入白名单的实体。它们监控来自各个数据源的价格,并将此数据提交给预言机合约。你可以在 Pragma 的发布者注册表合约中找到发布者的完整列表。在撰写本文时,当前的 publishers 包括以下内容:

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

因此,当 ARGENT(一个发布者)想要报告价格时,它会监控上方红框中突出显示的多个数据源,并将观察到的价格数据提交给 Pragma 的预言机合约。
价格数据如何从数据源流向你的合约
-
数据收集
发布者监控分配给它们的数据源(交易所、预言机、数据提供商)并收集当前的价格观察结果。
-
链上提交
发布者为收集到的数据添加时间戳,然后将其直接提交到 Starknet 上的 Pragma 预言机合约中。这里没有中心化的链下聚合层;每个发布者都会独立地在链上提交他们的数据,使其能够被公开验证。例如,如果 10 个发布者提交了 ETH 价格,Pragma 在链上就有 10 个数据点可供使用。
-
价格计算
Pragma 使用两步聚合过程来计算最终价格:
- 按数据源聚合:对于每个单独的数据源(例如 Binance),Pragma 会根据所有报告了该数据源数据的发布者计算出价格中位数。例如,如果三个发布者各自提交了他们从 Binance 观察到的 ETH/USD 价格,Pragma 会取这三个值的中位数,以获得 Binance 的单一共识价格。
- 跨数据源聚合:在从每个数据源(Binance、Coinbase、OKX 等)获得共识价格后,你的智能合约可以选择如何将这些数据源价格组合成最终值。你可以根据需求使用中位数、平均数、时间加权平均价格(TWAP)或其他聚合方法。
-
智能合约查询
你的合约向 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;
}
定义 ERC20 接口
由于金库在存款和取款期间需要转移 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;
}
合约导入
首先声明合约模块并导入所需的类型、trait 和函数:
#[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 预言机合约函数的合约分发器和 traitDataType:指定我们所需的数据类型(当前市场价格的现货价格)PragmaPricesResponse:保存 Pragma 返回的价格信息
定义合约常量
我们定义两个关键常量:
// Constants
const STRK_USD_PAIR_ID: felt252 = 6004514686061859652; // STRK/USD pair ID from Pragma
const PRICE_THRESHOLD: u128 = 16000000; // $0.16 in 8 decimals (0.16 * 10^8)
STRK_USD_PAIR_ID 标识了要查询哪个 Pragma 喂价。在本例中,我们要查询的是 STRK/USD 喂价。交易对 ID 6004514686061859652 对应大写股票代码字符串 'STRK/USD' 的 UTF-8 编码。这意味着交易对 ID 常量可以写得更具可读性:
const STRK_USD_PAIR_ID: felt252 = 'STRK/USD';
同样的模式也适用于其他价格交易对。每个交易对 ID 只是其大写代码字符串的 UTF-8 编码。例如,'ETH/USD' 可以写成字符串 'ETH/USD' 或其 felt252 数值 19514442401534788,两者在 Cairo 中是等效的。可以从 Pragma 的 price feeds 文档中获取其他资产交易对的 ID。每个资产交易对都有自己独特的 ID 和小数精度;在选择不同的交易对时,请务必检查 Decimals(小数位数)列。Sepolia 测试网和主网使用的是相同的交易对 ID。

PRICE_THRESHOLD 设置了允许提款所需的最低 STRK 价格($0.16)。由于 Pragma 返回的 STRK 价格具有 8 位小数精度,我们将 $0.16 表示为 16,000,000 (0.16 × 10^8)。
定义合约存储
Storage 结构体定义了我们的合约存储哪些数据:
#[storage]
struct Storage {
pragma_oracle: ContractAddress, //pragma_oracle contract address
strk_token: ContractAddress, // strk token contract address
balances: Map<ContractAddress, u256>, // user balances
}
我们存储的内容:
pragma_oracle:Pragma 在 Starknet 上的预言机合约地址,用于获取实时资产价格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 代币从用户账户转移到金库。这需要用户事先批准(approve)金库合约。转移成功后,存储中的用户余额将被更新,并发出一个 Deposit 事件。
fn deposit(ref self: ContractState, amount: u256) {
let caller = get_caller_address();
assert(amount > 0, 'Amount must be greater than 0');
// Transfer STRK from user to this contract
let strk = IERC20Dispatcher { contract_address: self.strk_token.read() };
let success = strk.transfer_from(caller, starknet::get_contract_address(), amount);
assert(success, 'Transfer failed');
// Update user balance
let current_balance = self.balances.entry(caller).read();
self.balances.entry(caller).write(current_balance + amount);
// Emit deposit event
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, 'No balance to withdraw');
// Get current STRK price
let current_price = self.get_strk_price();
// Check if price threshold is met
assert(current_price >= PRICE_THRESHOLD, 'Price threshold not met');
// Reset balance before transfer (CEI pattern)
self.balances.entry(caller).write(0);
// Transfer STRK back to user
let strk = IERC20Dispatcher { contract_address: self.strk_token.read() };
let success = strk.transfer(caller, balance);
assert(success, 'Transfer failed');
// Emit withdrawal event
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, // The aggregated price
decimals: u32, // Number of decimal places
last_updated_timestamp: u64, // When the price was last updated
num_sources_aggregated: u32, // Number of sources used in aggregation
expiration_timestamp: Option<u64>, // Optional expiration time
}
get_strk_price 仅返回该结构体中的 price 字段:
response.price
返回的价格是一个具有 8 位小数精度的 u128。例如,如果 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};
// Add this import for the ERC20 dispatcher
use super::{IERC20Dispatcher, IERC20DispatcherTrait};
// Constants
const STRK_USD_PAIR_ID: felt252 = 6004514686061859652; // STRK/USD pair ID
const PRICE_THRESHOLD: u128 = 16000000; // $0.16 in 8 decimals(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, 'Amount must be greater than 0');
// Transfer STRK from user to this contract
let strk = IERC20Dispatcher { contract_address: self.strk_token.read() };
let success = strk.transfer_from(caller, starknet::get_contract_address(), amount);
assert(success, 'Transfer failed');
// Update user balance
let current_balance = self.balances.entry(caller).read();
self.balances.entry(caller).write(current_balance + amount);
// Emit deposit event
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, 'No balance to withdraw');
// Get current STRK price
let current_price = self.get_strk_price();
// Check if price threshold is met
assert(current_price >= PRICE_THRESHOLD, 'Price threshold not met');
// Reset balance before transfer (CEI pattern)
self.balances.entry(caller).write(0);
// Transfer STRK back to user
let strk = IERC20Dispatcher { contract_address: self.strk_token.read() };
let success = strk.transfer(caller, balance);
assert(success, 'Transfer failed');
// Emit withdrawal event
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 提供了部署所需的两个地址。我们会将它们作为参数传递给构造函数:
- Pragma Oracle:
0x036031daa264c24520b11d93af622c848b2499b66b41d611bac95e13cfca131a - STRK Token:
0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d
你可以在 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 代币。
授权(Approve)金库
在存款之前,必须授权金库以转移 STRK 代币。导航至 Voyager 上 STRK 代币合约的“Write Contract”选项卡,并调用 approve 函数,将金库的合约地址作为 spender,将你打算存入的 STRK 数量作为参数。完成后,执行交易并等待确认。

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

检查余额
若要验证存款是否成功,请导航至“Read Contract”选项卡并使用钱包地址调用 get_balance。它应该会返回余额,从而确认 STRK 存款完成。

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

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

若要立即测试成功取款,可以使用较低的阈值(如代表 $0.10 的 10000000)重新声明并重新部署合约,或者也可以等待 STRK 价格自然上涨到 $0.16 以上。
结论
集成 Pragma 的喂价可以使借贷协议、预测市场、动态 NFT 和其他应用程序能够在链上访问经过验证的价格数据。现在,该金库仅根据价格简单地锁定和释放资金。除了价格条件外,没有任何收益或奖励机制。我们可以使用适合特定用例的功能来扩展该合约:为存款人提供质押奖励、为不同用户层级设置多个价格阈值或时间加权的解锁机制。
Pragma 的计算数据馈送提供的不只是现货价格。波动率数据可为具有风险调整机制的协议衡量市场动荡程度。TWAP 提供了可抵抗闪电贷攻击操纵的时间平均价格。探索这些数据馈送,尝试不同的资产对,并结合不同的条件,你会发现预言机是如何重塑合约以动态响应现实世界情况的。