[9232585]合约注意事项

solidity 合约注意事项

1. 栈深度

EVM 栈深度1024字(1024*256字节)

2. solidity 局部变量个数不能超过16

因为提供的指令集对于栈深度的访问不能超过16,也就是说最多把第16个字换到栈顶进行操作.

3.内存费用

占用 EVM 的内存越大,收费越高,收费是内存大小的平方,具体计算方式未知.

4.delete 关键字

delete 作用相当于将变量的值设置为默认值,但是会降低 gas 的费用.

4.abi.encodePacked 有什么价值?

以下面的registerSecret为例,如果参数个数为1个,那么有abi.encodePacked,将会多浪费350的 gas, 如果两个参数多浪费458的 gas, 如果是五个参数,将会多浪费749的 gas.
但是有没有 abi.encodePacked, 得到的 secrethash 都是一样的.也就是结果完全一样. 不明白为什么要多浪费这些 gas.

pragma solidity ^0.4.23;

contract SecretRegistry {

    /*
     *  Data structures
     */

    string constant public contract_version = "0.3._";

    // secrethash => block number at which the secret was revealed
    mapping(bytes32 => uint256) public secrethash_to_block;

    /*
     *  Events
     */

    event SecretRevealed(bytes32 indexed secrethash);


    function registerSecret(uint256 u1,uint256 u2,uint256 u3,uint256 u4,uint256 u5) public returns (bool) {
        bytes32 secrethash = keccak256(abi.encodePacked(u1,u2,u3,u4,u5));
        if (u1 == 0x0 || secrethash_to_block[secrethash] > 0) {
            return false;
        }
        secrethash_to_block[secrethash] = block.number;
        emit SecretRevealed(secrethash);
        return true;
    }

    function getSecretRevealBlockHeight(bytes32 secrethash) public view returns (uint256) {
        return secrethash_to_block[secrethash];
    }
}

5. sha256,ripemd160,ecrecover

这些功能并没有专门的指令集,而是有一个内置的合约来实现上述功能.(指令集明明有sha256,难道现在移除了?)
参考 solidity0.4.24文档
It might be that you run into Out-of-Gas for sha256, ripemd160 or ecrecover on a private blockchain. The reason for this is that those are implemented as so-called precompiled contracts and these contracts only really exist after they received the first message (although their contract code is hardcoded). Messages to non-existing contracts are more expensive and thus the execution runs into an Out-of-Gas error. A workaround for this problem is to first send e.g. 1 Wei to each of the contracts before you use them in your actual contracts. This is not an issue on the official or test net.

6. 合约中使用 call

下面提供了两个例子, approve and call, 主要是尽量减少 transactions.
比如雷电网络在 openchannel 的时候,如果对应的 ERC20token 支持approve and call,那么 deposit 行为就没必要是两个 transaction, 只需一个即可.

/* Approves and then calls the receiving contract */
function approveAndCall(address _spender, uint256 _value, bytes _extraData) returns (bool success) {
    allowed[msg.sender][_spender] = _value;
    Approval(msg.sender, _spender, _value);

    //call the receiveApproval function on the contract you want to be notified. This crafts the function signature manually so one doesn't have to include a contract in here just for this.
    //receiveApproval(address _from, uint256 _value, address _tokenContract, bytes _extraData)
    //it is assumed that when does this that the call *should* succeed, otherwise one would use vanilla approve instead.
    if(!_spender.call(bytes4(bytes32(sha3("receiveApproval(address,uint256,address,bytes)"))), msg.sender, _value, this, _extraData)) { throw; }
    return true;
}

/* Approves and then calls the contract code*/
function approveAndCallcode(address _spender, uint256 _value, bytes _extraData) returns (bool success) {
    allowed[msg.sender][_spender] = _value;
    Approval(msg.sender, _spender, _value);

    //Call the contract code
    if(!_spender.call(_extraData)) { throw; }
    return true;
}

7.call,delegatecall,callcode 注意事项

如果被调合约地址不存在,也会返回成功.
The low-level call, delegatecall and callcode will return success if the called account is non-existent, as part of the design of EVM. Existence must be checked prior to calling if desired.

8.require 和 assert 的区别

require(对应指令0xfd) 用于验证用户输入是否有效, assert(对应指令0xfe) 用于未想到的内部错误.
Internally, Solidity performs a revert operation (instruction 0xfd) for a require-style exception and executes an invalid operation (instruction 0xfe) to throw an assert-style exception.

#9. 函数external,internal,public和 private 的区别
external 只能通过message call 的形式调用,就是本合约内部调用也要通过 this.f()这种形式,无法直接 f()
internal 只能在合约内以及继承合约内调用
Public 既可以在合约内调用也可以在合约外调用,合约内调用是直接 jump, 合约外调用是 message call
private 只能被合约自己调用,包括集成合约都不可以

