Back to Blog

Solana 简化 02:从零开始编写你的第一个 Solana 智能合约

June 21, 2024
12 min read

引言

2024年,Solana 凭借其锁仓总价值 (TVL) 跃升至第四大公链,吸引了投资者和开发者的广泛关注。

BlockSec 推出了“Solana 简明教程”系列,包含 Solana 基础概念、Solana 智能合约编写教程以及 Solana 交易分析指南。旨在帮助读者理解 Solana 生态系统,并掌握在 Solana 上开发项目和进行交易的关键技能。

在本系列的第一篇文章 “Solana 简明教程:一文掌握 Solana 核心概念” 中,我们深入探讨了 Solana 网络的核心概念,包括其运行机制、账户模型和交易,为编写正确且高性能的 Solana 合约奠定了坚实的基础。

在本文中,我们将指导您编写一个用于发布和展示文章的 Solana 程序(即 Solana 智能合约)。这将有助于巩固第一篇文章中的概念,并介绍一些我们尚未讨论的 Solana 特性。相关程序和测试代码已发布在 GitHub 上。

设置环境

注意:以下命令仅适用于 Ubuntu 系统。某些命令在 Windows 和 macOS 系统上可能无法正常工作。

👉 您可以使用替代命令或参考“设置本地开发并安装 Solana CLI”来解决此问题。

我们将在本地环境中编译和部署 Solana 程序。在深入进行 Solana 程序开发之前,我们需要安装一些命令和依赖项。

Rust

Solana 程序主要使用 Rust 编程语言编写,因此我们需要执行以下命令来安装 Rust:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Node.js & TypeScript

测试脚本是用 TypeScript 编写的,因此我们需要安装 Node.js 和 TypeScript 工具链:

curl -fsSL https://deb.nodesource.com/setup_15.x | sudo -E bash -
sudo apt-get install -y nodejs npm
npm install -g typescript
npm install -g ts-node

Solana CLI

接下来,我们需要安装 Solana CLI 工具,它提供了诸如创建钱包、部署合约等任务的命令:

sh -c "$(curl -sSfL https://release.solana.com/stable/install)"

运行以下命令将更改应用到当前终端:

source ~/.profile

您可以执行 solana --version 来验证 Solana CLI 是否已成功安装:

$ solana --version
solana-cli 1.17.25 (src:d0ed878d; feat:3580551090, client:SolanaLabs)

本地钱包

接下来,执行以下命令生成一个本地文件系统钱包:

solana-keygen new

默认情况下,此命令会在 ~/.config/solana/id.json 创建一个私钥文件,我们将在稍后的测试脚本中使用它。您可以使用 solana address 命令查看它生成的地址:

$ solana address
9FcGDHvzs8Czo4B1pQq8GPdXfdGHqNqayvh6bu4hVGfy

Devnet

程序将部署在 devnet 上,因此我们需要执行以下命令将 Solana CLI 切换到 devnet:

solana config set --url https://api.devnet.solana.com

SOL 代币

由于部署合约和发送测试交易并非免费,我们需要请求一些 SOL 代币。您可以执行 solana airdrop 2 或直接从 公共网页水龙头 请求代币。

编写程序

我们将介绍的程序允许用户发布文章并列出所有已发布的文章。它处理三种类型的指令:

  • init 指令:通过创建一个数据账户来初始化程序,该账户用于存储文章的当前最大索引。
  • post 指令:将发布的文章存储在一个新的数据账户中。
  • list 指令:在日志中打印所有发布的文章。

程序结构如下:

$ tree program/
program/
├── Cargo.lock
├── Cargo.toml
└── src
    ├── instructions
    │   ├── init.rs
    │   ├── list.rs
    │   ├── mod.rs
    │   └── post.rs
    ├── lib.rs
    └── processor.rs

Cargo.toml 指定了项目的外部库:

[dependencies]
borsh = "0.10.0"
borsh-derive = "0.10.0"
solana-program = "=1.17.25"

注意:solana-program 的版本应与安装的 Solana CLI 版本匹配(您可以使用 solana --version 进行检查)。否则,编译时可能会遇到错误。

合约的入口函数定义在 processor.rs 中。该文件首先导入所需的依赖项(我们将在其他 Rust 文件中跳过类似部分):

接下来是合约入口函数的定义:

在程序的第 10 行,solana 库中的 entrypoint! 宏指定入口函数是 process_instruction。此函数接受三个参数:

  • program_id:当前程序的部署地址。
  • accounts:指令涉及的所有账户。
  • instruction_data:用于处理指令的字节数组。

process_instruction 提取 instruction_data 的第一个字节来识别指令类型,并调用相应的函数进行处理。接下来,我们将看看这三个函数的实现。

init

三个函数中的每一个都定义在 program/src/instructions 目录下的同名文件中。让我们从 init.rs 开始。

由于 init 指令不需要额外的字节数组,它只接受两个参数:program_idaccounts

