阅读 65

第十三课 SOLIDITY语法难点解析及故障排查

1.编辑器说明

(1)推荐编辑器 目前尝试 Solidity 编程的最好的方式是使用 Remix (需要时间加载,请耐心等待)。Remix 是一个基于 Web 的 IDE,它可以让你编写 Solidity 智能合约,然后部署并运行该智能合约。 如果外网不能访问,可以访问欧阳哥哥搭建的REMIX编辑器 (2)Visual Studio Extension Microsoft Visual Studio 的 Solidity 插件,包含 Solidity 编译器。 (3)Visual Studio Code extension Microsoft Visual Studio Code 插件,包含语法高亮和 Solidity 编译器。

2. REMIN的函数引用

function mint(address receiver, uint amount)

(1) 在REMIX输入时,地址一定要有""表示,否则amount就取不到值。 例如是mint("0xca35b7d915458ef540ade6068dfe2f44e8fa733c",101) (2) 程序中,如果注释包含中文,单步调试的位置不准确。

3.address相关全局函数

.balance (uint256): 该地址有多少以太坊余额(wei为单位)

.transfer(uint256 amount): 发送特定数量(wei为单位)的以太坊到对应地址,当出现错误时会扔出异常,但不会因异常而停止。固定需要耗费2300个gas。

.send(uint256 amount) returns (bool): 发送特定数量(wei为单位)的以太坊到对应地址,当出现错误时会返回flase。固定需要耗费2300个gas。

【警告】send() 执行有一些风险:如果调用栈的深度超过1024或gas耗光,交易都会失败。因此,为了保证安全,必须检查send的返回值,如果交易失败,会回退以太币。如果用transfer会更好。

.call(...) returns (bool): CALL的低级调用函数,当失败时返回false。执行需要消耗不固定的gas。 【说明】不鼓励使用call函数,后期将会被移除。调用该函数可能造成安全攻击,详见后期安全相关文章。

.callcode(...) returns (bool): CALLCODE的低级调用函数,当失败时返回false。执行需要消耗不固定的gas。 不建议使用,后续版本会删除。

.delegatecall(...) returns (bool): DELEGATECALL的低级调用函数,当失败时返回false。执行需要消耗不固定的gas。 **【说明】**为了和非ABI协议的合约进行交互,可以使用call() 函数, 它用来向另一个合约发送原始数据,支持任何类型任意数量的参数,每个参数会按规则(ABI协议)打包成32字节并一一拼接到一起。一个例外是:如果第一个参数恰好4个字节,在这种情况下,会被认为根据ABI协议定义的函数器指定的函数签名而直接使用。如果仅想发送消息体,需要避免第一个参数是4个字节。如下面的例子:

function callfunc(address addr) returns (bool){ bytes4 methodId = bytes4(keccak256("setScore(uint256)")); return addr.call(methodId, 100); }

测试地址和地址调用代码举例

pragma solidity ^0.4.18;

contract AddrTest{
    /*event函数知识把参数信息打印在REMIX等编译器的LOG位置区,不需要函数定义。*/
    event logdata(bytes data);
    event LogContractAddress(address exAccount, address contractAddress);
    
    uint score = 0;
    
    /*回调函数,没有函数名。任何调用不存在的函数,这时被调用的合约的fallback函数会执行。
     payable:如果一个函数需要进行货币操作,必须要带上payable关键字*/
    function() payable {
        logdata(msg.data);
    }
    
    /*智能合约构建函数*/
    function AddrTest(){
    LogContractAddress(msg.sender,this);
    }

    function getBalance() returns (uint) {
        return this.balance;
    }


    function setScore(uint s) public {
        score = s;
    }

    function getScore() returns ( uint){
        return score;
    }
}

