Solidity入门

Posted 南墙一棵树

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Solidity入门相关的知识,希望对你有一定的参考价值。

1.简单的智能合约

//关键字  pragmas(编译指令)是告知编译器如何处理源代码的指令的, 代码所适用的Solidity版本为>=0.4.16 及 <0.9.0 。这是为了确保合约不会在新的编译器版本中突然行为异常。
pragma solidity >=0.4.16 <0.9.0; 


//创建合约 contract

contract SimpleStorage 

//定义一个无符号整型256位
    uint storedData;

//创建函数,设置一个storedata
    function set(uint x) public 
        storedData = x;
    
//创建函数,返回storedate

    function get() public view returns (uint) 
        return storedData;
    

 2.货币合约

// 编译指令 solidity 版本>0.7.0 <0.9.0
pragma solidity  >=0.7.0 <0.9.0;
//创建合约 合约名:coin
contract Coin 
    // 关键字“public”让这些变量可以从外部读取
    address public minter;
    //mapping 映射 可视为指针 从address 映射到 uint
    mapping (address => uint) public balances;

    // 轻客户端可以通过事件针对变化作出高效的反应
    event Sent(address from, address to, uint amount);

    // 这是构造函数,只有当合约创建时运行
    constructor() 
        minter = msg.sender;
    

    function mint(address receiver, uint amount) public 
        require(msg.sender == minter);
        require(amount < 1e60);
        balances[receiver] += amount;
    

    function send(address receiver, uint amount) public 
        require(amount <= balances[msg.sender], "Insufficient balance.");
        balances[msg.sender] -= amount;
        balances[receiver] += amount;
        emit Sent(msg.sender, receiver, amount);
    

address 类型是160位的值 ,不允许任何算数操作。这种类型适合存储合约地址或外部人员的密钥对。

关键字public,允许你在这个合约之外访问这个状态变量的当前值。如果没有这个关键字,编译会出错。

mapping映射是创建一个公共状态变量,mapping(address => uint) ...是将address映射为无符号整型变量, Mappings 可以看作是一个哈希表,它会执行虚拟初始化,以使所有可能存在的键都映射到一个字节表示为全零的值。 但是,这种类比并不太恰当,因为它既不能获得映射的所有键的列表,也不能获得所有值的列表。

event Sent(address from,address to,uint amount); 这行声明了一个所谓的“事件(event)”,它会在 send 函数的最后一行被发出。用户界面(当然也包括服务器应用程序)可以监听区块链上正在发送的事件,而不会花费太多成本。一旦它被发出,监听该事件的listener都将收到通知。而所有的事件都包含了 from , to 和 amount 三个参数,可方便追踪交易。 

特殊函数constructor是仅在创建合约期间运行的构造函数,不能在事后调用。 它永久存储创建合约的人的地址: msg (以及 tx 和 block ) 是一个特殊的全局变量,其中包含一些允许访问区块链的属性。 msg.sender 始终是当前(外部)函数调用的来源地址。

require函数用于确认条件有效性,例如输入变量,或合约状态变量是否满足条件,或验证外部合约调用返回的值 

require有两个参数:

        第一个参数为条件判断表达式,必选

        第二个参数为要返回的异常消息提醒,可选

如果判断为真,则继续执行require下面的语句,如果为假,则执行第二个参数并且不执行下面的语句。

最后,真正被用户或其他合约所调用的,以完成本合约功能的方法是 mint 和 send

如果 mint 被合约创建者外的其他人调用则什么也不会发生。 另一方面, send 函数可被任何人用于向他人发送币 (当然,前提是发送者拥有这些币)。记住,如果你使用合约发送币给一个地址,当你在区块链浏览器上查看该地址时是看不到任何相关信息的。因为,实际上你发送币和更改余额的信息仅仅存储在特定合约的数据存储器中。通过使用事件,你可以非常简单地为你的新币创建一个“区块链浏览器”来追踪交易和余额。

3.合约主要架构:

  • 状态变量

状态变量是永久地存储在合约存储中的值。

pragma solidity >=0.4.0 <0.9.0;

contract TinyStorage 
    uint storedXlbData; // 状态变量
    // ...
  • 函数

函数是代码的可执行单元。函数通常在合约内部定义,但也可以在合约外定义。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >0.7.0 <0.9.0;

contract TinyAuction 
    function Mybid() public payable  // 定义函数
        // ...
    


// Helper function defined outside of a contract
function helper(uint x) pure returns (uint) 
    return x * 2;


//函数调用 可发生在合约内部或外部,且函数对其他合约有不同程度的可见性

//函数,可以接受 (参数和返回值)。
  • 函数修改器

函数 修改器可以用来以声明的方式修改函数语义

pragma solidity >=0.4.22 <0.9.0;

contract MyPurchase 
    address public seller;

    modifier onlySeller()  // 修改器
        require(
            msg.sender == seller,
            "Only seller can call this."
        );
        _;
    

    function abort() public onlySeller  // 修改器用法
        // ...
    
  • 事件Event

