doc
- https://docs.soliditylang.org/zh/latest/introduction-to-smart-contracts.html
- https://ethereum.org/en/developers/
- https://docs.soliditylang.org/en/v0.8.25/metadata.html
call, delegatecall 和 staticcall
在 Solidity 中,call,delegatecall,和staticcall是底层函数,用于与其他合约交互或在合约内部发送以太币。每个函数有不同的用途和特点:
-
call
call用于向另一个合约发送以太币(ETH)和/或任意数据。它可以调用合约的函数,但不会改变调用者的存储。call返回一个布尔值,表示操作成功与否,以及任何返回的数据。- 应用场景:当你想要调用另一个合约的函数,并且你可能不关心它的接口时。
使用示例:
(bool success, bytes memory data) = someContractAddress.call{value: etherAmount}(abi.encodeWithSignature("someFunction(uint256)", arg));
require(success, "Call failed"); -
delegatecall
delegatecall是一个低级别的函数,它调用另一个合约的函数,但是执行环境仍然是当前合约的,这意味着它使用当前合约的存储和 ETH 余额。delegatecall返回一个布尔值和任何返回的数据。- 应用场景:在高级用例,如代理合约或库的函数调用,其中你希望在当前合约的上下文中执行代码,而不是在被调用合约的上下文中。
使用 示例:
(bool success, bytes memory data) = someContractAddress.delegatecall(abi.encodeWithSignature("someFunction(uint256)", arg));
require(success, "Delegatecall failed"); -
staticcall
staticcall类似于call,但它保证不会改变状态,即它不会改变任何存储的数据。这是只读操作。staticcall返回一个布尔值和任何返回的数据。- 应用场景:当你想要确保调用不会产生任何状态变化,例如查询另一个合约的状态而不引起副作用时。
使用示例:
(bool success, bytes memory data) = someContractAddress.staticcall(abi.encodeWithSignature("someFunction(uint256)", arg));
require(success, "Staticcall failed");
安全注意事项:
- 这些函数都跳过了 Solidity 的类型检查和函数签名检查,因此它们可能会增加智能合约的安全风险。使用它们时需要非常谨慎,确保调用的数据是正确的。
- 由于它们允许与任意合约交互,因此存在重入攻击的风险。在执行这些调用后,合约应该小心处理状态变化。
- 从 Solidity 0.5.0 版本开始,当发送 ETH 时,使用
call的新语法(someAddress.call{value: amount}(""))比使用transfer或send更推荐,因为后两者在 gas 上有固定的限制,可能会因为 gas 不足而失败。
在实际应用中,除非有特定的底层需求,否则推荐使用 Solidity 的高级特性(如直接的函数调用和合约接口),因为它们更安全,也更容易理解和维护。
外部调用合约
在 Solidity 中,"外部调用"(External Call)是指一个合约调用另一个合约的函数或者发送以太币到另一个地址的操作。这是合约与区块链上其他合约或账户交互的基本方式。
外部调用可以通过几种不同的方式进行:
-
直接调用: 如果你知道目标合约的地址和它的接口(ABI),你可以创建一个合约实例并直接调用它的方法。例如:
// 假设已知外部合约的接口为SomeContract
SomeContract externalContract = SomeContract(externalContractAddress);
externalContract.someFunction(); -
通过接口调用: 你可以定义一个接口来表示外部合约的部分或全部函数,然后通过这个接口与合约通信。例如:
interface IExternalContract {
function someFunction() external;
}
IExternalContract externalContract = IExternalContract(externalContractAddress);
externalContract.someFunction(); -
低级调用: 你也可以使用
call、delegatecall、或staticcall等低级函数进行更灵活的调用,这些调用允许你指定调用的数据(例如函数选择器和参数)。例如:(bool success, bytes memory data) = externalContractAddress.call(abi.encodeWithSignature("someFunction(uint256)", 123));
require(success, "Call failed"); -
发送以太币: 你可以使用
transfer、send或call(推荐使用call以避免固定的 2300 gas 限制)来发送以太币。例如:// 推荐的方式来发送以太币
(bool sent, ) = externalContractAddress.call{value: amount}("");
require(sent, "Failed to send Ether");
外部调用是合约与外部世界交互的主要方式,但它们也增加了合约的复杂性和潜在的安全风险。例如,外部调用可能会失败,或者它们可能会被恶意合约利用来进行重入攻击。因此,编写合约时需要格外小心,确保正确处理外部调用的结果,并采取 措施防范潜在的安全问题。
为什么可以通过接口调用?
在 Solidity 中,接口(Interface)是一种特殊的合约结构,它定义了一组函数声明但不包含它们的实现。当你想与一个已经部署在以太坊网络上的合约交互时,接口允许你的合约知道如何正确地调用那个合约的函数,即使你没有那个合约的完整代码。
接口的工作原理是基于以太坊的智能合约和 EVM(以太坊虚拟机)如何处理函数调用。每个函数调用都是通过一种叫做 ABI 编码的方式来构造的,这种编码包括了函数的标识符(函数选择器)和它的参数。
这里是为什么可以通过接口调用的原因:
-
函数选择器:在 Solidity 中,每个函数都有一个对应的唯一函数选择器,这是一个由函数的名称和参数类型组成的签名通过 Keccak-256 哈希算法取前 4 个字节而来的。这个选择器用于在以太坊上唯一地标识函数。
-
ABI 编码:当你通过接口调用一个函数时,Solidity 编译器会根据接口中定义的函数签名来生成这个函数选择器,并且将它与函数参数一起 ABI 编码,形成调用数据。
-
动态链接:即使你的合约并不包含外部合约的完整实现代码,只要你知道外部合约的接口,你就可以构造出正确的调用数据。因此,当你的合约发送这个调用数据到外部合约的地址时,EVM 就能够根据函数选择器找到并执行正确的函数。
通过这种方式,接口充当了合约间通信的“桥梁”,允许合约开发者在 不需要知道外部合约的内部实现细节的情况下,与这些合约进行交互。
下面是一个简单的例子来展示如何使用接口:
// 定义一个接口,它声明了一个外部合约的函数
interface IExternalContract {
function someFunction(uint256 _value) external returns (bool);
}
contract MyContract {
// 使用接口与外部合约进行交互
function callExternalContract(address _externalContract, uint256 _value) public returns (bool) {
IExternalContract externalContract = IExternalContract(_externalContract);
return externalContract.someFunction(_value);
}
}
在这个例子中,MyContract合约通过IExternalContract接口与外部合约_externalContract进行交互。即使MyContract不知道someFunction函数的具体实现,它也能通过接口正确地调用该函数。
接口的参数为什么是合约地址?
在 Solidity 中,使用接口与另一个合约进行交互时,需要提供目标合约的地址,因为:
- 接口只定义函数签名:它不包含状态或者实现代码。
- 合约地址指定目标实例:每个合约实例在区块链上有一个唯一的地址。
- 通过地址创建实例:我们使用接口和地址来创建一个指向具体合约的引用,以便调用其函数。
示例代码:
interface IExternalContract {
function someFunction(uint256 _value) external returns (bool);
}
contract MyContract {
function callExternalContract(address contractAddress, uint256 _value) public returns (bool) {
IExternalContract external = IExternalContract(contractAddress);
return external.someFunction(_value);
}
}
在这段代码中,callExternalContract接收一个合约地址contractAddress,然后通过接口IExternalContract与该地址上的合约实例进行交互。
合约函数中的 this 指向
在 Solidity 中,this 关键字是一个指向合约自身的指针。更具体地说,this 表示当前合约的实例,你可以通过它来访问合约的公共和外部函数以及状态变量。
以下是一些使用 this 的场景:
-
调用合约的当前实例的函数: 如果你想要从合约内部以外部调用(external call)的方式调用合约自己的一个公共(public)或外部(external)函数,你可以使用
this关键字。pragma solidity ^0.8.0;
contract MyContract {
uint public value;
function setValue(uint _value) public {
value = _value;
}
function externalSetValue(uint _value) public {
// 使用 this 来以外部调用的方式调用 setValue 函数
this.setValue(_value);
}
}在上面的例子中,
externalSetValue函数通过this.setValue(_value)以外部调用的方式调用了setValue函数。 -
获取合约的地址: 你可以使用
this来获取当前合约实例的地址。address public contractAddress;
function setContractAddress() public {
// 将合约地址设置为当前合约的地址
contractAddress = address(this);
}在上面的例子中,
setContractAddress函数设置contractAddress为当前合约的地址。
需要注意的是,当你通过 this 来调用合约的函数时,这些调用被视为外部调用,即使是在合约内部进行的。外部调用会使用消息调用(message call),这意味着它们会产生一个新的 EVM 调用栈,可能会有额外的 gas 成本,并且对状态变量的更改将不会在调用中反映出来,除非调用成功完成。
合约函数类型相互转化
在 Solidity 中,函数可以被定义为几种不同的类型,主要包括:external, public, internal, 和 private。这些类型定义了函数的可见性和可调用性。然而,你可能想要在合约中改变一个函数的类型,或者更准确地说,你可能想要在不同的上下文中以不同方式调用函数。这涉及到函数选择器和低级调用。
函数选择器是函数签名的前四个字节,它是调用合约函数时必须提供的。在 Solidity 中,你可以通过 this.functionName 访问外部函数 的选择器,即使该函数实际上是公共(public)的。这样,你就可以以外部调用的方式调用一个公共函数。
这里有一些关于函数类型转换的说明:
-
从 Public 到 External: 你可以通过
this.functionName使用公共函数的外部接口。这会导致稍微高一些的气体费用,因为它通过外部调用的方式而不是直接跳转来执行。 -
从 External 到 Public/Internal: 一个外部函数不能直接转换为公共或内部函数。如果你想要在合约内部以内部方式调用一个外部函数,你需要写一个包装器函数,这个函数是公共或内部的,并且它内部调用外部函数。
-
从 Internal 到 Public/External: 内部函数可以通过定义一个新的公共或外部函数来"暴露"。这个新函数将简单地调用内部函数。
-
从 Private 到 Public/External/Internal: 和内部函数一样,私有函数可以通过定义一个新的公共、外部或内部函数来"暴露"。同样,这个新函数会调用私有函数。
在 Solidity 中,你不能直接改变一个已经定义的函数的类型,但你可以通过定义新的函数来间接地"转换"类型。请注意,这种转换实际上不是改变原有函数的类型,而是创建了一个新的函数,新函数按照你需要的那种方式调用原有函数。
这里有一个简单的示例:
pragma solidity ^0.8.0;
contract Example {
// Internal function
function internalFunction() internal pure returns (string memory) {
return "Called internalFunction";
}
// Public function that exposes the internal function
function callInternal() public pure returns (string memory) {
return internalFunction();
}
// External function
function externalFunction() external pure returns (string memory) {
return "Called externalFunction";
}
// Public function that exposes the external function
function callExternal() public pure returns (string memory) {
return this.externalFunction();
}
}
在这个例子中,callInternal 是一个公共函数,它调用了内部函数 internalFunction。而 callExternal 是一个公共函数,它以外部调用的方式调用了外部函数 externalFunction。
AJAX VS JSON-RPC
前后端之间的 AJAX 通信和前端与智能合约之间的通信是两种不同的交互模式,它们在底层技术、安全性、交互方式和使用的协议等方面有所不同。以下是它们之间的一些主要异同点:
相同点
-
异步性: AJAX 通信和前端与智能合约的通信通常都是异步的。这意味着在发送请求后,用户界面可以继续响应用户操作,而不需要等待响应返回。
-
基于 JavaScript: 在前端,无论是与后端服务器还是与区块链智能合约通信,通常都会使用 JavaScript 或者 JavaScript 框架来处理这些通信。
-
触发事件: 在两种通信方式中,都会有事件触发请求,比如用户点击按钮,或者满足特定条件。
-
处理响应: 两种通信方式都需要处理返回的数据,这可能涉及到更新前端的状态或界面。
不同点
-
通信协议:
- AJAX: 通常使用 HTTP/HTTPS 协议与服务器进行数据交换。
- 合约通信: 与智能合约通信通常使用 JSON-RPC 协议通过以太坊客户端与区块链交互。
-
安全性:
- AJAX: 通信安全性 依赖于 HTTPS 和服务器的安全配置。
- 合约通信: 通信安全性依赖于区块链的加密和共识机制。
-
数据存储:
- AJAX: 数据存储在服务器上,服务器可以是中心化的,也可以是分布式的,但通常由单个实体控制。
- 合约通信: 数据存储在区块链上,是分布式的,不可篡改的。
-
身份验证:
- AJAX: 身份验证通常通过用户名和密码、Token 或其他身份验证机制实现。
- 合约通信: 身份验证通过私钥签名交易实现,确保了交易的发送者拥有相应的地址权限。
-
交互方式:
- AJAX: 请求可以是简单的读取操作,也可以是写入操作,后者可能会更改服务器上的数据。
- 合约通信: 读取操作不需要进行交易,但写入操作需要创建并发送交易,可能需要支付一定的燃料费(Gas)。
-
工具和库:
- AJAX: 使用诸如 XMLHttpRequest、Fetch API、jQuery、Axios 等 JavaScript 库。
- 合约通信: 使用诸如 Web3.js、ethers.js 等专门为与区块链交互设计的库。
-
费用:
- AJAX: 通常不需要支付费用来与服务器通信。
- 合约通信: 执行写入操作(即发送交易)通常需要支付燃料费,这是因为区块链网络需要对交易进行验证和执行。
-
响应时间:
- AJAX: 响应时间相对较快,取决于网络延迟和服务器处理能力。
- 合约通信: 读取操作响应较快,但写入操作(交易)的确认时 间取决于区块链网络的拥堵程度和提供的燃料费价格。
总而言之,AJAX 通信是建立在传统的客户端-服务器模型之上,而前端与智能合约的通信则是建立在去中心化的区块链技术之上。尽管在交互方式上有一些相似之处,但它们在实现细节、安全性、成本和基础架构上有很大的不同。
json-rpc 是数据协议,http 是传输协议
是的,您的理解是正确的。JSON-RPC 和 HTTP 分别代表了数据协议和传输协议这两个不同的概念:
-
JSON-RPC 是一个数据协议,它定义了客户端和服务器之间交换数据的格式和规则。JSON-RPC 使用 JSON(JavaScript Object Notation)作为数据格式,使得数据易于读写,同时也易于被人和机器解析。JSON-RPC 定义了一种简单的协议,允许在不同的系统之间进行远程过程调用(RPC),并不限定于特定的传输协议。
-
HTTP(HyperText Transfer Protocol)是一个传输协议,它定义了客户端和服务器之间如何发送和接收数据。HTTP 是互联网上最常用的协议之一,通常用于网页浏览,但也可用于传输任何类型的数据。因为它是一个基础的网络协议,所以可以用来传输各种数据格式,包括 JSON-RPC 请求和响应。
简而言之,JSON-RPC 关注于“要发送什么数据”(即调用的方法和参数),而 HTTP 关注于“如何发送数据”(即通过请求和响应的结构)。在实际应用中,JSON-RPC 请求通常被封装在 HTTP 请求的 body 中发送给服务器,服务器处理后,再将 JSON-RPC 响应封装在 HTTP 响应的 body 中返回给客户端。这种结合使用的方式使得 JSON-RPC 可以利用 HTTP 的广泛支持和成熟的基础设施。
字符串类型
在 Solidity 中,字符串是一种特殊的数组类型,它本质上是一个字节序列。字符串被表示为string类型,但它们并不像在许多其他编程语言中那样具有许多内置的方法。这是因为 Solidity 主要关注于低级别的合约操作和气体效率,而字符串操作通常是高级别的、成本较高的。
Solidity 中的string类型是动态大小的,且是 UTF-8 编码的字节序列。但是,与 JavaScript 或 Python 等语言不同,Solidity 的string类型并没有内置的方法来直接操作这些字符串。例如,你不能直接在 Solidity 合约中调用方法来获取字符串长度、连接两个字符串或者切分字符串。
要在 Solidity 中操作字符串,你通常需要自己写函数或者使用第三方库。例如,你可能需要使用bytes类型来处理字符串数据,因为bytes是一个低级数据类型,它有一些可以操作字节序列的方法。
以下是一些在 Solidity 中处理字符串时可能会用到的bytes方法:
length:获取字节序列的长度。push:向动态字节数组的末尾添加一个新元素(仅限bytes,不适用于string)。pop:移除动态字节数组末尾的元素(仅限bytes,不适用于string)。
如果你需要进行更复杂的字符串处理,你可能需要使用汇编语言或者引入外部库。例如,OpenZeppelin 是一个流行的智能合约开发库,它提供了一些字符串操作的函数。
这里是一个使用bytes来获取字符串长度的例子:
pragma solidity ^0.8.0;
contract StringHelpers {
function getLength(string memory str) public pure returns (uint) {
return bytes(str).length;
}
}
在 Solidity 中,处理字符串通常需要更多的注意和手动操作,因为每个操作都可能涉及到非常昂贵的气体成本。设计智能合约时,通常建议尽量避免复杂的字符串操作,或者尽可能将这些操作移到链下处理。
如何避免悬空引用
在 Solidity 中,悬空引用(dangling reference)通常指的是指向一个已经被删除或者不再有效的存储位置的引用。在使用数组时,悬空引用可能发生在以下情况:
- 当引用指向一个已经被删除的动态数组元素时。
- 当引用指向一个已经被删除或者超出了数组长度的固定大小数组元素时。
要避免悬空引用,你应该确保在访问数组元素之前,该元素是存在的。在 Solidity 中,你可以通过检查数组的长度来做到这一点。如果你尝试访问一个超出数组长度的元素,Solidity 会抛出一个异常。
这里有一些避免悬空引用的方法:
- 检查数组长度:在访问数组的特定索引之前,先检查索引是否小于数组的长度。
if (index < myArray.length) {
// 安全地访问数组元素
uint value = myArray[index];
}
- 使用数组操作的返回值:对于
pop这样会改变数组长度的操作,你可以直接使用其返回值(如果有的话),而不是通过索引访问。
uint removedValue = myArray.pop(); // pop 返回被移除的元素
-
避免在数组长度变化后使用旧索引:如果你在数组长度变化后保留了一个索引,确保在再次使用它之前重新检查数组长度。
-
使用映射(Mapping)来管理元素的存在性:如果你在删除元素时需要保持引用的有效性,可以考虑使用映射来跟踪哪些元素是有效的。
-
使用高级语言特性:Solidity 0.6.0 之后的版本支持
try/catch语句,你可以用它来处理数组访问可能抛出的异常。
通过这些方法,你可以确保在访问数组时不会遇到悬空引用的问题。记住,智能合约的异常处理非常重要,因为错误的处理可能会导致合约被锁定或资金丢失。始终确保你的合约代码能够安全地处理各种边缘情况。
从 java 类的角度理解下合约
在 Java 中,一个类是一个定义对象属性和方法的蓝图。一个 Java 类可以包含字段(属性)、方法(函数)、构造函数和更多的结构来定义一个对象的状态和行为。在智能合约和 Solidity 的上下文中,合约可以被看作是类的一个类似物。智能合约中的结构体(Structs)与 Java 中的类有一些相似之处。
让我们通过比较 Java 类和 Solidity 合约来理解这些概念:
Java 类
一个 Java 类通常包含以下部分:
- 字段(Field):定义对象状态的变量。
- 方法(Method):定义对象可以执行的操作。
- 构造函数(Constructor):用于创建类的实例。
- 访问修饰符(Access Modifier):定义类、字段和方法的访问 级别,如
public、private等。
public class User {
private int id;
private String name;
private String email;
public User(int id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
// 其他方法...
}
Solidity 合约
一个 Solidity 合约通常包含以下部分:
- 状态变量(State Variable):存储合约持久状态的变量。
- 函数(Function):定义合约可以执行的操作。
- 构造函数(Constructor):用于初始化合约状态。
- 修饰符(Modifier):用于修改函数的行为。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract UserContract {
struct User {
uint id;
string name;
string email;
}
User[] public users;
function createUser(uint _id, string memory _name, string memory _email) public {
users.push(User(_id, _name, _email));
}
function getUser(uint _index) public view returns (User memory) {
require(_index < users.length, "Index out of bounds");
return users[_index];
}
// 其他函数...
}
在这个 Solidity 合约中,User结构体类似于 Java 中的类,它定义了用户的属性。合约中的函数可以看作是 Java 类中的方法。
比较
- 实例化:在 Java 中,你使用
new关键字创建类的实例。在 Solidity 中,你可以直接创建结构体实例,或者部署另一个合约的实例。 - 存储:Java 对象通常存储在堆内存中,而 Solidity 的状态变量存储在区块链上,这意味着它们是持久化的。
- 访问控制:Java 使用访问修饰符来限制访问范围。Solidity 也有类似的概念(如
public、private等),但它们的含义与区块链的上下文紧密相关。 - 构造函数:Java 和 Solidity 都有构造函数,但 Solidity 的构造函数只在合约创建时执行一次。
- 继承:Java 支持多重继承,而 Solidity 只支持单继承,但允许多个接口的实现。
在将 Java 类的概念映射到 Solidity 合约时,重要的是要记住它们的执行环境和目的是不同的。Java 类通常用于构建通用的软件应用程序,而 Solidity 合约是为了在以太坊这样的区块链上执行,有着不同的性能考虑和安全性要求。
solidity 特有的数据类型有哪些
Solidity 作为一种智能合约编程语言,拥有一些专门为区块链环境设计的特有数据类型。以下是一些 Solidity 特有的数据类型:
-
地址(Address):
address: 用于存储以太坊地址,是一个 20 字节的值,通常代表账户地址或合约地址。address payable: 类似于address,但是拥有transfer和send方法,可以接收以太币。
-
合约(Contracts):
- 合约类型允许一个合约变量代表另一个合约,使得合约间的交互成为可能。
-
枚举(Enums):
enum: 用于创建自定义类型,限制变量到几个预定义的值中。
-
映射(Mappings):
mapping: 类似于哈希表或字典,是一种键值对存储结构,其中键是不可遍历的。
-
固定大小的字节类型:
bytes1到bytes32: 表示从 1 个字节到 32 个字节的固定大小的字节数组。bytes: 动态大小的字节数组,但与bytes1到bytes32不同,它不是值类型。
-
固定点数:
fixed和ufixed: Solidity 支持固定点数,但这些类型目前还没有完全实现,不建议在生产环境中使用。
-
函数类型:
- 函数类型是指向函数的引用,它可以用来实现回调函数或将函数作为参数传递。
-
存储位置(Storage, Memory, and Calldata):
storage: 代表永久存储,是状态变量的默认存储位置。memory: 代表临时存储,用于函数执行期间的数据存储。calldata: 类似于memory,但是专门用于存储函数的输入参数,是只读的,并且比memory更加节约 gas。
这些类型是为了与以太坊虚拟机(EVM)的特性和限制相适应,同时也考虑了智能合约开发中的安全性和效率。例如,地址类型和映射类型是处理以太坊账户和状态存储的基础,而存储位置关键字则是 Solidity 中特有的内存管理机制,它们帮助开发者以成本效益的方式使用 EVM 的存储资源。
有哪些功能型的合约?
在 Solidity 和其他智能合约编程语言中,"功能型合约"通常指的是那些提供特定功能或服务的合约。这些合约可以独立使用,也可以作为更大应用的一部分。以下是一些常见的功能型合约类型:
-
代币合约:
- ERC-20 合约: 实现了 ERC-20 标准的代币合约,用于创建可交换的代币。
- ERC-721 合约: 实现了 ERC-721 标准的非同质化代币(NFT)合约,用于创建独一无二的代币。
- ERC-1155 合约: 实现了 ERC-1155 标准,可以创建可交换和非同质化代币的多代币合约。
-
众筹合约:
- 用于实现众筹或集资功能,允许用户向项目捐款或投资。
-
投票合约:
- 用于组织投票和治理决 策,允许持有代币的用户对提案进行投票。
-
贷款和借贷合约:
- 实现去中心化的贷款和借贷机制,用户可以借出或借入资产。
-
去中心化交易所(DEX)合约:
- 用于实现代币之间的去中心化交易,如自动做市商(AMM)模型。
-
预言机合约:
- 用于从链外世界获取数据,并将其提供给链上的智能合约。
-
游戏合约:
- 用于构建区块链游戏,可以包括角色、物品和其他游戏逻辑。
-
保险合约:
- 提供去中心化的保险服务,根据合约条款自动处理索赔。
-
拍卖合约:
- 用于实现去中心化的拍卖机制,如荷兰式或英式拍卖。
-
钱包合约:
- 多重签名钱包合约,要求多个签名才能执行交易,增加安全性。
-
自动化任务合约:
- 如以太坊上的定时任务(Ethereum Alarm Clock),可以在未来的某个时间点自动执行合约函数。
-
流动性池合约:
- 用于去中心化金融(DeFi)平台,允许用户提供流动性并获得交易费用作为回报。
-
分红合约:
- 自动分配利润或代币给持有特定代币的用户。
这些合约类型只是智能合约应用的一部分,实际上,随着区块链技术的发展,可能会出现更多新的合约类型和用例。智能合约的设计和实现通常需要考虑安全性、效率和合规性,因为它们直接处理价值转移和业务逻辑。
除了我之前列举的功能型合约类型,还有其他一些常见的 智能合约,它们在区块链应用中扮演特定的角色:
-
身份和认证合约:
- 用于身份验证和授权,可以与现实世界的身份信息桥接,以实现链上身份管理。
-
分布式自治组织(DAO)合约:
- 用于创建和管理去中心化组织,成员可以投票决定组织的方向和资金使用。
-
抵押和锁定合约:
- 允许用户锁定代币以参与系统治理或获得奖励,例如在质押证明(Proof of Stake, PoS)系统中。
-
逃逸舱(Escape Hatch)或紧急停止合约:
- 用于在合约出现安全问题时提供紧急停止机制,可以限制损失。
-
库合约:
- 提供可重用的代码逻辑,其他合约可以通过继承或委托调用库合约的函数。
-
工厂合约:
- 用于部署新合约实例,可以创建和管理多个合约实例。
-
桥接合约:
- 用于不同区块链之间的资产或数据传输,例如将代币从一个链桥接到另一个链。
-
隐私合约:
- 提供隐私保护功能,如零知识证明(ZKP)或混币器,以隐藏交易细节。
-
数据存储合约:
- 专门用于存储和管理链上数据,可能采用特定的数据结构来优化读写效率。
-
市场合约:
- 用于创建各种市场,如预测市场,用户可以就特定事件的结果进行投注。
-
债务和债券合约:
- 允许创建和管理债务工具,如债券或债务债权。
-
合成资产合约:
- 用于 创建跟踪现实世界资产价格的合成代币,如合成股票或货币。
-
奖励和激励合约:
- 为用户提供完成特定任务或达成目标的奖励。
-
托管和中介合约:
- 在买卖双方之间提供托管服务,确保交易的公平性。
每种合约类型都有其特定的用途和设计模式,开发者可以根据应用需求组合不同的合约来构建复杂的去中心化应用(DApps)。随着区块链技术的不断发展,新的合约类型和模式也在不断出现,推动着这个领域的创新。
不能对映射进行递归调用,也就是说,您不能列举它们的键
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.8;
struct IndexValue { uint keyIndex; uint value; }
struct KeyFlag { uint key; bool deleted; }
struct itmap {
mapping(uint => IndexValue) data;
KeyFlag[] keys;
uint size;
}
type Iterator is uint;
library IterableMapping {
function insert(itmap storage self, uint key, uint value) internal returns (bool replaced) {
uint keyIndex = self.data[key].keyIndex;
self.data[key].value = value;
if (keyIndex > 0)
return true;
else {
keyIndex = self.keys.length;
self.keys.push();
self.data[key].keyIndex = keyIndex + 1;
self.keys[keyIndex].key = key;
self.size++;
return false;
}
}
function remove(itmap storage self, uint key) internal returns (bool success) {
uint keyIndex = self.data[key].keyIndex;
if (keyIndex == 0)
return false;
delete self.data[key];
self.keys[keyIndex - 1].deleted = true;
self.size --;
}
function contains(itmap storage self, uint key) internal view returns (bool) {
return self.data[key].keyIndex > 0;
}
function iterateStart(itmap storage self) internal view returns (Iterator) {
return iteratorSkipDeleted(self, 0);
}
function iterateValid(itmap storage self, Iterator iterator) internal view returns (bool) {
return Iterator.unwrap(iterator) < self.keys.length;
}
function iterateNext(itmap storage self, Iterator iterator) internal view returns (Iterator) {
return iteratorSkipDeleted(self, Iterator.unwrap(iterator) + 1);
}
function iterateGet(itmap storage self, Iterator iterator) internal view returns (uint key, uint value) {
uint keyIndex = Iterator.unwrap(iterator);
key = self.keys[keyIndex].key;
value = self.data[key].value;
}
function iteratorSkipDeleted(itmap storage self, uint keyIndex) private view returns (Iterator) {
while (keyIndex < self.keys.length && self.keys[keyIndex].deleted)
keyIndex++;
return Iterator.wrap(keyIndex);
}
}
// 如何使用
contract User {
// 只是一个保存我们数据的结构体。
itmap data;
// 对数据类型应用库函数。
using IterableMapping for itmap;
// 插入一些数据
function insert(uint k, uint v) public returns (uint size) {
// 这将调用 IterableMapping.insert(data, k, v)
data.insert(k, v);
// 我们仍然可以访问结构中的成员,
// 但我们应该注意不要乱动他们。
return data.size;
}
// 计算所有存储数据的总和。
function sum() public view returns (uint s) {
for (
Iterator i = data.iterateStart();
data.iterateValid(i);
i = data.iterateNext(i)
) {
(, uint value) = data.iterateGet(i);
s += value;
}
}
}
这段代码是 Solidity 语言编写的,它实现了一个可迭代的映射(itmap),这是 Solidity 中的一个高级数据结构,因为 Solidity 的标准映射类型不支持迭代或获取所有键的列表。IterableMapping库通过结合映射和数组来提供这样的功能。
下面是代码的一些关键点的解释:
-
定义数据结构:
IndexValue结构体包含一个映射的键在数组中的索引和值。KeyFlag结构体包含键和一个标记,指示该键是否已被删除。itmap结构体包含一个映射,映射uint到IndexValue,一个KeyFlag数组,以及一个表示映射大小的size变量。
-
定义迭代器:
Iterator是一个封装了uint的自定义类型,用于在可迭代映射中追踪位 置。
-
IterableMapping库:insert函数用于向映射中插入一个键值对。如果键已存在,则更新值并返回true;如果键不存在,则添加键值对并返回false。remove函数用于从映射中移除一个键,并将其标记为已删除。contains函数检查一个键是否存在于映射中。iterateStart函数返回迭代器的初始位置。iterateValid函数检查迭代器当前位置是否有效。iterateNext函数移动迭代器到下一个有效位置。iterateGet函数根据迭代器的位置获取键值对。iteratorSkipDeleted函数是一个私有函数,用于跳过已删除的键。
-
使用库:
User合约定义了一个itmap类型的变量data。- 使用
using IterableMapping for itmap;语句,允许User合约调用IterableMapping库中定义的函数,就像它们是itmap类型的方法一样。 insert函数插入键值对并返回映射的新大小。sum函数计算所有存储在映射中的值的总和。
关于您提到的IterableMapping没有data的问题,这里有一些可能导致误解的地方。IterableMapping是一个库,它定义了一组函数,这些函数通过参数接收itmap类型的存储引用,然后对其进行操作。在 Solidity 中,库函数可以像操作自己的状态变量一样操作传入的参数,这就是为什么您看到self关键字在这些函数中被用来代表传入的itmap实例。
User合约中的data变量是itmap类型的实例,它实际上是使用IterableMapping库的数据存储。当您在User合约中调用data.insert(k, v)时,您实际上是在调用IterableMapping库的insert函数,并将data作为参数传入。这就是为什么您在User合约中没有直接看到data变量被定义在IterableMapping库中,因为它是作为参数传递给库函数的。
类比以太坊 的 EVM, JAVA EVM 怎么没有提到过 ABI?
Java 虚拟机(JVM)和以太坊虚拟机(EVM)虽然都是虚拟机,但它们的设计目的和运行的上下文有很大的不同。
JVM(Java 虚拟机)
JVM 是用来运行 Java 字节码的虚拟机。Java 源代码被编译成字节码(.class文件),然后可以在任何安装了 JVM 的平台上运行。JVM 负责将这些字节码转换为特定操作系统上的机器码。在这个过程中,JVM 使用的是 Java 的类和方法信息,这些信息是在 Java 源代码被编译成字节码时嵌入的。Java 的字节码已经包含了足够的信息来指导 JVM 如何执行代码,包括类型信息、方法签名等。
在 Java 的世界中,"接口"通常是指一个可以被实现的抽象类型,而不是一个数据交换的格式。Java 程序在运行时通过类加载器和反射机制来动态加载和交互,这些机制内置于 JVM 中,无需类似于 ABI 的外部定义。
EVM(以太坊虚拟机)
EVM 是专门为执行以太坊智能合约而设计的虚拟机。智能合约是用 Solidity(或其他语言)编写的,然后编译成 EVM 能够理解的字节码。不同于 JVM,EVM 的字节码需要与外部世界(如钱包、其他合约、用户界面)进行交互,而这些交互必须通过交易来完成。
因为交易只能包含字节数据,所以需要一种标准化的方式来编码和解码这些数据,以便 EVM 能够正确地执行智能合约的函数调用。这就是 ABI 的角色。ABI 定义了如何将函数调用和参数转换为字节串,以及如何将交易的结果(也是字节串)转换回可读的格式。
总结
所以,JVM 没有类似于 ABI 的概念,因为它是为了运行 Java 字节码而设计的,而这些字节码已经包含了所有必要的类型和结构信息。另一方面,EVM 需要 ABI 来定义如何在智能合约和调用它们的外部世界之间传递信息。这是因为 EVM 的上下文是去中心化的区块链环境,其中任何人都可以与智能合约交互,而且这些交互必须通过标准化的数据格式来完成。