contract CallTest{
    /*该函数有函数申明没有实际函数内容,在remix的value区域设置以太坊的个数,调用该函数会把外部账户(ACCOUNT)中的
      以太坊转移到智能合约账户中*/
    function deposit() payable {
    }

    event logSendEvent(address to, uint value);
    event LogContractAddress(address exAccount, address contractAddress);
    
    
    /*转以太坊给目标地址*/
    function transferEther(address towho) payable {
        towho.transfer(10);/*单位为wei*/
        
        logSendEvent(towho, 10);
    }
   
    /*不指定调用函数,则调用无函数名的回调函数*/
    function callNoFunc(address addr) returns (bool){
        return addr.call("tinyxiong", 1234);
    }

    /*制定调用函数的方法*/
    function callfunc(address addr) returns (bool){
        bytes4 methodId = bytes4(keccak256("setScore(uint256)"));
        return addr.call(methodId, 100);
    }  

    /*返回当前合约的以太坊余额*/
    function getBalance() returns (uint) {
        return this.balance;//0
    }  
    
    /*销毁智能合约,把以太坊余额返回给当前外部账户*/
    function ContractSuide() {
        LogContractAddress(this,msg.sender);
        suicide(msg.sender);
    }
}
复制代码

4.Contract Related

this (current contract’s type): 表示当前合约,可以显式的转换为Address selfdestruct(address recipient): destroy the current contract, sending its funds to the given Address 销毁当前合约,发送当前以太坊余额到给定的地址 suicide(address recipient): selfdestruct的别名函数

#5. 区块和交易属性 block.blockhash(uint blockNumber) returns (bytes32): 给定区块的哈希—仅对最近的 256 个区块有效而不包括当前区块 block.coinbase (address): 挖出当前区块的矿工地址 block.difficulty (uint): 当前区块难度 block.gaslimit (uint): 当前区块 gas 限额 block.number (uint): 当前区块号 block.timestamp (uint): 自 unix epoch 起始当前区块以秒计的时间戳 msg.data (bytes): 完整的 calldata msg.gas (uint): 剩余 gas msg.sender (address): 消息发送者(当前调用) msg.sig (bytes4): calldata 的前 4 字节(也就是函数标识符) msg.value (uint): 随消息发送的 wei 的数量 now (uint): 目前区块时间戳(block.timestamp) tx.gasprice (uint): 交易的 gas 价格 tx.origin (address): 交易发起者(完全的调用链)

注解 对于每一个外部函数调用,包括 msg.sender 和 msg.value 在内所有 msg 成员的值都会变化。这里包括对库函数的调用。

注解 不要依赖 block.timestamp、 now 和 block.blockhash 产生随机数,除非你知道自己在做什么。 时间戳和区块哈希在一定程度上都可能受到挖矿矿工影响。例如,挖矿社区中的恶意矿工可以用某个给定的哈希来运行赌场合约的 payout 函数,而如果他们没收到钱,还可以用一个不同的哈希重新尝试。 当前区块的时间戳必须严格大于最后一个区块的时间戳,但这里唯一能确保的只是它会是在权威链上的两个连续区块的时间戳之间的数值。

注解 基于可扩展因素,区块哈希不是对所有区块都有效。你仅仅可以访问最近 256 个区块的哈希,其余的哈希均为零。

#6. 错误处理 assert(bool condition): 如果条件不满足就抛出—用于内部错误。

require(bool condition): 如果条件不满足就抛掉—用于输入或者外部组件引起的错误。

revert(): 终止运行并恢复状态变动。

7 数学和密码学函数

addmod(uint x, uint y, uint k) returns (uint): 计算 (x + y) % k,加法会在任意精度下执行,并且加法的结果即使超过 2**256 也不会被截取。从 0.5.0 版本的编译器开始会加入对 k != 0 的校验(assert)。

mulmod(uint x, uint y, uint k) returns (uint): 计算 (x * y) % k,乘法会在任意精度下执行,并且乘法的结果即使超过 2**256 也不会被截取。从 0.5.0 版本的编译器开始会加入对 k != 0 的校验(assert)。

keccak256(...) returns (bytes32): 计算 (tightly packed) arguments 的 Ethereum-SHA-3 (Keccak-256)哈希。

sha256(...) returns (bytes32): 计算 (tightly packed) arguments 的 SHA-256 哈希。

sha3(...) returns (bytes32): 等价于 keccak256。

ripemd160(...) returns (bytes20): 计算 (tightly packed) arguments 的 RIPEMD-160 哈希。

ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address) : 利用椭圆曲线签名恢复与公钥相关的地址,错误返回零值。(example usage)

上文中的“tightly packed”是指不会对参数值进行 padding 处理(就是说所有参数值的字节码是连续存放的,译者注),这意味着下边这些调用都是等价的: keccak256("ab", "c") keccak256("abc") keccak256(0x616263) keccak256(6382179) keccak256(97, 98, 99) 如果需要 padding,可以使用显式类型转换:keccak256("\x00\x12") 和 keccak256(uint16(0x12)) 是一样的。