10.函数的其他描述关键字 view

view 保证不修改合约状态.以下均认为会改变合约状态

  • Writing to state variables.
  • Emitting events.
  • Creating other contracts.
  • Using selfdestruct.
  • Sending Ether via calls.
  • Calling any function not marked view or pure.
  • Using low-level calls.
  • Using inline assembly that contains certain opcodes.

    11. pure 关键字

    pure 标注以后不仅不修改合约状态,甚至不会读合约状态,
    读合约状态包括以下行为
    Reading from state variables.
    Accessing this.balance or

    .balance.
    Accessing any of the members of block, tx, msg (with the exception of msg.sig and msg.data).
    Calling any function not marked pure.
    Using inline assembly that contains certain opcodes.

12. library用法

下面这个 Set 的insert,remove,contains 都是 delegatecall 调用,
但是 contains 是通过来jump 调用.
delegatecall 也是一种 messagecall 但是将使用当前合约的上下文环境.也就是msg.sender,msg.value,this 都不变

pragma solidity ^0.4.22;

library Set {
  // We define a new struct datatype that will be used to
  // hold its data in the calling contract.
  struct Data { mapping(uint => bool) flags; }

  // Note that the first parameter is of type "storage
  // reference" and thus only its storage address and not
  // its contents is passed as part of the call.  This is a
  // special feature of library functions.  It is idiomatic
  // to call the first parameter `self`, if the function can
  // be seen as a method of that object.
  function insert(Data storage self, uint value)
      public
      returns (bool)
  {
      if (self.flags[value])
          return false; // already there
      self.flags[value] = true;
      return true;
  }

  function remove(Data storage self, uint value)
      public
      returns (bool)
  {
      if (!self.flags[value])
          return false; // not there
      self.flags[value] = false;
      return true;
  }

  function contains(Data storage self, uint value)
      public
      view
      returns (bool)
  {
      return self.flags[value];
  }
}

contract C {
    Set.Data knownValues;

    function register(uint value) public {
        // The library functions can be called without a
        // specific instance of the library, since the
        // "instance" will be the current contract.
        require(Set.insert(knownValues, value));
    }
    // In this contract, we can also directly access knownValues.flags, if we want.
}

合约优化123

存储优化

定义的 struct 就像 c 语言有 align 问题一样,否则会带来空间浪费.
除非是定义在 storage 区域的 state 变量,否则没必要使用小于一个字的类型,因为那样更浪费指令,并且不节省空间.
When using elements that are smaller than 32 bytes, your contract’s gas usage may be higher. This is because the EVM operates on 32 bytes at a time. Therefore, if the element is smaller than that, the EVM must use more operations in order to reduce the size of the element from 32 bytes to the desired size.

It is only beneficial to use reduced-size arguments if you are dealing with storage values because the compiler will pack multiple elements into one storage slot, and thus, combine multiple reads or writes into a single operation. When dealing with function arguments or memory values, there is no inherent benefit because the compiler does not pack these values.

Finally, in order to allow the EVM to optimize for this, ensure that you try to order your storage variables and struct members such that they can be packed tightly. For example, declaring your storage variables in the order of uint128, uint128, uint256 instead of uint128, uint256, uint128, as the former will only take up two slots of storage whereas the latter will take up three.

EVM指令集消耗 gas 统计

关键是看 gas 消耗最大的一些项目

  1. create contract 32000
  2. 在 storage 区域写数据,第一次写20000,以后写5000
  3. EXTCODESIZE 700,也就是判断一下某个地址是否有合约代码,需要 gas700
  4. BALANCE 获取账户 ether 余额 400
  5. 几个 log 指令,375起步
  6. SLOAD 读取 storage 区域,200
    像 SHA3这些指令反而很便宜,不知道 ecrecover 有多贵,ecrecover 是一种合约调用指令,比较复杂
  7. 分配 storage 空间并不花费 gas, 只有写内容才花费 gas
  8. 将 storage 内容由0设置为非0,补偿15000,相当于直接补贴10000(还要减去指令本身的5000??验证一下)
  9. 销毁一个合约账户补偿24000,还有5000销毁本身的费用,相当于获取收益19000

    EVM指令集gas统计

    7.1.3 The Ethereum Virtual Machine

Overview

The Ethereum Virtual Machine or EVM is the runtime environment for smart contracts in Ethereum. It is not only sandboxed but actually completely isolated, which means that code running inside the EVM has no access to network, filesystem or other processes. Smart contracts even have limited access to other smart contracts.

