meta: "Explore the fundamentals of smart contracts in this comprehensive introduction, tailored for developers using Cairo. Learn how smart contracts work and their role in blockchain technology."
Introduction to Smart Contracts
This chapter will give you a high level introduction to what smart contracts are, what they are used for, and why blockchain developers would use Cairo and Starknet. If you are already familiar with blockchain programming, feel free to skip this chapter. The last part might still be interesting though.
Smart Contracts - Introduction
随着以太坊的诞生,智能合约得到了普及并变得更加广泛。智能合约本质上是部署在区块链上的程序。术语 "智能合约 "有些误导,因为它们既不 "智能 "也不是 "合约",而只是根据特定输入执行的代码和指令。它们主要由两部分组成:存储和函数。部署后,用户可以通过启动包含执行数据的区块链交易(调用哪个函数,输入什么参数)与智能合约互动。智能合约可以修改和读取底层区块链的存储。智能合约有自己的地址,因此它是一个区块链账户,意味着它可以持有代币。
The programming language used to write smart contracts varies depending on the blockchain. For example, on Ethereum and the EVM-compatible ecosystem, the most commonly used language is Solidity, while on Starknet, it is Cairo. The way the code is compiled also differs based on the blockchain. On Ethereum, Solidity is compiled into bytecode. On Starknet, Cairo is compiled into Sierra and then into Cairo Assembly (CASM).
Smart contracts possess several unique characteristics. They are permissionless, meaning anyone can deploy a smart contract on the network (within the context of a decentralized blockchain, of course). Smart contracts are also transparent; the data stored by the smart contract is accessible to anyone. The code that composes the contract can also be transparent, enabling composability. This allows developers to write smart contracts that use other smart contracts. Smart contracts can only access and interact with data from the blockchain they are deployed on. They require third-party software (called oracles) to access external data (the price of a token for instance).
For developers to build smart contracts that can interact with each other, it is required to know what the other contracts look like. Hence, Ethereum developers started to build standards for smart contract development, the ERCxx
. The two most used and famous standards are the ERC20
, used to build tokens like USDC
, DAI
or STARK
, and the ERC721
, for NFTs (Non-Fungible Tokens) like CryptoPunks
or Everai
.
Smart Contracts - Use Cases
There are many possible use cases for smart contracts. The only limits are the technical constraints of the blockchain and the creativity of developers.
DeFi
For now, the principal use case for smart contracts is similar to that of Ethereum or Bitcoin, which is essentially handling money. In the context of the alternative payment system promised by Bitcoin, smart contracts on Ethereum enable the creation of decentralized financial applications that no longer rely on traditional financial intermediaries. This is what we call DeFi (decentralized finance). DeFi consists of various projects such as lending/borrowing applications, decentralized exchanges (DEX), on-chain derivatives, stablecoins, decentralized hedge funds, insurance, and many more.
Tokenization
智能合约可以促进现实世界资产的代币化,如房地产、艺术品或贵金属。代币化将资产划分为数字代币,可以在区块链平台上轻松交易和管理。这可以增加流动性,实现部分所有权,并简化购买和销售过程。
Voting
智能合约可用于创建安全和透明的投票系统。投票可以记录在区块链上,确保不可更改性和透明度。然后,智能合约可以自动统计票数并宣布结果,将欺诈或操纵的可能性降到最低。
Royalties
智能合约可以为艺术家、音乐家和其他内容创作者自动支付版税。当一段内容被消费或出售时,智能合约可以自动计算并将版税分配给合法的所有者,确保公平的补偿并减少对中间人的需求。
Decentralized Identities DIDs
智能合约可用于创建和管理数字身份,允许个人控制其个人信息,并与第三方安全地分享。智能合约可以验证用户身份的真实性,并根据用户的凭证自动授予或撤销对特定服务的访问。
As Ethereum continues to mature, we can expect the use cases and applications of smart contracts to expand further, bringing about exciting new opportunities and reshaping traditional systems for the better.
The Rise of Starknet and Cairo
Ethereum, being the most widely used and resilient smart contract platform, became a victim of its own success. With the rapid adoption of some previously mentioned use cases, mainly DeFi, the cost of performing transactions became extremely high, rendering the network almost unusable. Engineers and researchers in the ecosystem began working on solutions to address this scalability issue.
A famous trilemma called The Blockchain Trilemma in the blockchain space states that it is hard to achieve a high level of scalability, decentralization, and security simultaneously; trade-offs must be made. Ethereum is at the intersection of decentralization and security. Eventually, it was decided that Ethereum's purpose would be to serve as a secure settlement layer, while complex computations would be offloaded to other networks built on top of Ethereum. These are called Layer 2s (L2s).
L2的两种主要类型是乐观rollup和有效性rollup。这两种方法都涉及压缩和批量处理大量交易,计算新状态,并将结果结算在以太坊(L1)上。区别在于在L1上结算结果的方式。对于乐观rollup,默认情况下新状态被认为是有效的,但节点有7天的窗口期来识别恶意交易。
与此相反,有效性rollup(如Starknet)使用加密技术来证明新状态的计算是正确的。这就是STARKs的目的,这种加密技术可以使有效性rollup的扩展能力大大超过乐观rollup。你可以从Starkware的Medium文章中了解更多关于STARKs的信息,它是一个很好的入门读物。
Starknet's architecture is thoroughly described in the Starknet documentation, which is a great resource to learn more about the Starknet network.
还记得Cairo吗?事实上,它是一种专门为STARKs开发的语言,并使其具有通用性。使用Cairo,我们可以编写可证明的代码。在 Starknet 中,这可以证明从一个状态到另一个状态的计算的正确性。
大多数(也许不是全部)Starknet的竞争对手都选择使用 EVM(原版或改版)作为基础层,而Starknet则不同,它采用了自己的虚拟机。这使开发人员摆脱了 EVM 的束缚,开辟了更广阔的可能性。加上交易成本的降低,Starknet 与 Cairo 的结合为开发人员创造了一个令人兴奋的乐园。原生账户抽象使我们称之为 "智能账户" 的账户和交易流的逻辑更加复杂。新兴用例包括 透明人工智能 和机器学习应用。最后,区块链游戏 可以完全在 链上 开发。Starknet 经过专门设计,可最大限度地发挥 STARK 证明的能力,实现最佳可扩展性。
Learn more about Account Abstraction in the Starknet documentation.
Cairo Programs and Starknet Smart Contracts: What Is the Difference?
Starknet contracts are a special superset of Cairo programs, so the concepts previously learned in this book are still applicable to write Starknet contracts. As you may have already noticed, a Cairo program must always have a main
function that serves as the entry point for this program:
fn main() {}
Contracts deployed on the Starknet network are essentially programs that are run by the sequencer, and as such, have access to Starknet's state. Contracts do not have a main
function but one or multiple functions that can serve as entry points.
Starknet contracts are defined within modules. For a module to be handled as a contract by the compiler, it must be annotated with the #[starknet::contract]
attribute.
Anatomy of a Simple Contract
This chapter will introduce you to the basics of Starknet contracts using a very simple smart contract as example. You will learn how to write a contract that allows anyone to store a single number on the Starknet blockchain.
Let's consider the following contract for the whole chapter. It might not be easy to understand it all at once, but we will go through it step by step:
#[starknet::interface]
trait ISimpleStorage<TContractState> {
fn set(ref self: TContractState, x: u128);
fn get(self: @TContractState) -> u128;
}
#[starknet::contract]
mod SimpleStorage {
use core::starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[storage]
struct Storage {
stored_data: u128,
}
#[abi(embed_v0)]
impl SimpleStorage of super::ISimpleStorage<ContractState> {
fn set(ref self: ContractState, x: u128) {
self.stored_data.write(x);
}
fn get(self: @ContractState) -> u128 {
self.stored_data.read()
}
}
}
What Is this Contract?
Contracts are defined by encapsulating state and logic within a module annotated with the #[starknet::contract]
attribute.
The state is defined within the Storage
struct, and is always initialized empty. Here, our struct contains a single field called stored_data
of type u128
(unsigned integer of 128 bits), indicating that our contract can store any number between 0 and \( {2^{128}} - 1 \).
The logic is defined by functions that interact with the state. Here, our contract defines and publicly exposes the functions set
and get
that can be used to modify or retrieve the value of the stored variable. You can think of it as a single slot in a database that you can query and modify by calling functions of the code that manages the database.
The Interface: the Contract's Blueprint
#[starknet::interface]
trait ISimpleStorage<TContractState> {
fn set(ref self: TContractState, x: u128);
fn get(self: @TContractState) -> u128;
}
Interfaces represent the blueprint of the contract. They define the functions that the contract exposes to the outside world, without including the function body. In Cairo, they're defined by annotating a trait with the #[starknet::interface]
attribute. All functions of the trait are considered public functions of any contract that implements this trait, and are callable from the outside world.
The contract constructor is not part of the interface. Nor are internal functions.
All contract interfaces use a generic type for the self
parameter, representing the contract state. We chose to name this generic parameter TContractState
in our interface, but this is not enforced and any name can be chosen.
In our interface, note the generic type TContractState
of the self
argument which is passed by reference to the set
function. Seeing the self
argument passed in a contract function tells us that this function can access the state of the contract. The ref
modifier implies that self
may be modified, meaning that the storage variables of the contract may be modified inside the set
function.
On the other hand, the get
function takes a snapshot of TContractState
, which immediately tells us that it does not modify the state (and indeed, the compiler will complain if we try to modify storage inside the get
function).
By leveraging the traits & impls mechanism from Cairo, we can make sure that the actual implementation of the contract matches its interface. In fact, you will get a compilation error if your contract doesn’t conform with the declared interface. For example, Listing 14-3 shows a wrong implementation of the ISimpleStorage
interface, containing a slightly different set
function that doesn't have the same signature.
#[abi(embed_v0)]
impl SimpleStorage of super::ISimpleStorage<ContractState> {
fn set(ref self: ContractState) {}
fn get(self: @ContractState) -> u128 {
self.stored_data.read()
}
}
Trying to compile a contract using this implementation will result in the following error:
$ scarb cairo-run
Compiling listing_99_02 v0.1.0 (listings/ch100-introduction-to-smart-contracts/listing_02_wrong_impl/Scarb.toml)
error: The number of parameters in the impl function `SimpleStorage::set` is incompatible with `ISimpleStorage::set`. Expected: 2, actual: 1.
--> listings/ch100-introduction-to-smart-contracts/listing_02_wrong_impl/src/lib.cairo:23:16
fn set(ref self: ContractState) {}
^*********************^
error: Wrong number of arguments. Expected 2, found: 1
--> listings/ch100-introduction-to-smart-contracts/listing_02_wrong_impl/src/lib.cairo:23:9
fn set(ref self: ContractState) {}
^********************************^
error: could not compile `listing_99_02` due to previous error
error: `scarb metadata` exited with error
Public Functions Defined in an Implementation Block
在我们进一步探讨之前,让我们先定义一些术语。
-
In the context of Starknet, a public function is a function that is exposed to the outside world. A public function can be called by anyone, either from outside the contract or from within the contract itself. In the example above,
set
andget
are public functions. -
What we call an external function is a public function that can be directly invoked through a Starknet transaction and that can mutate the state of the contract.
set
is an external function. -
A view function is a public function that is typically read-only and cannot mutate the state of the contract. However, this limitation is only enforced by the compiler, and not by Starknet itself. We will discuss the implications of this in a later section.
get
is a view function.
#[abi(embed_v0)]
impl SimpleStorage of super::ISimpleStorage<ContractState> {
fn set(ref self: ContractState, x: u128) {
self.stored_data.write(x);
}
fn get(self: @ContractState) -> u128 {
self.stored_data.read()
}
}
Since the contract interface is defined as the ISimpleStorage
trait, in order to match the interface, the public functions of the contract must be defined in an implementation of this trait — which allows us to make sure that the implementation of the contract matches its interface.
However, simply defining the functions in the implementation block is not enough. The implementation block must be annotated with the #[abi(embed_v0)]
attribute. This attribute exposes the functions defined in this implementation to the outside world — forget to add it and your functions will not be callable from the outside. All functions defined in a block marked as #[abi(embed_v0)]
are consequently public functions.
Because the SimpleStorage
contract is defined as a module, we need to access the interface defined in the parent module. We can either bring it to the current scope with the use
keyword, or refer to it directly using super
.
When writing the implementation of an interface, the self
parameter in the trait methods must be of type ContractState
. The ContractState
type is generated by the compiler, and gives access to the storage variables defined in the Storage
struct. Additionally, ContractState
gives us the ability to emit events. The name ContractState
is not surprising, as it’s a representation of the contract’s state, which is what we think of self
in the contract interface trait. When self
is a snapshot of ContractState
, only read access is allowed, and emitting events is not possible.
Accessing and Modifying the Contract's State
Two methods are commonly used to access or modify the state of a contract:
read
, which returns the value of a storage variable. This method is called on the variable itself and does not take any argument.
self.stored_data.read()
write
, which allows to write a new value in a storage slot. This method is also called on the variable itself and takes one argument, which is the value to be written. Note thatwrite
may take more than one argument, depending on the type of the storage variable. For example, writing on a mapping requires 2 arguments: the key and the value to be written.
self.stored_data.write(x);
Reminder: if the contract state is passed as a snapshot with
@
instead of passed by reference withref
, attempting to modify the contract state will result in a compilation error.
This contract does not do much apart from allowing anyone to store a single number that is accessible by anyone in the world. Anyone could call set
again with a different value and overwrite the current number. Nevertheless, each value stored in the storage of the contract will still be stored in the history of the blockchain. Later in this book, you will see how you can impose access restrictions so that only you can alter the number.