请注意,常量值会使用存储它们所需要的最少字节数进行打包。例如:keccak256(0) == keccak256(uint8(0)),keccak256(0x12345678) == keccak256(uint32(0x12345678))。

在一个私链上,你很有可能碰到由于 sha256、ripemd160 或者 ecrecover 引起的 Out-of-Gas。这个原因就是他们被当做所谓的预编译合约而执行,并且在第一次收到消息后这些合约才真正存在(尽管合约代码是硬代码)。发送到不存在的合约的消息非常昂贵,所以实际的执行会导致 Out-of-Gas 错误。在你的合约中实际使用它们之前,给每个合约发送一点儿以太币,比如 1 Wei。这在官方网络或测试网络上不是问题。

8 Using for 如何使用

using A for B,这里A通常是某个library里面定义的某个方法,B是某种数据类型,这句话是把A方法绑定到B类型上,相当于给B类型附加了一个A方法。(也有翻译为附着库的) 在上面的例子中,将LibContract里定义的方法绑定到所有的数据类型。但是一般我们不会在所有的类型实例上都去调用LibContract的方法,应该是要按需using的,这里偷懒就写*。 在通俗一点的例子就是, 比如 using LibInt for uint,然后LibInt里面有定义一个toString方法。我们有一个uint a;那么可以这样调用a.toString(),toString方法在定义的时候,第一个参数会是一个uint类型的变量,表示调用者。

using A for B,A的函数的第一个参数必须和B的数据类型一致。
复制代码

还有这个方法是可以重载的,你可以定义好几个同名的方法,但是第一个参数的类型不同,调用的时候自动的根据调用类型选择某一种方法。

#9 数组

数组可以在声明时指定长度,也可以动态调整大小。 对于 "存储"的数组来说,元素类型可以是任意的(即元素也可以是数组类型,映射类型或者结构体)。 对于 "memory"的数组来说,元素类型不能是映射类型,如果作为 public 函数的参数,它只能是 ABI 类型。

一个元素类型为 T,固定长度为 k 的数组可以声明为 T[k],而动态数组声明为 T[]。 举个例子,一个长度为 5,元素类型为 uint 的动态数组的数组,应声明为 uint[][5] (注意这里跟其它语言比,数组长度的声明位置是反的)。 要访问第三个动态数组的第二个元素,你应该使用 x[2][1](数组下标是从 0 开始的,且访问数组时的下标顺序与声明时相反,也就是说,x[2] 是从右边减少了一级)。

bytesstring 类型的变量是特殊的数组。 bytes 类似于 byte[],但它在 calldata 中会被“紧打包”(译者注:将元素连续地存在一起,不会按每 32 字节一单元的方式来存放)。 stringbytes 相同,但(暂时)不允许用长度或索引来访问。

注解 如果想要访问以字节表示的字符串 s,请使用 bytes(s).length / bytes(s)[7] = 'x';。 注意这时你访问的是 UTF-8 形式的低级 bytes 类型,而不是单个的字符。

可以将数组标识为 public,从而让 Solidity 创建一个 getter。 之后必须使用数字下标作为参数来访问 getter。

创建内存数组

可使用 new 关键字在内存中创建变长数组。 与"存储"(storage)数组相反的是,你不能通过修改成员变量 .length 改变 "内存"(memory)数组的大小。

contract C {
    function f(uint len) public pure {
        uint[] memory a = new uint[](7);
        bytes memory b = new bytes(len);
        // 这里我们有 a.length == 7 以及 b.length == len
        a[6] = 8;
    }
}
复制代码

数组字面常数 / 内联数组

数组字面常数是写作表达式形式的数组,并且不会立即赋值给变量。

contract C {
    function f() public pure {
        g([uint(1), 2, 3]);
    }
    function g(uint[3] _data) public pure {
        // ...
    }
}
复制代码

数组字面常数是一种定长的 "内存"(memory) 数组类型,它的基础类型由其中元素的普通类型决定。 例如,[1, 2, 3] 的类型是 uint8[3] memory,因为其中的每个字面常数的类型都是 uint8。 正因为如此,有必要将上面这个例子中的第一个元素转换成 uint 类型。 目前需要注意的是,定长的 "内存"(memory) 数组并不能赋值给变长的 "内存"(memory) 数组,下面是个反例:

