Solidity 使用及注意事项

1、规范

(1)命名

习惯上函数中的参数变量是以_(下划线)开头,以区别全局变量;私有函数名字也用_(下划线)开头。

pragma solidity ^0.6.0;

contract Student {
    string name;
    //私有函数名_create 与参数变量_name,都以_开头
    function _create(string _name) private {
        name = _name;
    }
}

(2)注释

solidity使用的一个注释标准被称作natspec的格式:

使用三个反斜杠 /// 进行注释

/// @title 一个简单的基础运算合约
/// @author Tracy
/// @notice 目前这个合约只添加一个加法
contract Math {
    /// @notice 两个数相加
    /// @param x 第一个 uint
    /// @param y 第二个 uint
    /// @return z (x + y) 的结果
    /// @dev 目前这个方法不检查溢出
    function add(uint x, uint y) external pure returns (uint z) {
        // 这只是个普通的注释,不会被 natspec 解释
        z = x + y;
    }
}

我们在查看ERC20或ERC721接囗规范时,都能看到这样的natspec格式,以下简单解释下标签作用: 

@title         标题

@author 作者

@notice     须知,解释这个方法或者合约是做什么的

@dev         开发者,向开发者解释更多细节

@param    参数,描述方法需要传入什么参数

@return     返回值 ,描述方法返回什么值

以上标签都是可选的,并不需要都用,但最少用一个@dev注释来解释每个方法是做什么的。

使用双星开头的块 /** ... */  进行注释

/** @title 一个简单的基础运算合约
    @author Tracy
    @notice 目前这个合约只添加一个加法
*/
contract Math {
    /** @notice 两个数相加
        @param x 第一个 uint
        @param y 第二个 uint
        @return z (x + y) 的结果
        @dev 目前这个方法不检查溢出
    */
    function add(uint x, uint y) external pure returns (uint z) {
        // 这只是个普通的注释,不会被 natspec 解释
        z = x + y;
    }
}

2、变量、变量类型、变量修饰符

(1)状态变量

状态变量会被永久地保存在合约中,也就是它们会被写入以太坊区块链中。

(2)动态数组

动态数组可以不断添加元素,在合约中创建动态数组保存数据是非常有意义的;在动态数组添加元素array.push(),新元素加在数组的尾部,所以元素在数组中的顺序就是添加的顺序。array.push()返回数组的长度,类型是uint。

若动态数组是memory时,则不能使用push添加元素,可以先定义一个数组长度,再添加元素,举个列子:

/// @dev 查看所有男生/女生
function list_sex(bool _sex) external override view returns (StudentInfo[] memory) {
    StudentInfo[] memory retList = new StudentInfo[](getSexCount(_sex));
    StudentInfo memory studentTmp;
    uint256 index = 0;

    for (uint256 i = 0; i < studentList.length; i++) {
        if (studentList[i].sex == _sex) {
            studentTmp = studentList[i];
            retList[index] = studentTmp;
            index++;
        }
    }

    return retList;
}

/// @dev 查看所有男生/女生数量
function getSexCount(bool _sex) internal view returns (uint256) {
    uint256 count = 0;
    for (uint256 i = 0; i < studentList.length; i++) {
        if (studentList[i].sex == _sex) {
            count++;
        }
    }

    return count;
}

若上面代码中直接使用 retList.push(studentTmp),则会报错:TypeError: Member "push" is not available in struct StudentInfo memory[] memory outside of storage.

(3)数组初始化

  // 初始化一个长度为3的内存数组
  uint[] memory values = new uint[](3);

(4)字符串

solidity不支持原生的字符串比较,需要通过比较两个字符的keccak256哈希值进行判断

    //比较2个字符类型是否相等
    function isEqual(string memory a, string memory b) internal pure returns(bool) {
        bytes32 hashA = keccak256(abi.encode(a));
        bytes32 hashB = keccak256(abi.encode(b));

        return ( hashA == hashB );
    }

(5)storage与memory区别

storage变量永久存储在区块链中,memory变量则是临时的,当外部函数对某合约调用完成时,memory变量即被移除。状态变量(在函数之外声明的变量)思想文化 为storage,并永久写入区块链;而在函数内部声明的变量是memory,函数调用结束后即消失。

3、函数相关

(1)函数修饰符 public

solidity定义的函数的属性默认为public,即表示任何合约都可以调用这个函数。

(2)函数修饰符 internal与private区别