在第 12 至 15 行之间,程序依次提取所需的账户信息,然后调用 find_program_address 函数来计算用于存储当前最大文章索引的数据账户 index_pda_key 的地址。然后程序断言 index_pda_key 等于 index_pda 的地址。

这里,index_pda 的地址是一种特殊的地址,称为 PDA(程序派生地址)。与从公钥生成的“钱包”账户地址不同,PDA 是从可选的字节数组、一个称为“bump”的字节以及程序的地址派生出来的。字节数组和程序地址由调用者明确提供,而 bump 由 find_program_address 函数自动生成并返回。bump 确保这样创建的地址不会与公钥生成的地址冲突。

在确认 index_pda 的地址是预期的之后,我们使用 SystemProgram 提供的 create_account 指令来创建 index_pda。在第 22 行,程序创建了一个 IndexAccountData 类型的变量,该变量记录了文章的当前最大索引(初始化为 0)。如下图所示,此类型实现了 BorshSerializeBorshDeserialize trait,允许它以 Borsh 格式进行序列化和反序列化:

第 23 至 24 行计算了存储账户数据所需的空间以及创建账户所需的最低租金。

第 27 至 37 行使用 invoke_signed 方法从 SystemProgram 创建并调用 create_account 指令。在一个程序中调用另一个程序的指令的操作称为 CPI(跨程序调用)。在 Solana 中,您可以使用两个函数来执行 CPI:invokeinvoke_signed

invoke 函数直接处理给定的指令,其函数签名如下:

pub fn invoke(
    instruction: &Instruction,
    account_infos: &[AccountInfo<'_>]
) -> Result<(), ProgramError>

invoke_signedinvoke 的唯一区别在于 invoke_signed 提供了为 PDA 账户签名的方式。PDA 账户没有公私钥,因此无法直接提供签名。invoke_signed 解决了这个问题。此函数接受一个额外的参数,称为 signers_seeds。其函数签名如下。

find_program_address 函数的工作方式类似,invoke_signed 根据提供的 signers_seeds 和调用者的 program_id 计算一组 PDA 地址。如果 account_infos 包含这些 PDA 地址,它们将被标记为签名者,就像它们提供了签名一样。

pub fn invoke_signed(
    instruction: &Instruction,
    account_infos: &[AccountInfo<'_>],
    signers_seeds: &[&[&[u8]]]
) -> Result<(), ProgramError>

在这里,由于 create_account 指令需要被创建的账户作为签名者,我们必须使用 invoke_signedindex_pda 提供签名。

在程序结束时,合约调用 index_data 上的 serialize 方法将其序列化,并写入 index_pdadata 字段。

post_article

接下来我们来看 post 指令的实现。与 init 指令不同,post 指令接受一个已发布的文章,因此它有一个额外的参数 instruction_data_inner,用于存储文章的序列化数据。

一如既往,程序首先提取指令所需的账户信息。由于我们为每篇文章生成一个单独的 PDA 账户,因此程序在第 23 行接受一个额外的账户 new_article_pda

init 指令类似,第 27 至 30 行使用 find_program_address 来验证传入的 index_pda 地址是否正确。然后我们反序列化其 data 字段中的数据,以读取当前分配的最大索引。

第 33 至 40 行也验证了 new_article_pda 的地址。程序使用字符串“ARTICLE_PDA”和当前索引来生成新地址。每次上传文章时,索引都会递增,这确保了生成的地址是唯一的。

接下来,程序将 instruction_data_inner 反序列化为文章数据,其类型为 PostArticleData

该结构只有两个字段:titlecontent。在第 45 至 49 行,程序进行检查,以确保文章不会过大,无法包含在单个账户中。

接下来的步骤也与 init 指令类似:程序首先计算账户所需的空间和租金,然后调用 SystemProgramcreate_account 指令来创建用于存储文章的 PDA 账户。最后,将发布的文章序列化到 new_article_pdadata 字段,并将当前最大索引加一,序列化到 index_pdadata 字段。

list_articles

最后,让我们看看 list 指令的实现。由于此指令需要列出所有文章,因此在第 15 行使用了一个向量来表示存储文章的所有 PDA 账户,并且 index_pda 账户的地址也得到了验证。

之后,程序验证向量的大小是否与当前最大索引相同,并检查每个账户地址的正确性。

在程序结束时,它遍历每个账户,并使用 solana-program 中的 msg! 宏逐个输出反序列化的文章内容。

交易测试

为了测试我们的合约,我们使用了存储在仓库 client/main.ts 中的 TypeScript 脚本。

在脚本的顶部,我们导入了所有必要的库并定义了三个全局常量。KEYPAIR_PATH 指示使用 solana-keygen new 生成的私钥文件的路径,PROGRAM_ID 是我们在上一节中编写的已部署程序的地址,POST_ARTICLE_SCHEMA 是用于文章序列化的对象。

在脚本的主体中,它首先调用 loadKeyFromFile 函数来解析私钥文件并获取一个 Keypair 对象作为交易费用的支付者。然后,它使用 @solana/web3.js 提供的 findProgramAddressSync 方法来计算 indexPda 的地址。此方法使用与 Rust 合约相同的算法,确保它使用相同的参数计算相同的地址。connection 对象指定我们将使用 devnet 进行测试。

接下来,我们发送第一个初始化交易。我们创建一个包含单个指令的交易。此指令的 data 字段仅包含一个字节“0”,表示这是对 init 指令的调用。然后使用 sendAndConfirmTransaction 发送并确认交易。

第二个交易调用 post 指令来发布一些文章。从第 56 至 63 行,脚本序列化了两篇文章并计算了相应的 PDA 地址。然后将两个 post 指令添加到单个交易中以创建这些文章。

最后一个交易调用 list 指令,然后在交易确认后打印日志。

要使用脚本测试程序,请将 KEYPAIR_PATH 替换为您的私钥路径(如果不是默认路径)。然后,通过执行以下命令编译并部署 Rust 智能合约:

cd program
cargo build-bpf
solana program deploy ./target/deploy/hello_solana.so

然后,复制部署的地址并粘贴到 PROGRAM_ID 字段中。在项目根目录下,运行 npm install 安装依赖项,然后运行 npm start 来执行脚本。

之后,我们可以使用 Solscan 查看测试交易的执行详情。

让我们直接查看 发布文章的第二笔交易。它包含两个指令,每个指令的 Instruction Data Raw 的第一个字节都是 0x01。此外,每个指令都包含一个内部指令,这是对 System ProgramCreateAccount 指令的调用。

同样,我们可以检查 列出所有文章的第三笔交易。在 Program Logs 部分,我们可以发现文章的索引、标题和内容都被打印出来了。

结论

在本文中,我们首先解释了如何在本地设置 Solana 编程和运行时环境。然后,我们详细介绍了 Solana 合约的实现逻辑。最后,我们使用 TypeScript 脚本测试了合约功能。通过这个循序渐进的教程,我们相信您已经学会了如何编写一个简单的 Solana 智能合约 🥳。

在下一篇文章中,我们将提供一个详细的指南,介绍如何使用 Phalcon Explorer(Phalcon Explorer 现在支持 Solana)查看和分析 Solana 交易。敬请期待!

阅读本系列的更多文章:

Sign up for the latest updates
The Decentralization Dilemma: Cascading Risk and Emergency Power in the KelpDAO Crisis
Security Insights

The Decentralization Dilemma: Cascading Risk and Emergency Power in the KelpDAO Crisis

This BlockSec deep-dive analyzes the KelpDAO $290M rsETH cross-chain bridge exploit (April 18, 2026), attributed to the Lazarus Group, tracing a causal chain across three layers: how a single-point DVN dependency enabled the attack, how DeFi composability cascaded the damage through Aave V3 lending markets to freeze WETH liquidity exceeding $6.7B across Ethereum, Arbitrum, Base, Mantle, and Linea, and how the crisis forced decentralized governance to exercise centralized emergency powers. The article examines three parameters that shaped the cascade's severity (LTV, pool depth, and cross-chain deployment count) and provides an exclusive technical breakdown of Arbitrum Security Council's forced state transition, an atomic contract upgrade that moved 30,766 ETH without the holder's signature.

Weekly Web3 Security Incident Roundup | Apr 13 – Apr 19, 2026
Security Insights

Weekly Web3 Security Incident Roundup | Apr 13 – Apr 19, 2026

This BlockSec weekly security report covers four attack incidents detected between April 13 and April 19, 2026, across multiple chains such as Ethereum, Unichain, Arbitrum, and NEAR, with total estimated losses of approximately $310M. The highlighted incident is the $290M KelpDAO rsETH bridge exploit, where an attacker poisoned the RPC infrastructure of the sole LayerZero DVN to fabricate a cross-chain message, triggering a cascading WETH freeze across five chains and an Arbitrum Security Council forced state transition that raises questions about the actual trust boundaries of decentralized systems. Other incidents include a $242K MMR proof forgery on Hyperbridge, a $1.5M signed integer abuse on Dango, and an $18.4M circular swap path exploit on Rhea Finance's Burrowland protocol.

Weekly Web3 Security Incident Roundup | Apr 6 – Apr 12, 2026
Security Insights

Weekly Web3 Security Incident Roundup | Apr 6 – Apr 12, 2026

This BlockSec weekly security report covers four DeFi attack incidents detected between April 6 and April 12, 2026, across Linea, BNB Chain, Arbitrum, Optimism, Avalanche, and Base, with total estimated losses of approximately $928.6K. Notable incidents include a $517K approval-related exploit where a user mistakenly approved a permissionless SquidMulticall contract enabling arbitrary external calls, a $193K business logic flaw in the HB token's reward-settlement logic that allowed direct AMM reserve manipulation, a $165.6K exploit in Denaria's perpetual DEX caused by a rounding asymmetry compounded with an unsafe cast, and a $53K access control issue in XBITVault caused by an initialization-dependent check that failed open. The report provides detailed vulnerability analysis and attack transaction breakdowns for each incident.