Accounts

There are two kinds of accounts in Ethereum which share the same address space: External accounts that are controlled by public-private key pairs (i.e. humans) and contract accounts which are controlled by the code stored together with the account.

The address of an external account is determined from the public key while the address of a contract is determined at the time the contract is created (it is derived from the creator address and the number of transactions sent from that address, the so-called “nonce”).

Regardless of whether or not the account stores code, the two types are treated equally by the EVM.

Every account has a persistent key-value store mapping 256-bit words to 256-bit words called storage.

Furthermore, every account has a balance in Ether (in “Wei” to be exact) which can be modified by sending transactions that include Ether.

Transactions

A transaction is a message that is sent from one account to another account (which might be the same or the special zero-account, see below). It can include binary data (its payload) and Ether.

If the target account contains code, that code is executed and the payload is provided as input data.

If the target account is the zero-account (the account with the address 0), the transaction creates a new contract. As already mentioned, the address of that contract is not the zero address but an address derived from the sender and its number of transactions sent (the “nonce”). The payload of such a contract creation transaction is taken to be EVM bytecode and executed. The output of this execution is permanently stored as the code of the contract. This means that in order to create a contract, you do not send the actual code of the contract, but in fact code that returns that code when executed.

Note: While a contract is being created, its code is still empty. Because of that, you should not call back into the contract under construction until its constructor has finished executing.

Gas

Upon creation, each transaction is charged with a certain amount of gas, whose purpose is to limit the amount of work that is needed to execute the transaction and to pay for this execution. While the EVM executes the transaction, the gas is gradually depleted according to specific rules.

The gas price is a value set by the creator of the transaction, who has to pay gas_price * gas up front from the sending account. If some gas is left after the execution, it is refunded in the same way.

If the gas is used up at any point (i.e. it is negative), an out-of-gas exception is triggered, which reverts all modifications made to the state in the current call frame.

Storage, Memory and the Stack

Each account has a persistent memory area which is called storage. Storage is a key-value store that maps 256-bit words to 256-bit words. It is not possible to enumerate storage from within a contract and it is comparatively costly to read and even more so, to modify storage. A contract can neither read nor write to any storage apart from its own.

The second memory area is called memory, of which a contract obtains a freshly cleared instance for each message call. Memory is linear and can be addressed at byte level, but reads are limited to a width of 256 bits, while writes can be either 8 bits or 256 bits wide. Memory is expanded by a word (256-bit), when accessing (either reading or writing) a previously untouched memory word (ie. any offset within a word). At the time of expansion, the cost in gas must be paid. Memory is more costly the larger it grows (it scales quadratically).

The EVM is not a register machine but a stack machine, so all computations are performed on an area called the stack. It has a maximum size of 1024 elements and contains words of 256 bits. Access to the stack is limited to the top end in the following way: It is possible to copy one of the topmost 16 elements to the top of the stack or swap the topmost element with one of the 16 elements below it. All other operations take the topmost two (or one, or more, depending on the operation) elements from the stack and push the result onto the stack. Of course it is possible to move stack elements to storage or memory, but it is not possible to just access arbitrary elements deeper in the stack without first removing the top of the stack.

Instruction Set

The instruction set of the EVM is kept minimal in order to avoid incorrect implementations which could cause consensus problems. All instructions operate on the basic data type, 256-bit words. The usual arithmetic, bit, logical and comparison operations are present. Conditional and unconditional jumps are possible. Furthermore, contracts can access relevant properties of the current block like its number and timestamp.

Message Calls

Contracts can call other contracts or send Ether to non-contract accounts by the means of message calls. Message calls are similar to transactions, in that they have a source, a target, data payload, Ether, gas and return data. In fact, every transaction consists of a top-level message call which in turn can create further message calls.

A contract can decide how much of its remaining gas should be sent with the inner message call and how much it wants to retain. If an out-of-gas exception happens in the inner call (or any other exception), this will be signalled by an error value put onto the stack. In this case, only the gas sent together with the call is used up. In Solidity, the calling contract causes a manual exception by default in such situations, so that exceptions “bubble up” the call stack.

As already said, the called contract (which can be the same as the caller) will receive a freshly cleared instance of memory and has access to the call payload - which will be provided in a separate area called the calldata. After it has finished execution, it can return data which will be stored at a location in the caller’s memory preallocated by the caller.

Calls are limited to a depth of 1024, which means that for more complex operations, loops should be preferred over recursive calls.

Delegatecall / Callcode and Libraries

There exists a special variant of a message call, named delegatecall which is identical to a message call apart from the fact that the code at the target address is executed in the context of the calling contract and msg.sender and msg.value do not change their values.