internal与private类似,区别在于,如果某个合约继承父合约,这个合约可以访问父合约中的internal函数,但不能访问private函数。

(3)函数修饰符 external与public区别

external与public类似,区别在于,external只能在合约之外调用,不能被合约内其他函数调用,public即可被合约外调用,也可被合约内的函数调用。

(4)函数返回多个值

函数返回多个值时,如果只想要其中一个返回变量, 可以使用,(逗号分隔)对其他字段留空;注意返回值使用()括号括起来:

function multipleReturns() internal returns(uint a, uint b, uint c) {
  return (1, 2, 3);
}

function getLastReturnValue() external {
  uint c;
  // 对其他字段留空:
  (,,c) = multipleReturns();
}

(5)检查 public和external函数

仔细 检查所有声明public和external函数,一个个排除用户滥用它们的可能,谨防安全漏洞,如果这些函数没有类似onlyOwner(权限控制)这样的函数修饰符,用户可能利用各种可能的参数去调用这些函数。onlyOwner可参见 Access Control - OpenZeppelin Docs

4、gas优化相关

(1)结构体封装会省gas

在solidity中,uintx的大小无论是多少(如uint8、uint16、uint32等),都为它保留256位的存储空间,因此不会节省任何gas。但把一些uint绑定到struct中,solidity会这些uint打包在一起,从而占用较少的存储空间。

struct A {
  uint a;
  uint b;
  uint c;
}

struct B{
  uint32 a;
  uint32 b;
  uint c;
}

// 因为使用了结构打包,`B` 比 `A` 占用的空间更少
A a = A(10, 20, 30);
B b = B(10, 20, 30); 

当uint定义在一个struct中时,尽量使用最小的整数子类型(如uint8、uint16、uint32等)以节约空间; 并且把同样类型的变量放在一起(在struct中将把变量按照类型依次放置),这样solidity可以将存储空间最小化。如下结构体C比结构体D占用空间更少,所以gas消耗也会更少。

struct C {
  uint32 a;
  uint32 b;
  uint c;
}

struct D{
  uint32 a;
  uint b;
  uint32 c;
}

(2)view修饰符在外部调用时(external view)不花费gas 

view函数不会改变区块链上的任何数据,只会读取数据,运行view函数只需要查询自己本地以太坊节点,它不需要在区块链上创建一个事务(事务需要运行在每个节点上,因此花费gas),因此不需要gas。

注意:如果一个view函数在另一个函数的内部被调用,而调用函数与view函数不属于同一个合约,也会产生调用成本。这是因为如果主调函数在以太坊创建了一个事务,它仍然需要逐个节点去验证。所以标记为view的函数只有在外部调用时才是免费的。

 (3)storage动态数组元素不要删除(元素只添加或修改,不删除)

针对storage的动态数组变量,如果删除数组元素,删除元素后面的元素都要前移,即执行写操作,由于写入存储是solidity中最费gas的操作之一,所以删除元素非常消耗gas。更糟糕的是,每次删除元素不一定是同一位置,所以调用函数时每次的gas都不同。当然我们也可以把数组中最后一个元素移到删除元素的位置,并将数组长度减一,但这样每做一笔交易,都会改变数组元素的顺序。

5、时间

(1)now(<0.7.0)block.timestamp(0.7.0+)

now 返回当前的unix时间戳(即自1970-01-01 00:00:00以来经过的秒数),返回类型默认是uint256,unix时间一般会用一个32位的整数uint32进行存储,但会导致“2038年”的问题,即到2038年32位的整型时间戳不够用,会产出溢出。

now在0.7.0版本被移除了,0.7.0及以后版本使用block.timestamp。

(2)时间单位

solidity时间单位包含秒(seconds)、分钟(minutes)、小时(hours)、天(days)、周(weeks)、年(years)等,都是复数加s(1 days),它们都会转换成对应的秒数放入uint中。所以1分钟是60秒,1小时是3600秒,1天是86400秒,依次类推。

1 == 1 seconds

1 minutes = 60 seconds

1 hours = 60 minutes

1 days = 24 hours

1 weeks = 7 days

years在0.5.0版本移除了,因为闰年的原因。

6、事件

事件是合约和区块链通讯的一种机制,前端 应用“监听”某些事件,并做出反应。

7、合约调用合约

