智能合约编程语言

TRON支持开发者友好的智能合约开发语言Solidity,它具有如下特性:

  • 面向对象的高级语言
  • 静态类型
  • 支持: 继承、库、复杂的用户自定义类型。

智能合约的组成

智能合约由数据和函数组成

数据

任何智能合约数据都必须存放到一个地方:要么存储在storage,要不存储在memory。在智能合约中修改数据存储位置是非常昂贵的,因此在编写合约代码时,您需要谨慎的考虑将数据存储在哪里。

Storage

变量值需要永久存储在合约中的变量称为状态变量,由状态变量表示的数据被放入storage,并永久存储在区块链中。状态变量在定义时,需要声明变量的类型,以便在合约编译时,能够知道它需要占用区块链上的多少存储空间。

contract SimpleStorage {
    uint storedData; // State variable
    // ...
}

变量的类型包括:地址类型、布尔型、整型、定点数 、固定大小的字节数组 、动态大小字节数组 、有理数 、字符串、十六进制字符 、枚举。

地址类型:address。为了兼容以太坊,Solidity中的address类型的数据为对TRON网络账户Hex格式地址进行以下操作得到的地址:

  1. 去掉Hex格式地址的前缀41
  2. 对上步得到的结果进行混合大小写校验
    混合大小写校验按照一定的逻辑,将地址中的部分字母大写,与剩余的小写字母来形成校验和,让地址拥有自校验的能力,在地址中平均生成 15 个检查点,这将使对错误的输入未能被校验出来的失误率降到 0.0247% 以下,减少了转账出错的可能。

例如这个TRON网络账户TA9h822trLafTtsGXQc4g4ehPvyNzkQNsS,Hex格式地址为:4101fba20cb405734c6b2e704b9ed67c0b5ea74d9e ,它在solidity中的表示为:

address newAddress = 0x01fbA20CB405734C6B2e704B9eD67C0b5ea74D9E

Memory

仅在合约函数执行期间存储的变量称为内存变量。由于它们不是永久存储在区块链上,因此使用它们的成本要低得多。

环境变量

除了在合约中定义的变量之外,还有一些特殊的全局变量。它们主要用于提供关于区块链或当前交易相关的信息。例如:

环境变量状态变量类型描述
block.timestampuint256当前区块以秒计的时间戳
block.numberuint当前区块号
block.coinbaseaddress产当前区块的超级节点地址
msg.senderaddress消息发送方,即智能合约调用者
msg.valueuint随消息发送的 sun 的数量
msg.databytes完整的 call data
msg.sigbytes4call data 的前 4 字节(也就是函数标识符)
nowuint目前区块时间戳(block.timestamp)

函数

合约函数从函数调用的角度,可以分为内部函数和外部函数:

  • internal - 不创建TVM调用
    • 内部函数和状态变量只能在内部访问 ( 即只能被当前合约或当前合约派生的合约访问 )
  • external - 会创建一个TVM调用
    • 外部函数是合约接口的一部分,不仅可以在合约内部被访问,它们也可以被外部账户或其他合约调用。外部函数 f() 在内部被调用时,需要以this.f()的方式,而不能是f()。

函数也可分为公共函数或私有函数:

  • public - 公共函数可以从合约内部调用,也可以通过消息从外部调用
  • private - 私有函数只能在定义它的合约内部调用。

下面是一个更新合约状态变量的函数,它的函数名update_name,参数value为string类型;函数被声明为public,任何人都可以访问;函数没有声明为view,所以此函数可以更改合约状态:

function update_name(string value) public {
    dapp_name = value;
}

View修饰的函数

view修饰的函数不会更改合同数据的状态,比如查询操作。下面是一个查询账户余额的函数示例:

function balanceOf(address _owner) public view returns (uint256 _balance) {
    return ownerPizzaCount[_owner];
}

修改合约状态包括以下几种情况:

  1. 给状态变量赋值
  2. 发出事件
  3. 创建其他合约
  4. 使用selfdestruct
  5. 通过合约调用转账trx
  6. 调用任何未标记viewpure的函数
  7. 使用低级别调用low-level calls
  8. 使用包含某些操作码的内联汇编

构造函数

构造函数只在合约部署时执行一次,与其它面向对象编程语言中的构造函数一样,构造函数通常是将状态变量初始化为指定值。