This means that a contract can dynamically load code from a different address at runtime. Storage, current address and balance still refer to the calling contract, only the code is taken from the called address.

This makes it possible to implement the “library” feature in Solidity: Reusable library code that can be applied to a contract’s storage, e.g. in order to implement a complex data structure.

Logs

It is possible to store data in a specially indexed data structure that maps all the way up to the block level. This feature called logs is used by Solidity in order to implement events. Contracts cannot access log data after it has been created, but they can be efficiently accessed from outside the blockchain. Since some part of the log data is stored in bloom filters, it is possible to search for this data in an efficient and cryptographically secure way, so network peers that do not download the whole blockchain (“light clients”) can still find these logs.

Create

Contracts can even create other contracts using a special opcode (i.e. they do not simply call the zero address). The only difference between these create calls and normal message calls is that the payload data is executed and the result stored as code and the caller / creator receives the address of the new contract on the stack.

Self-destruct

The only possibility that code is removed from the blockchain is when a contract at that address performs the selfdestruct operation. The remaining Ether stored at that address is sent to a designated target and then the storage and code is removed from the state.

Warning: Even if a contract’s code does not contain a call to selfdestruct, it can still perform that operation using delegatecall or callcode.

Note: The pruning of old contracts may or may not be implemented by Ethereum clients. Additionally, archive nodes could choose to keep the contract storage and code indefinitely.

Note: Currently external accounts cannot be removed from the state

call,delegateCall,callCode和staticCall的区别

func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *big.Int) (ret []byte, leftOverGas uint64, err error) 
func (evm *EVM) CallCode(caller ContractRef, addr common.Address, input []byte, gas uint64, value *big.Int) (ret []byte, leftOverGas uint64, err error) 
func (evm *EVM) DelegateCall(caller ContractRef, addr common.Address, input []byte, gas uint64) (ret []byte, leftOverGas uint64, err error) 
func (evm *EVM) StaticCall(caller ContractRef, addr common.Address, input []byte, gas uint64) (ret []byte, leftOverGas uint64, err error) 
  1. call 是通过Tx发起的合约调用,走的都是这个调用.合约内调用其他合约也是走的这个函数
  2. CallCode主要是服务于library,假设如下调用
    账户A调用ContractA的FA,然后FA调用Library B的FB,这时候在FB中看到的msg.sender是ContractA,value是由调用FB的时候指定的.
    目前solidity生成的代码中已经不包含该指令.

  3. DelegateCall 是CallCode的升级,也是服务于Library,假设调用如下:
    账户A调用ContractA的FA,然后FA调用Library B的FB,这时候在FB中看到的msg.sender是A,value是由A调用FA的时候指定的
    在如下示例中可以观察到DelegateCall,如果Library没有数据,则不会观察到DelegateCall
    ```sol
    // Code for StringToUintMap.sol

pragma solidity ^0.4.24;

library StringToUintMap {

struct Data {
    mapping (string => uint8) map;
}

function insert(
    Data storage self,
    string key,
    uint8 value) public returns (bool updated)
{
    require(value > 0);

    updated = self.map[key] != 0;
    self.map[key] = value;
}

function get(Data storage self, string key) view public returns (uint8) {
    return self.map[key];
}

}

```solidty
pragma solidity ^0.4.24;

import { StringToUintMap } from "lib1.sol";

contract PersonsAge {

    StringToUintMap.Data private _stringToUintMapData;

    event PersonAdded(string name, uint8 age);
    event GetPersonAgeResponse(string name, uint8 age);

    function addPersonAge(string name, uint8 age) public {
        StringToUintMap.insert(_stringToUintMapData, name, age);

        emit PersonAdded(name, age);
    }

    function getPersonAge(string name) public  returns (uint8) {
        uint8 age = StringToUintMap.get(_stringToUintMapData, name);

        emit GetPersonAgeResponse(name, age);

        return age;
    }
}
  1. StaticCall 用于查询合约.保证该调用执行过程中合约状态不被修改.
    用户直接发起的查询或者在合约调用中(比如刚刚的FA),读取查询合约的状态.
    实际发现并没有如此使用
    sol require(token_contract.totalSupply() > 0);
    这句调用生成的asm,使用的仍然是call指令,而不是StaticCall指令

STATICCALL
The Byzantium network upgrade scheduled for October 9 will add a STATICCALL opcode that enforces read-only calls at runtime. Solidity only implements the STATICCALL opcode in its assembly language. Adding pureand view specifiers does not change the opcode that will be used to call the function, so they only affect compiler errors, not any behavior on chain. In the future, calls to pure or view functions could be compiled as STATICCALL, ensuring that the developer’s expectations of immutability are never violated.