事件是能方便地调用以太坊虚拟机日志功能的接口。

pragma solidity >=0.4.21 <0.9.0;
contract TinyAuction 
    event HighestBidIncreased(address bidder, uint amount); // 事件

    function bid() public payable 
        // ...
        emit HighestBidIncreased(msg.sender, msg.value); // 触发事件
    
  • 结构体

结构体是可以将几个变量分组的自定义类型.

pragma solidity >=0.4.0 <0.9.0;

contract TinyBallot 
    struct Voter  // 结构体
        uint weight;
        bool voted;
        address delegate;
        uint vote;
    
  • 枚举类型

枚举可用来创建由一定数量的“常量值”构成的自定义类型

pragma solidity >=0.4.0 <0.9.0;

contract Upchain 
    enum State  Created, Locked, InValid  // 枚举

-------------------------------------------------------------------------------------------------------------------------------- 

4.源文件

  • 合约定义
  • 导入源文件指令
  • 版本标识指令
  • 结构体
  • 枚举
  • 函数
pragma solidity ^0.7.2;

pragma如前文所讲,标识指令,用来启动某些编译器检查。Pragma 是 pragmatic information 的简称

而^0.7.2是版本标识。而为了避免未来被可能引入不兼容更新的编译器编译,所以^0.7.2就不允许低于0.7.2版本的编译器进行编译,同时也不允许高于0.8.0的版本的编译器进行编译。(^标识要低于0.8.0)

导入源文件

solidity的语法和javascript比较相似

import * as symbolName from "filename";
//将从 “filename” 中导入所有的全局符号到当前全局作用域中

import * as symbolName from "filename";
//其实也等价于
import "filename" as symbolName;
//创建了新的 symbolName 全局符号,他的成员都来自与导入的 "filename" 文件中的全局符号

import symbol1 as alias, symbol2 from "filename";
//如果存在命名冲突,则可以在导入时重命名符号。例如代码创建了新的全局符号 alias 和 symbol2 ,引用的 symbol1 和 symbol2 来自 “filename” 。

路径

上文中的 filename 总是会按路径来处理,以 / 作为目录分割符、以 . 标示当前目录、以 .. 表示父目录。 当 . 或 .. 后面跟随的字符是 / 时,它们才能被当做当前目录或父目录。 只有路径以当前目录 . 或父目录 .. 开头时,才能被视为相对路径

import "./filename" as symbolName;
//同目录下的文件filename

import "filename" as symbolName;
//可能filename是不同目录下的

//实际过程中,编译器还可以指定路径前缀重映射。
import "github.com/ethereum/dapp-bin/library/iterable_mapping.sol" as it_mapping;
//运行编译器
solc github.com/ethereum/dapp-bin/=/usr/local/dapp-bin/ source.sol
//如果有多个重映射指向一个有效文件,那么具有最长公共前缀的重映射会被选用

 注释

// 这是一个单行注释。

/*
这是一个
多行注释。
*/

///或者是/**....**/书写,是要放到函数申明或者是语句上使用,也可文档化函数

//和js一样

--------------------------------------------------------------------------------------

 

5.补充知识:

账户:以太坊有两列账户:

  • 外部账户:由公钥-私钥对(也就是人)控制
  • 合约账户:由和账户一起存储的代码控制.

外部账户的地址:是由公钥决定的

合约账户的地址:是在创建该合约时确定的(这个地址通过合约创建者的地址和从该地址发出过的交易数量计算得到的,也就是所谓的“nonce”)

这两类账户都有一个键值对形式的持久化存储。其中key和value的长度都是256位。我们称之为 存储

并且每个账户都会有一个以太币余额(balance)单位是wei  ,1 eth=10^18wei,余额会因为发送包含以太币的交易而改变。

交易

而所谓交易,就是从一个帐户发送到另一个帐户的消息。它能包含一个二进制数据(合约负载)和以太币。

  1. 如果目标账户含有代码,此代码会被执行,并以 payload 作为入参。
  2. 如果目标账户是零账户(账户地址为 0 ),此交易将创建一个 新合约 。 如前文所述,合约的地址不是零地址,而是通过合约创建者的地址和从该地址发出过的交易数量计算得到的(所谓的“nonce”)。 这个用来创建合约的交易的 payload 会被转换为 EVM 字节码并执行。执行的输出将作为合约代码被永久存储。这意味着,为创建一个合约,你不需要发送实际的合约代码,而是发送能够产生合约代码的代码。

Gas

一经创建,每笔交易都收取一定数量的 gas(就是手续费) ,目的是限制执行交易所需要的工作量和为交易支付手续费。EVM 执行交易时,gas 将按特定规则逐渐耗尽。

gas price 是交易发送者设置的一个值,发送者账户需要预付的手续费= gas_price * gas 。如果交易执行后还有剩余, gas 会原路返还。