// Initializes the contract's data, setting the `owner`
// to the address of the contract creator.
constructor() public {
    // All smart contracts rely on external transactions to trigger its functions.
    // `msg` is a global variable that includes relevant data on the given transaction,
    // such as the address of the sender and the trx value included in the transaction.
    owner = msg.sender;
}

内置函数

除了在合约中定义的变量和函数之外,还有一些特殊的内置函数,比如: address.send(),它允许合约给其他账户转账trx。

编写合约函数

在编写合约函数时,需要考虑以下几个方面:

  • 参数及类型
  • 声明 public 或者 private
  • 声明 pure 或者 view 或者 payable
  • 返回值类型

如下示例是一个最简单的完整的合约,构造函数为变量dapp_name提供了初始值。

pragma solidity >=0.4.0 <=0.6.0;

contract ExampleDapp {
    string dapp_name; // state variable

    // Called when the contract is deployed and initializes the value
    constructor() public {
        dapp_name = "My Example dapp";
    }

    // Get Function
    function read_name() public view returns(string) {
        return dapp_name;
    }

    // Set Function
    function update_name(string value) public {
        dapp_name = value;
    }
}

事件和日志

事件允许我们可以轻松的查询到在合约交易执行时发生的“事情”。日志用于将数据“写”到智能合约之外的数据结构中,日志信息不能被智能合约访问,但能提供关于交易和区块中发生的信息。当合约交易成功执行时,智能合约可以发出事件并将日志写入区块链。

智能合约库

许多开源的智能合约库,可以为您的项目提供可重用的构建块,因此在您的项目中,不需要从头开始编写每一个智能合约。

智能合约库中包含的内容

智能合约库中通常包含两种构建模块:可以添加到合约中的可重用功能,以及不同标准的实现方式。

功能

在编写智能合约时,很可能会经常编写功能类似的代码,比如在合约中分配一个管理员地址来执行某些保护操作。智能合约库通常以库或者继承的方式来实现这些功能的重用。

例如,下面是来自OpenZeppelin智能合约库的Ownable合约的简化版本,它指定一个地址作为合约的所有者,并提供一个修饰符作为限制方法,使该合约只有所有者才能访问。

contract Ownable {
    address public owner;

    constructor() internal {
        owner = msg.sender;
    }

    modifier onlyOwner() {
        require(owner == msg.sender, "Ownable: caller is not the owner");
        _;
    }
}

要在您的合约中使用这样的构建块,首先要导入该合约库,然后在您自己的合约中扩展它,这样就直接使用了Ownable合约提供的修饰符保护了自己的函数。

import ".../Ownable.sol"; // Path to the imported library

contract MyContract is Ownable {
    // The following function can only be called by the owner
    function secured() onlyOwner public {
        msg.sender.transfer(1 ether);
    }
}

另一个常用的例子是SafeMath,该智能合约库提供了带有溢出检查的算术函数。使用该库函数可以有效防止可能会带来灾难性的后果的算数溢出。

标准

TRON社区以trc的形式定义了几个标准,如TRC10、TRC20、TRC721等等。当在合约中包含TRC标准时,选择一个现有的标准总比尝试创建一个自己的更方便。许多智能合约库都包含了trc的标准。

如何在项目中包含智能合约库

请根据智能合约库的文档在项目中引用库。有一些solidity合约库是用npm打包的,所以你可以用npm来安装它们,但无论使用哪种方法,在包含库时,都要注意语言版本。例如,如果你用solidity0.5编写合同,你就不能使用solidity0.6的库。

何时使用智能合约库

在项目中使用智能合约库有许多好处。首先,它为您提供了现成的构建模块,使您不用自己编写代码,从而节省了开发时间。然后,安全性也是一大优势,由于许多项目都依赖于开源智能合约库,因此智能合约库也会经常被严格审查。这也就是为什么在应用程序代码中发现错误比在可重用的合约库中更为常见。

然而,使用智能合约库存在将不熟悉的代码包含到项目中的也存在风险。如果没有很好地理解合约的作用,直接导入一个合约并将其包含到项目中,可能会由于意外的行为导致系统出现问题。所以,在将其作为项目的一部分之前,请一定要阅读您要导入的代码及其文档。

最后,当决定是否包含一个库时,要考虑它的整体使用情况。一个被广泛采用的智能合约库,拥有更大的社区和更多的关注,也就相对更安全。