// 这段代码并不能编译。

pragma solidity ^0.4.0;

contract C {
    function f() public {
        // 这一行引发了一个类型错误,因为 unint[3] memory
        // 不能转换成 uint[] memory。
        uint[] x = [uint(1), 3, 4];
    }
}
复制代码

已经计划在未来移除这样的限制,但目前数组在 ABI 中传递的问题造成了一些麻烦。

成员

length: 数组有 length 成员变量表示当前数组的长度。 动态数组可以在 存储storage (而不是 内存memory )中通过改变成员变量 .length 改变数组大小。 并不能通过访问超出当前数组长度的方式实现自动扩展数组的长度。 一经创建,内存memory 数组的大小就是固定的(但却是动态的,也就是说,它依赖于运行时的参数)。

push: 变长的 存储storage 数组以及 bytes 类型(而不是 string 类型)都有一个叫做 push 的成员函数,它用来附加新的元素到数组末尾。 这个函数将返回新的数组长度。

警告 在外部函数中目前还不能使用多维数组。

警告 由于 以太坊虚拟机Ethereum Virtual Machine(EVM) 的限制,不能通过外部函数调用返回动态的内容。 例如,如果通过 web3.js 调用 contract C { function f() returns (uint[]) { ... } } 中的 f 函数,它会返回一些内容,但通过 Solidity 不可以。 目前唯一的变通方法是使用大型的静态数组。

contract ArrayContract {
    uint[2**20] m_aLotOfIntegers;
    // 注意下面的代码并不是一对动态数组,
    // 而是一个数组元素为一对变量的动态数组(也就是数组元素为长度为 2 的定长数组的动态数组)。
    bool[2][] m_pairsOfFlags;
    // newPairs 存储在 memory 中 —— 函数参数默认的存储位置

    function setAllFlagPairs(bool[2][] newPairs) public {
        // 向一个 storage 的数组赋值会替代整个数组
        m_pairsOfFlags = newPairs;
    }

    function setFlagPair(uint index, bool flagA, bool flagB) public {
        // 访问一个不存在的数组下标会引发一个异常
        m_pairsOfFlags[index][0] = flagA;
        m_pairsOfFlags[index][1] = flagB;
    }

    function changeFlagArraySize(uint newSize) public {
        // 如果 newSize 更小,那么超出的元素会被清除
        m_pairsOfFlags.length = newSize;
    }

    function clear() public {
        // 这些代码会将数组全部清空
        delete m_pairsOfFlags;
        delete m_aLotOfIntegers;
        // 这里也是实现同样的功能
        m_pairsOfFlags.length = 0;
    }

    bytes m_byteData;

    function byteArrays(bytes data) public {
        // 字节的数组(语言意义中的 byte 的复数 ``bytes``)不一样,因为它们不是填充式存储的,
        // 但可以当作和 "uint8[]" 一样对待
        m_byteData = data;
        m_byteData.length += 7;
        m_byteData[3] = byte(8);
        delete m_byteData[2];
    }

    function addFlag(bool[2] flag) public returns (uint) {
        return m_pairsOfFlags.push(flag);
    }

    function createMemoryArray(uint size) public pure returns (bytes) {
        // 使用 `new` 创建动态 memory 数组:
        uint[2][] memory arrayOfPairs = new uint[2][](size);
        // 创建一个动态字节数组:
        bytes memory b = new bytes(200);
        for (uint i = 0; i < b.length; i++)
            b[i] = byte(i);
        return b;
    }
}
复制代码

18. solidity常见错误提示及原因分析

1). 智能合约执行失败

告警描述: " Warning! Error encountered during contract execution [Out of gas] " 发生场景: 执行官网众筹智能合约代码,在给智能合约打代币前,往智能合约地址账户打ETH,交易失败,提示如下: 点击查看信息链接 代码及原因分析: 下面是执行的回调函数,其中“tokenReward.transfer(msg.sender, amount / price);”的意思就是把智能合约的代币打给发送者账号。这个账号还没有代币,所以肯定会执行失败。

    function () payable {
        require(!crowdsaleClosed);
        uint amount = msg.value;
        balanceOf[msg.sender] += amount;
        amountRaised += amount;
        tokenReward.transfer(msg.sender, amount / price);
        FundTransfer(msg.sender, amount, true);
    }