无论执行到什么位置,一旦 gas 被耗尽(比如降为负值),将会触发一个 out-of-gas 异常。当前调用帧(call frame)所做的所有状态修改都将被回滚。

存储,内存和栈

每个账户有一块持久化内存区称为 存储 。 存储是将256位字映射到256位字的键值存储区。 在合约中枚举存储是不可能的,且读存储的相对开销很高,修改存储的开销甚至更高。合约只能读写存储区内属于自己的部分。

第二个内存区称为 内存 ,合约会试图为每一次消息调用获取一块被重新擦拭干净的内存实例。 内存是线性的,可按字节级寻址,但读的长度被限制为256位,而写的长度可以是8位或256位。当访问(无论是读还是写)之前从未访问过的内存字(word)时(无论是偏移到该字内的任何位置),内存将按字进行扩展(每个字是256位)。扩容也将消耗一定的gas。 随着内存使用量的增长,其费用也会增高(以平方级别)。

EVM 不是基于寄存器的,而是基于栈的,因此所有的计算都在一个被称为 栈(stack) 的区域执行。 栈最大有1024个元素,每个元素长度是一个字(256位)。对栈的访问只限于其顶端,限制方式为:允许拷贝最顶端的16个元素中的一个到栈顶,或者是交换栈顶元素和下面16个元素中的一个。所有其他操作都只能取最顶的两个(或一个,或更多,取决于具体的操作)元素,运算后,把结果压入栈顶。当然可以把栈上的元素放到存储或内存中。但是无法只访问栈上指定深度的那个元素,除非先从栈顶移除其他元素。

指令集

EVM的指令集量应尽量少,以最大限度地避免可能导致共识问题的错误实现。所有的指令都是针对”256位的字(word)”这个基本的数据类型来进行操作。具备常用的算术、位、逻辑和比较操作。也可以做到有条件和无条件跳转。此外,合约可以访问当前区块的相关属性,比如它的编号和时间戳

消息调用

合约可以通过消息调用的方式来调用其它合约或者发送以太币到非合约账户。消息调用和交易非常类似,它们都有一个源、目标、数据、以太币、gas和返回数据。事实上每个交易都由一个顶层消息调用组成,这个消息调用又可创建更多的消息调用。

合约可以决定在其内部的消息调用中,对于剩余的 gas ,应发送和保留多少。如果在内部消息调用时发生了out-of-gas异常(或其他任何异常),这将由一个被压入栈顶的错误值所指明。此时,只有与该内部消息调用一起发送的gas会被消耗掉。并且,Solidity中,发起调用的合约默认会触发一个手工的异常,以便异常可以从调用栈里“冒泡出来”。 如前文所述,被调用的合约(可以和调用者是同一个合约)会获得一块刚刚清空过的内存,并可以访问调用的payload——由被称为 calldata 的独立区域所提供的数据。调用执行结束后,返回数据将被存放在调用方预先分配好的一块内存中。 调用深度被 限制 为 1024 ,因此对于更加复杂的操作,我们应使用循环而不是递归。

日志

有一种特殊的可索引的数据结构,其存储的数据可以一路映射直到区块层级。这个特性被称为 日志(logs) ,Solidity用它来实现 事件(events) 。合约创建之后就无法访问日志数据,但是这些数据可以从区块链外高效的访问。因为部分日志数据被存储在布隆过滤器中,我们可以高效并且加密安全地搜索日志,所以那些没有下载整个区块链的网络节点(轻客户端)也可以找到这些日志。

合约创建

合约甚至可以通过一个特殊的指令来创建其他合约(不是简单的调用零地址)。创建合约的调用 create calls 和普通消息调用的唯一区别在于,负载会被执行,执行的结果被存储为合约代码,调用者/创建者在栈上得到新合约的地址。

失效和自毁

合约代码从区块链上移除的唯一方式是合约在合约地址上的执行自毁操作 selfdestruct 。合约账户上剩余的以太币会发送给指定的目标,然后其存储和代码从状态中被移除。移除一个合约听上去不错,但其实有潜在的危险,如果有人发送以太币到移除的合约,这些以太币将永远丢失。

注意:尽管一个合约的代码中没有显式地调用 selfdestruct ,它仍然有可能通过delegatecall或者 callcode 执行自毁操作。

如果要使合同失效,则应通过更改内部状态来禁用合约,这样可以在使用函数无法执行从而进行 revert,从而达到返还以太币的目的。

以上是关于Solidity入门的主要内容,如果未能解决你的问题,请参考以下文章

区块链舰长:以太坊矿工将暂时离场!数字货币将大幅度回血?

区块链-如何在以太坊上制作自己的数字货币

区块链数字货币——以太坊黑色情人节事件

区块链数字货币——以太坊黑色情人节事件

[币严区块链]数字货币交易所之以太坊(ETH)钱包对接 以太坊Geth命令大全

solidity 0.5.7简明教程