一个合约调B用另一个合约A的函数a,需要先定义一个接囗,接囗中声明合约A中的函数(接囗中没有状态变量或其它函数,且函声明的函数没有使用{},只用;(分号)结束函数声明),合约B声明合约A中要使用的函数a,可参见 solidity 合约通过接囗调用另一合约方法_ling1998的博客-CSDN博客 

8、合约与库的区别

 库(library)使用using关键字,可以把库的所有方法添加给一个数据类型,如下面是一个安全运算库,里面有一个加法方法:

library SafeMath {
  //两数相加
  function add(uint256 a, uint256 b) internal pure returns (uint256) {
    uint256 c = a + b;
    assert(c >= a);
    return c;
  }
}

在合约中使用using来引用库的所有方法添加给数据类型uint,库在调用方法的时候uint自动被作为第一个参数传递:

contract Test {
    //引入库
    using SafeMath for uint;

    function add(uint a, uint b) external pure returns (uint) {
        //使用库中加法
        return a.add(b);
    }
}

在上面引用库作用于类型uint(using SafeMath for uint),如果在uint8上使用库中的add方法,并不能防止溢出,因为会把uint8的变量都转化为uint。如果要作用于uint8,需要再写一个库支持uint8安全运算,如下所示(方法中除uint256与uint8类型不一样,其它都一样):

library SafeMath8 {
  //两数相加
  function add(uint8 a, uint8 b) internal pure returns (uint8) {
    uint8 c = a + b;
    assert(c >= a);
    return c;
  }
}

9、判断是否是合约地址(2种方法)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

contract isContract{
    function isContract1(address _addr) external view returns (bool) {
        //若为外部账户false,若为合约账户true
        return _addr.code.length > 0;
    }

    function isContract2(address _addr) external view returns (bool) {
        uint256 _size;
 
        //若为外部账户_size = 0,若为合约账户 _size > 0
        assembly { _size := extcodesize(_addr) }
        return _size > 0;
    }
}

10、导入import

通过url导入合约

注意:

  • 版本号与导入合约版本一致
  • 可为导入合约起别名,调用导入的合约时加上别名,别名不能为关键字(如 alias)
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.6;

import "https://github.com/tracyzhang1998/smartcontract/blob/main/Security/Reentrancy/reentrancy.sol" as alias_new;

contract TestImport {

    function getAddr() external returns (address) {
        //调用导入合约时,加上别名
        alias_new.EtherStore addr = new alias_new.EtherStore();
        return address(addr);
    }

}

11、send与transfer区别

send是transfer的低级版本,如果执行失败,send会返回false,当前合约不会因为异常而终止。transfer如果执行失败,则会因异常而终止。

send 与 transfer 发送 2300gas 矿工费,且不可调节。

测试示例源码

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;


contract TestSendAndTransfer {
    /// @dev 状态变量 - 用于测试send与trnasfer
    string message;

    /// @dev 构造函数,初始化状态变量message
    constructor() {
        message = "hello";
    }

    /// @dev 存款,若部署时忘记存款,可直接调用此函数向合约账户存款
    function deposit() external payable {
    }

    /// @dev 发送以太 - send
    function sendEther(address _addr) external returns (bool) {
        bool result = payable(_addr).send(2);
        if (result) {
            message = "send success";
        } else {
            message = "send fail";
        }
        return result;
    }

    /// @dev 发送以太 - transfer
    function transferEther(address _addr) external {
        payable(_addr).transfer(2);
        message = "transfer success";
    }

    /// @dev 查看合约账户余额
    function getContractBalance() external view returns (uint256) {
        return address(this).balance;
    }

    /// @dev 查看message
    function getMsg() external view returns (string memory) {
        return message;
    }

    /// @dev 设置message
    function setMsg(string memory _message) external {
        message = _message;
    }
}

测试send失败的情况

步骤:

  1. 当合约账户没有余额时,向另一个账户地址发送以太(或是当前合约账户有余额,向一个没有fallback函数的合约账户发送以太),查看交易 数据,返回false
  2. 查看状态变量message,已由初始值”hello“变为”send fail“

测试send成功的情况

步骤:

  1. 向合约账户充值,查看合约账户余额,确保大于2Wei
  2. 调用sendEther函数,向用户账户转账
  3. 查看合约账户余额,已经少了2Wei
  4. 查看message已变为“send success”

测试transfer失败的情况

合约账户没有余额时,向用户账户转账

测试transfer成功的情况

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值