复制代码

解决方法: 先往智能合约账号打代币,然后打ETH,就不会执行失败了。

####2). REMIX+MetaMASK的场景下,调用智能合约函数,提示不可知地址 告警描述: REMIX输出框输出以下告警内容: transact to Crowdsale.checkGoalReached errored: Unknown address - unable to sign transaction for this address: "0x3d7dfb80e71096f2c4ee63c42c4d849f2cbbe363" 发生场景: 更换了Meta账号后,调用之前账号创建的智能合约的函数 原因分析: Remix输出框提示未知地址:

告警描述
查看MetaMask的当前账号,发现是Account 1的账号,所以无法执行合约。
MetaMask的当前账号
解决方法: 更换MetaMask为Account8(地址为0x3D7DfB80E71096F2c4Ee63C42C4D849F2CBBE363)的账号即可正常运行。

3). GETH安装时出现异常

告警描述: GETH安装时出现以下告警: E: Failed to fetch http://101.110.118.22/ppa.launchpad.net/ethereum/ethereum/ubuntu/pool/main/e/ethereum/ethereum_1.8.10+build13740+artful_i386.deb Could not connect to 101.110.118.22:80 (101.110.118.22), connection timed out E: Unable to fetch some archives, maybe run apt-get update or try with --fix-missing? 发生场景: GETH安装,执行以下命令出现。

sudo apt-get install ethereum

原因分析: 解决方法: <1> 参考网上解决方案 sudo vim /etc/resolv.conf ,添加:

nameserver 8.8.8.8

然后执行:

sudo /etc/init.d/networking restart

还是不行。 <2> 从绿地金融中心搬到家里,做相同的操作,就可以安装成功了。 应该是绿地金融中心的路由器做了某些配置,导致下载不成功。

4). 问题描述模板

告警描述:

DocstringParsingError: Documented parameter "owner" not found in the parameter list of the function.
复制代码

发生场景: remix编译以下智能合约函数时出现:

    /** 
     * @dev Withdraw the token remained to the constructor address.
     * @param owner Address of owner.
     */
    function withdrawToken() public onlyCreator{
        if( 0 < token.balanceOf(address(this))) {
           token.transfer(creator, token.balanceOf(address(this)));
        }
    }    

复制代码

原因分析: remix竟然要检测@param后面跟的参数,看来注释不能随便乱写了。 解决方法: 函数修改为以下的就能编译通过了。

    /** 
     * @dev Withdraw the token remained to the constructor address.
     */
    function withdrawToken() public onlyCreator{
        if( 0 < token.balanceOf(address(this))) {
           token.transfer(creator, token.balanceOf(address(this)));
        }
    } 
复制代码

2). 问题描述模板

告警描述: 发生场景: 原因分析: 解决方法:

9. 常见问题及解答

1).modifer函数是干什么的?

2).如何打币回支付账号?

3).智能合约的定时器和系统函数是什么?

4).当创建一个智能合约时,msg.sender和this的区别? 答复:msg.sender是指外部账户的地址,this是指当前创建的智能合约的地址。

contract AddrTest{
    event LogContractAddress(address exAccount, address contractAddress);
    
    function AddrTest(){
    LogContractAddress(msg.sender,this);
    }    
}
复制代码

在remix运行,可以证明这个推测

LOG说明

10 View Functions,Pure Functions,Fallback Function的定义?

答案:点击参考官网文档,还没有时间翻译过来。

11. 文档参考

1,官方中文网站 http://solidity-cn.readthedocs.io/zh/develop/

2, tiny熊翻译系列 1] Solidity教程序列1 - 类型介绍 2] 智能合约语言Solidity教程系列2 - 地址类型介绍 3] 智能合约语言 Solidity 教程系列3 - 函数类型 4] 智能合约语言 Solidity 教程系列4 - 数据存储位置分析 5] 智能合约语言 Solidity 教程系列5 - 数组介绍 6] 智能合约语言 Solidity 教程系列6 - 结构体与映射 7] 智能合约语言 Solidity 教程系列7 - 以太单位及时间单位 8] 智能合约语言 Solidity 教程系列8 - Solidity API 9] 智能合约语言 Solidity 教程系列9 - 错误处理 10] 智能合约语言 Solidity 教程系列10 - 完全理解函数修改器

关注下面的标签,发现更多相似文章
评论