Sort:  
There are 2 pages
Pages

类型.jpg

  • 值类型
    值类型传值时,会临时拷贝一份内容出来,而不是拷贝指针,当你修改新的变量时,不会影响原来的变量的值。
    布尔(Booleans)
    整型(Integer)
    地址(Address)
    定长字节数组(fixed byte arrays)
    有理数和整型(Rational and Integer Literals,String literals)
    枚举类型(Enums)
    函数(Function Types)
  • 引用类型(Reference Types)
    引用即地址传递,复杂类型,占用空间较大。在拷贝时占用空间较大,所以考虑通过引用传递。
    不定长字节数组(bytes)
    字符串(string)
    数组(Array)
    结构体(Structs)

  • 两者区别:
    如果是值传递,修改新变量时,不会影响原来的变量值,如果是引用传递,那么当你修改新变量时,原来变量的值会跟着变化,这是因为新变量同时指向同一个地址的原因。

Loading...
Loading...

constant, immutable
constant 修饰的变量需要在编译期确定值, 链上不会为这个变量分配存储空间, 它会在编译时用具体的值替代, 因此, constant常量是不支持使用运行时状态赋值的(例如: block.number, block.timestamp, msg.sender 等这些是不能用constant )。
constant 目前仅支持修饰 strings 及 值类型. 它更为节约gas
uint public constant NUM = 69;

immutable 修饰的变量是在部署的时候确定变量的值, 它在构造函数中赋值一次之后,就不再改变, 这是一个运行时赋值, 就可以解除之前 constant 不支持使用运行时状态赋值的限制.
uint immutable decimals;
uint immutable maxBalance;
IMasterChef public immutable MASTER_CHEF;
address public immutable owner = msg.sender;

单位.png

gas价格

最小单位是wei
ether  wei  (finney szabo 7.0后已不再使用)
1 ether = 10**18 wei // 1e18
1 Gwei = 10**9 wei

solidity中并没有真正的随机数,不建议使用,最好使用预言机中的随机数功能。

random = uint256(keccak256(abi.encode(block.timestamp, admin, hash))) % 100;

eg:
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
function _generateRandomDna(string memory _str) public view returns (uint) {
  return uint(keccak256(abi.encodePacked(_str, block.timestamp))) % dnaModulus;
  //9758857365005908
}
默认单位是秒
block.timestamp  //1592697341 单位为秒的时间戳
1 == 1 seconds
1 minutes == 60 seconds
1 hours == 60 minutes
1 days == 24 hours
1 weeks == 7 days

eg:
return x * 1 hours + y * 1 minutes; //3660

手册

在全局命名空间中已经存在了(预设了)一些特殊的变量和函数,他们主要用来提供关于区块链的信息或一些通用的工具函数。可以把这些变量和函数理解为Solidity语言层面的(原生)API 。

blockhash(uint) :指定区块的区块哈希——仅可用于最新的 256 个区块且不包括当前区块
block.coinbase ( address ): 挖出当前区块的矿工地址
block.difficulty ( uint ): 当前区块难度
block.gaslimit ( uint ): 当前区块 gas 限额
block.number ( uint ): 当前区块号
block.timestamp ( uint): 自 unix epoch 起始当前区块以秒计的时间戳
gasleft() returns (uint256) :剩余的 gas

msg.data ( bytes ): 完整的 calldata
msg.gas (uint): 剩余的gas量 
msg.sender ( address ): 消息发送者(当前调用),调用合约方法的人
msg.sig ( bytes4 ): calldata 的前4字节(也就是函数标识符)
msg.value ( uint ): 随消息发送的 wei 的数量
tx.gasprice (uint): 交易的 gas 价格
tx.origin (address payable): 交易发起者(完全的调用链)

//合约相关
this : 合约指针,表示当前合约,可以转换为地址
selfdestruct(address recipient) 销毁合约,并把它所有资⾦发送到给定的地址recipient

函数是完成特定任务的自包含代码模块。与其他 web3 编程语言一样,Solidity 允许开发者通过使用函数编写模块化代码,以消除重新编写相同代码片段的冗余。相反,开发者可以在程序中必要时调用该函数。

function function-name(parankust...) modifiers returns(returnlist...) {
// statements
}
// 使用 function 关键字定义函数
// 创建一个唯一的函数名称,且不与任何保留关键字冲突
// 列出包含参数名称和数据类型的参数,或者不包含任何额外参数
// 创建一个用大括号包围的语句块

receive接收以太币, 无名称,无参数,无返回值
一个合约最多有一个 receive 函数, 声明函数为:
receive() external payable {}

Fallback 回退函数,无名称,无参数,无返回值
合约可以最多有一个回退函数。函数声明为:
fallback () external [payable] {}

receive() external payable {
    totalAmount += msg.value;
    addrs.push(msg.sender);
}

fallback() external payable {
    totalAmount += msg.value;
    addrs.push(msg.sender); 
}

有四个关键词:public private external internal

public - 任意访问
private - 仅当前合约内
internal - 仅当前合约及所继承的合约
external - 仅外部访问(在内部也只能用外部访问方式访问)

当使用public 函数时,Solidity会立即复制数组参数到内存, 而external函数则是从calldata读取,而分配内存开销比直接从calldata读取要大的多。
那为什么public函数要复制数组参数数据到内存呢?是因为public函数可能会被内部调用,而内部调用数组的参数是当做指向一块内存的指针。
对于external函数不允许内部调用,它直接从calldata读取数据,省去了复制的过程。

所以,如果确认一个函数仅仅在外部访问,请用external。
当需要内外部都要调用的时候,请用public。

public external
都可以访问,都可以继承
但是external内部不能访问,除非带上this

private internal
只能内部访问,private不可继承,internal可以继承

payable view pure:
payable 可以接受以太币
view 只看不修改(状态变量)
pure 纯函数,不读也不写(状态变量)

transfer 和 send异同:
两者都可以转移以太币,但尽量使用transfer。都有2300gas的限制。
在转帐时send异常时不会发生错误,只会返回一个布尔值(false),所以要使用判断函数assert(addr.send(1 ether))

address payable _to = 0xddsddfxxx;
//有2300gas的限制
 _to.transfer(1 ether);

bool sent = _to.send(1 ether);
require(sent, "Failed to send Ether");

// 当然也可以使用来call发送
(bool sent, bytes memory data) = _to.call{value: 1 ether}("");
require(sent, "Failed to send Ether");

//sendValue没有gas的限制
owner.sendValue(address(this).balance);
function sendValue(address payable recipient, uint256 amount) internal {
  require(address(this).balance >= amount, "Address: insufficient balance");

  // solhint-disable-next-line avoid-low-level-calls, avoid-call-value
  (bool success, ) = recipient.call{ value: amount }("");
  require(success, "Address: unable to send value, recipient may have reverted");
}

数据存储位置分析

memory storage calldata
memory 存储在EVM内存中,主要有局部变量,函数参数,值传递
storage 存储在区块链中,主要有状态变量,复杂变量,数组,引用传递,指针
calldata,用来存储函数参数,是只读的,不会永久存储的一个数据位置。外部函数的参数(不包括返回参数)被强制指定为calldata。效果与memory差不多,但更为节约gas。

int[] arr;
string tt;
function fun1(uint m, string memory s) public returns(string memory) {
    uint n = m;
    string memory str = s;
    tt = s;
    
    string memory s1 = 'abc';
    string memory s2 = s1;
    
    int[] storage abc = arr;
    return tt;
}

function fun2(uint m, string calldata s) external {
    uint n = m;
    string memory str = s;
}
Loading...

使用修饰器modifier可以轻松改变函数的行为。 例如,它们可以在执行函数之前自动检查某个条件。

contract owned {
  address owner;
  
  //修饰器所修饰的函数体会被插入到特殊符号 _; 的位置。
  modifier onlyOwner {
      require(
          msg.sender == owner,
          "Only owner can call this function."
      );
      _;
  }

  //只有合约的创建者才能销毁合约
  function destroy() public onlyOwner {
        selfdestruct(owner);
 }
}

参考

abi.encodeWithSignature(....)的前四个字节就是函数选择器,也就是msg.sig,
也可这这样计算 bytes4(keccak256(bytes(_func)))
这种方法可以直接调用合约的方法
eg:
  addr.call(abi.encodeWithSignature("transfer(address,uint256)", SomeAddress, 123))
  bytes4(keccak256("set(uint256)"))

eg2:
  function foo(string memory _message, uint _x) public payable returns (uint) {
      emit Received(msg.sender, msg.value, _message);
      return _x + 1;
  }
  (bool success, bytes memory data) = _addr.call{value: msg.value, gas: 5000}(
    abi.encodeWithSignature("foo(string,uint256)", "call foo", 123)
  );

import * as symbolName from "filename";
import "filename" as symbolName;
然后所有函数都以symbolName.symbol格式提供。

import 相当于接口,导入其它合约的函数,不过函数的执行环境不变,仍在原有合约中执行。例如:B中导入A,A.send()仍会在A的环境中执行。
import 演化成库和接口(interface), 标准化程度更高。

contract A {
    function add (uint x, uint y) public pure returns (uint) {
        return x.plus(y);
    }
}

import "./A.sol"
contract B {
    function add2 (A a, uint x, uint y) public pure returns (uint) {
        return a.add(x,y)
    }
}
Loading...

Assert, Require, Revert

assert(bool condition)
⽤于判断内部错误,条件不满⾜时抛出异常.函数只能用于测试内部错误,并检查非变量。
用于pure函数,会对用户惩罚,扣光gas

require(bool condition)
require(bool condition, string message) //提供错误信息。
函数用于确认条件有效性,例如输入变量,或合约状态变量是否满足条件,或验证外部合约调用返回的值。⽤于判断输⼊或外部组件错误,条件不满⾜时抛出异常。
会退还剩余gas

revert() //终⽌执⾏并还原改变的状态
revert(string reason) //提供⼀个错误信息。
可以用来标记错误并回退当前的调用。 revert 调用中还可以包含有关错误信息的参数,这个信息会被返回给调用者。

require(msg.value % 2 == 0, "Even value required.");
assert(this.balance == balanceBeforeTransfer - msg.value / 2);

if (amount > msg.value / 2 ether){
  revert("Not enough Ether provided.");
}
error Myerror(address caller, uint i);

function test(uint _i) public view {
  if(_i > 10) {
    revert Myerror(msg.sender, _i);
  }
}
Loading...

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

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

避免使用 selfdestruct。优先选择代理升级(Upgradeable Contracts)或状态冻结模式。

function destroy() public onlyOwner {
   selfdestruct(owner);
}
Loading...
  1. 版本申明
  2. 引用
  3. 合约主体
  4. 注释
// SPDX-License-Identifier: MIT  //开源协议
pragma solidity ^0.8.20;//申明版本,最低0.8.20
/*
* 这是注释段落
*
*/
import "./first_interface.sol"; //引用文件

contract SimpleStorage {
    //合约主体
    uint storedData;

    function set(uint x) public {
        storedData = x;
    }
    
    function get() public view returns (uint) {
        return storedData;
    }
}
Loading...

标准的内容:名称,发行量,统一函数名,事件
参考 |
参考2 |
erc20|
erc20-5.0|
erc20-old |
OpenZeppelin

cnpm install @openzeppelin/contracts --save
// npm install @openzeppelin/contracts

// SPDX-License-Identifier: MIT 
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract TutorialToken is ERC20 {
    constructor(uint256 initialSupply) ERC20("Gold", "GLD") {
        _mint(msg.sender, initialSupply);
    }
}
Loading...

calldata调用
EVM的函数选择原理

Low level interactions 一般是调用receive() 或 fallback()
Low level interactions are used to send funds or calldata or funds & calldata to a contract through the receive() or fallback() function. Typically, you should only need to implement the fallback function if you are following an upgrade or proxy pattern.

The low level interactions section is below the functions in each deployed contract.

//calldata计算的三种方法
a. bytes32 calldata = abi.encodeWithSignature("transfer(address,uint256)", SomeAddress, 123)
  addr.call(calldata)
  addr.call{value: msg.value, gas: 5000}(calldata) 

b. abi.encodeWithSelector(IERC20.transfer.selector, to, amount);
c. abi.encodeCall(IERC20.transfer, (to, amount));
Loading...
Loading...

interface id是接口中所有函数选择器的异或值。

function getSelector() public pure returns (bytes32, bytes4, bytes4) {
    //addUser(string memory _name, uint8 _age)
    bytes32 hash;
    //hash = keccak256(abi.encode("addUser(string,uint8)"));
    hash = keccak256("addUser(string,uint8)");
    return (hash, bytes4(hash), IUser.addUser.selector);
}

function getInterfaceID() public pure returns (bytes4) {
    return IUser.addUser.selector ^ IUser.getUser.selector;
}

Application Binary Interface 应⽤程序⼆进制接⼝
调⽤⼀个合约函数 = 向合约地址发送⼀个交易(交易的内容就是 ABI 编码数据)

⽤ABI编码传递数据给合约,因此与合约交互离不开ABI

abi.encode(...) returns (bytes) : 计算参数的ABI编码
abi.encodePacked(...) returns (bytes) : 计算参数的紧密打包编码,不会补零
abi.encodeWithSelector(bytes4 selector, ...) returns (bytes):计算函数选择器和参数的ABI编码
abi.encodeWithSignature(string signature, ...) returns (bytes): 等价于 abi.encodeWithSelector(bytes4(keccak256(signature), ...)

eg:
function set(uint x) public {
   storedData = x;
}
function abiEncode() public constant returns (bytes) {
    return abi.encode(1, 2);  // 计算set(uint x) 的ABI编码
    // return abi.encodePacked(1, 2);
    // return abi.encodeWithSignature("set(uint256)" ,1); //计算函数ABI编码
    // abi.encodeWithSelector(set.selector, x);
}
Loading...
Loading...

在Solidity中是无法直接判断字符串相等的,可以通过哈希值是否相等来判断。
参考

//以字符串的哈希值来判断是否相等
function isEqual(string memory a, string memory b) public view returns (bool) {
    //return a == b;
    // hash(a) == hash(b)  ==>  a == b?
    bytes32 hashA = keccak256(abi.encode(a));
    bytes32 hashB = keccak256(abi.encode(b));
    return hashA == hashB;
}

//eg 但是计算哈希值很费时,可以在这之前先判断下bytes是否相等
function compareStr (string _str1, string _str2) public returns(bool) {
  if(bytes(_str1).length == bytes(_str2).length){
    if(keccak256(abi.encodePacked(_str1)) == keccak256(abi.encodePacked(_str2))) {
        return true;
    }
  }
   return false;
}
Loading...
Loading...

txorigin.jpg

如上图所示,用户D通过合约B调用合约接口来调用合约A中的函数,这时tx.origin的穿透性就体现出来了。msg.sender只能得到直接调用它的地址(即合约B),得不到用户D的身份,只有tx.origin才能得到!所以,从这里可能看到,tx.origin具有的穿透的访问性。

Loading...

参考

struct Delegator {
    uint256 amount;     
    uint256 rewardDebt;    
    bool    hasDeposited;  
    string  hiveAccount;      
}
mapping(address => Delegator) public delegators;
address[] public delegatorList;

//一次获取所有的代理人
function getDelegators() external view returns(address[] memory delegatorLists){
  delegatorLists = delegatorList;
}

//一次获取所有代理人的结构体数据
function getDelegatorsInfo() external view returns (Delegator[] memory returnData){
    returnData = new Delegator[](delegatorList.length);
    
    for(uint256 i = 0; i < delegatorList.length; i ++){
        returnData[i] = delegators[delegatorList[i]];
    }
    return returnData;
}
Loading...
Loading...

教程 |
教程2 |
案例

可以使用assembly{}关键字开始编写Yul代码,它是一种简化且扩展了的汇编语言。通过使用assembly,我们可以直接访问堆栈,并优化代码以提高内存效率,从而减少执行交易所需的燃气量。这最终降低了用户的交易成本

contract StoringData {
  function setData(uint256 newValue) public {
    assembly {
      sstore(0, newValue)
    }
  }

  function getData() public view returns(uint256) {
    assembly {
      let v := sload(0)
      mstore(0x80, v)
      return(0x80, 32)
    }
  }
}

//send eth
success := call(gas(), _to, _amount, 0, 0, 0, 0)
Loading...

案例

  1. 最小化权限控制,尽量用external, private, internal, 尽量不用public
  2. 变量类型优先使用calldata而不是memory
  3. 加载状态变量到内存变量
  4. 多个条件判断使用短路方式
  5. 在循环中使用++i,而不是i+=1,i++
  6. 数组长度缓存到内存
  7. 缓存多次使用的数组元素到内存
There are 2 pages
Pages