Skip to content

Solidity8高级

合约部署

通过合约部署合约。

JavaScript
// SPDX-License-Identifier:MIT

  

pragma solidity 0.8.7;

  

contract TestContract1 {

    address public owner = msg.sender;

  

    function setOwner(address _owner) public {

        require(msg.sender == owner, "not owner");

        owner = _owner;

    }

}

  

contract TestContract2 {

    address public owner = msg.sender;

    uint public value = msg.value;

    uint public x;

    uint public y;

  

    constructor(uint _x, uint _y) payable {

        x = _x;

        y = _y;

    }

}

  

contract Proxy {

    function deploy(bytes memory _code) external payable {

        new TestContract1();

    }

}

但是我们希望直接通过在_code处输入机器码就可以直接部署合约,接下来实现一个新的代理合约:

JavaScript
// SPDX-License-Identifier:MIT

  

pragma solidity 0.8.7;

  

contract TestContract1 {

    address public owner = msg.sender;

  

    function setOwner(address _owner) public {

        require(msg.sender == owner, "not owner");

        owner = _owner;

    }

}

  

contract TestContract2 {

    address public owner = msg.sender;

    uint public value = msg.value;

    uint public x;

    uint public y;

  

    constructor(uint _x, uint _y) payable {

        x = _x;

        y = _y;

    }

}

  

contract Proxy {

    event Deploy(address);

  

    function deploy(bytes memory _code) external payable returns (address addr) {

        // 添加内联汇编

        assembly {

            // create(v, p, n)

            // v = amount of ETH to send

            // p = pointer in memory o start of code

            // n = size of code

             addr := create(callvalue(), add(_code, 0x20), mload(_code))  // 隐式返回

            // 这里用callvalue()替换平常使用的msg.sender来获取这次消息发送的主例

            // add(_code, 0x20)获取pointer,mload(_code)获取大小

        }

        // 返回的地址使零地址,说明这次合约失败

        require(addr != address(0), "deploy failed");

  

        // 之后触发这个事件,这样我们就可以在交易中看到这个地址是多少了

        emit Deploy(addr);

    }

    // 使代理合约将这个地址改为我们的

    function execute(address _target, bytes memory _data) external payable {

        (bool success, ) = _target.call{value: msg.value}(_data);

        require(success, "failed");

    }

}

// 获取机器码_code

contract Helper {

    function getBytecode1() external pure returns (bytes memory) {

        bytes memory bytecode = type(TestContract1).creationCode;

        return bytecode;

    }

    // 测试合约2它有构造函数,有参数,所以不能直接像上述操作一样,

    function getBytecode2(uint _x, uint _y) external pure returns (bytes memory) {

        bytes memory bytecode = type(TestContract2).creationCode;

        // 所以我们将xy通过打包的形式连接在其之后形成新的bytecode这样就能形成新的bytecode

        return abi.encodePacked(bytecode, abi.encode(_x, _y));

    }

    // 这是设置管理员那个方法的bytecode

    function getCalldata(address _owner) external pure returns (bytes memory) {

        return abi.encodeWithSignature("setOwner(address)" _owner);

    }

}

存储位置

  • storage:使用该定义修改后,状态变量的值就直接发生了修改
  • memory:使用该定义修改后,是处于局部变量的位置,函数调用结束后就消失了,并不会对状态变量产生影响。
  • calldata:和memory类似,但只能用在输入的参数中,相比于memroy参数,calldata在多函数传递参数时,可以直接传递,不需要像memory一样复制,从而可以节省gas。 当参数或返回值是数组、字符串、结构体这类的,都需要加上memory关键字 简单存储例子:
JavaScript
// SPDX-License-Identifier:MIT

  

pragma solidity 0.8.7;

contract SimpleStorage {

    string public text;

  

    // aaaaaaaaaaaaaa...

    // calldata 89626 gas

    // memory 90114 gas

    function set(string calldata _text) external {

        text = _text;  // 修改状态变量

    }

    // 相当于智能合约将状态变量拷贝到了内存中然后返回回来

    function get() external view returns (string memory) {

        return text;

    }

}

阶段例子--TodoList

JavaScript
// SPDX-License-Identifier:MIT

  

pragma solidity 0.8.7;

  

// insert, update, read  from array of structs

contract TodoList {

    struct Todo {

        string text;

        bool completed;

    }

  

    Todo[] public todos;

  

    function create(string calldata _text) external {

        todos.push(Todo({

            text: _text,

            completed: false

        }));

    }

    // 根据索引进行更新

    function updateText() external {

        // method1: 如果只更新其中一部分内容,这种方法会更节约gas

        todos[_index].text = _text;

  

        // method2:先装入到storage中,再进行更新。这种方法对于更新全部数据会更节约gas

        Todo storage todo = todos[_index];

        todo.text = _text;

        // other data...

    }

  

    function get(uint _index) external view returns (string memory, bool) {

        Todo memory todo = todos[_index];

        return (todo.text, todo.completed);

    }

  

    function toggleCompleted(uint _index) external {

        // 改变完成状态,反转即可。

        todos[_index].completed = !todos[_index].completed;

    }

}

事件

事件是一种记录当前智能合约运行状态的方法,但是它并不会记录在状态变量中,而是会体现在区块浏览器上,或是出现在交易记录中的Log中。

JavaScript
// SPDX-License-Identifier:MIT

  

pragma solidity 0.8.7;

  

contract Event {

    event Log(string message. uint val);

    // 注意:就是在一个事件中,可以让他汇报很多很多变量,但是在索引总,最多只能有三个变量。

    event IndexedLog(address indexed sender, uint val);

  

    function example() external {

        // 调用这个函数就会触发这个事件,这个事件就会记录在交易记录中的Logs里,也会体现在区块链浏览器上

        emit Log("Foo", 1234);

        emit IndexedLog(msg.sender, 789);

        // 然后我们在链外用web3或者其他就可以查出该地址所有的事件  

    }

    // 索引的三个变量例子

    event Message(address indexed _from, address indexed _to, string message);

    function sendMessage(address _to, string calldata message) external{

        // 然后同上,写个函数触发这个事件

        emit Message(msg.sender, _to, message);

    }

}

继承

基础

  • 在继承前,首先要用virtual标记哪些函数是可以被重写的
  • 然后后面来进行重写操作的函数也要通过override标记函数来证明它是覆盖调之前的函数
JavaScript
// SPDX-License-Identifier:MIT

  

pragma solidity 0.8.7;

  

contract A {

    function foo() public pure virtual returns (string memory) {

        return "A";

    }

  

    function bar() public pure virtual returns (string memory) {

        return "A";

    }

  

    // more code here

    function baz() public pure returns (string memory) {

        return "A";

    }

}

  

contract B is A {

    function foo() public pure override returns (string memory) {

        return "B";

    }

  

    function bar() public pure override returns (string memory) {

        return "B";

    }

  

    // more code here

}

多重继承:

原则:我们需要把继承最少,越基础的合约放在最前面,按层数从上往下来,不然编译的时候就会报错!

JavaScript
contract X{//...}
contract Y is X{//...}
contract Z is X,Y{
	function foo() public pure override(X, Y) returns (string memory) {//...}
}

注意:这里第三行就需要先写X,再写Y。

运行父合约中构造函数

JavaScript
// SPDX-License-Identifier:MIT

  

pragma solidity 0.8.7;

  

contract S{

    string public name;

  

    constructor(string memory _name) {

        name = _name;

    }

}

  

contract T {

    string public text;

  

    constructor(string memory _text) {

        text = _text;

    }

}

  

// method1:在已知构造函数所需参数的时候可以使用这种方法

contract U is S("s"), T("t") {

  

}

// method2:这样可以在部署的时候由调用者自己使用

contract V is S, T {

    constructor(string memory _name, string memory _text) S(_name) T(_text) {

  

    }

}

// 混合使用:一个继承的时候传,一个部署的时候传

contract UV is S("s"), T {

    constructor(string memory _text) T(_text) {

  

    }

}

构造函数初始化的运行顺序:会按照继承的顺序进行初始化,即使改变了构造函数上的S(_name) T(_text)的顺序也不会影响,始终是按继承的顺序进行初始化。

JavaScript
// Order of execution

// 1. T

// 2. S

// 3. V3

contract V3 is T, S {

    constructor(string memory _name. string memory _text) S(_name) T(_text) {}

}

调用父级合约函数

JavaScript
// SPDX-License-Identifier:MIT

  

pragma solidity 0.8.7;

  

contract E {

    event Log(string message);

    function foo() public virtual {

        emit Log("E.foo");

    }

  

    function bar() public virtual {

        emit Log("E.bar");

    }

}

// 调用父级合约的函数

contract F is E {

    function foo() public virtual override {

        emit Log("F.foo");

        // method1:直接调用

        E.foo();

    }

  

    function bar() public virtual override {

        emit Log("F.bar");

        // method2:使用super,这个关键字会自动寻找父级合约

        super.bar();  // 如果是多个父级合约都含有该方法,会栈式调用,并且调用且只调用一次所有相关合约

    }

}

可视范围

  • private:合约的内部可见
  • internal:合约内部和继承其的子合约
  • public:公开
  • external:只有外部的合约可见

不可变量

有些常量的值在部署合约之前是不知道的。 关键词:immutable 也会节约gas

JavaScript
// SPDX-License-Identifier:MIT

  

pragma solidity 0.8.7;

  
  

contract Immutable {

    // 45718 gas

    // address public owner = msg.sender;

  

    // 43585 gas

    address public immutable owner;

  

    constructor() {

        owner = msg.sender;

    }

  

    uint public x;

    function foo() external {

        require(msg.sender == owner);

        x += 1;

    }

}

支付Eth

如果你在函数中标记一个payable关键词,你就可以接收以太坊铸币的参数。

JavaScript
// SPDX-License-Identifier:MIT

  

pragma solidity 0.8.7;

  

contract Payable {

    // 标记地址变量,然后这个地址就可以发送以太坊铸币了

    address payable public owner;

  

    constructor() {

        owner = payable(msg.sender);  // 这里因为之前定义的owner具有payable关键字,所以这里也需要加上,否则报错!

    }

  

    function deposit() external payable{}  // 这里可以接收铸币了,别人发的会存在当前合约地址上

  

    function getBalance() external view returns (uint) {

        return address(this).balance;

    }

}

回退函数

功能:

  • 当你调用函数在合约中不存在的时候
  • 向合约中直接发送以太坊铸币的时候
JavaScript
// SPDX-License-Identifier:MIT

  

pragma solidity 0.8.7;

  

contract Fallback {

    // 当你调用合约中不存在的函数的时候,就会转入到回退函数中,触发里面的逻辑。

    // 而要接收铸币的发送,还需要加上payable

    fallback() external payable {}

    // solidity8.0进行了细化,对铸币的回退单独出了一个函数

    receive() external payable {}  // 有这个函数,发送铸币回退时就只进入该函数,没有这个函数,就会进入fallback函数
    // 同时receive函数时不接收任何数据的

}

发送Eth

  • transfer -- 2300gas,reverts(失败)
  • send -- 2300gas,return bool
  • call -- all gas,returns bool and data
JavaScript
// SPDX-License-Identifier:MIT

  

pragma solidity 0.8.7;

  

contract SendEther {

    constructor() payable {

        function sendViaTransfer(address payable _to) external payable {

            _to.transfer(123);

        }

        // 发送铸币的时候,只带2300gas,如果gas被消耗完,或者是发送的时候对方拒收等其他原因,就会报出异常revert

        function sendViaSend(address payable _to) external payable {

            _to.transfer(123);  // 123wei+2300gas

        }

        // 遇到错误情况并不会报出异常,而是返回一个布尔值

        function sendViaSend(address payable _to) external payable {

            bool sent = _to.send(123);

            // 确认

            require(sent, "send failed");

        }

  

        function sendViaCall(address payable _to) external payable {

            // 两个返回值,1:是否成功;2:bytes memory data:如果这次调用遇到的是智能合约,它有可能返回一个data数据

           (bool success, ) =  _to.call{value: 123}("");

           require(success, "call failed");

        }

    }

}

阶段例子--钱包合约

通过这个钱包,我们可以向合约中存入一定数量的以太坊铸币,并且可以随时从我们的合约中取出我们的以太坊铸币。 我们还需要规定管理员的身份。

JavaScript
// SPDX-License-Identifier:MIT

  

pragma solidity 0.8.7;

  

contract EtherWallet {

    address payable public owner;

  

    constructor() {

        owner = payable(msg.sender);

    }

  

    receive() external payable {}

  

    // 合约的拥有者可以随时取出里面的铸币,发送当然不需要了,谁都可以发送铸币给该合约。

    function withdraw(uint _amount) external {

        require(msg.sender == owner, "caller is not owner");

        // owner.transfer(_amount);

        // 节约gas可以这样写,因为owner是从状态变量中读取出来的

        payable(msg.sender).transfer(_amount);

    }

  

    function getBalance() external view returns (uint) {

        return address(this).balance;

    }

}

调用其他合约

JavaScript
// SPDX-License-Identifier:MIT

  

pragma solidity 0.8.7;

  

contract CallTestContract {

    // method1

    function setX1(address _test, uint _x) external {

        // 只需要把另一个合约当作类型,然后传入其地址,就可以调用其方法了。

        TestContract(_test).setX(_x);

    }

    // method2

    function setX2(TestContract _test, uint _x) external {

        _test.setX(_x);

    }

    // 更复杂的情况

    function setXandSendEther(address _test, uint _x) external payable {

        TestContract(_test).setXandReceiveEther{value: msg.value}(_x);

    }

}

  

contract TestContract {

    uint public x;

    uint public value = 123;

  

    function setX(uint _x) external {

        x = _x;

    }

  

    function getX() external view returns (uint) {

        return x;

    }

  

    function setXandReceiveEther(uint _x) external payable {

        x = _x;

        value = msg.value;

    }

  

    function getXandValue() external view returns (uint, uint) {

        return (x, value);

    }

}

接口合约

JavaScript
// SPDX-License-Identifier:MIT

  

pragma solidity 0.8.7;

  

interface ICounter {

    function count() external view returns (uint);

    function inc() external;

}

  

contract CallInterface {

    uint public count;

  

    function examples(address _counter) external {

        ICounter(_counter).inc();

        count = ICounter(_counter).count();

    }

}

// 然后你这里就可以部署了

// 其他地方实现对应的Counter合约

// 然后通过地址调用就可以了

低级call

JavaScript
// SPDX-License-Identifier:MIT

  

pragma solidity 0.8.7;

  

contract TestCall {

    string public message;

    uint public x;

  

    event Log(string message);

  

    fallback() external payable {

        emit Log("fallback was called");

    }

  

    function foo(string memory _message, uint _x) external payable returns (bool, uint) {

        message = _message;

        x = _x;

        return (true, 999);

    }

}

// 去调用上面的合约

contract Call {

    bytes public data;

  

    function callFoo(address _test) external payable{

        // 传入的是abi编码

        (bool success, bytes memory data)_test.call{value: 111, gas: 5000}(

            abi.encodeWithSignature(

                "foo(string, uint256)", "call foo", 123

            )

        );

        require(success, "call failed");

        data = _data;

    }

  

    function callDoesNotExit(address _test) external {

        (bool success, ) = _test.call(abi.encodeWithSignature("doesNotExist()"));

        require(success, "call failed");

    }

}

委托调用

C是委托调用的B,所以值和铸币都是保存在B的,C可以看到但不可以修改,同时看到的也是A的。

JavaScript
// SPDX-License-Identifier:MIT

  

pragma solidity 0.8.7;

  

contract TestDelegateCall {

    uint public num;

    address public sender;

    uint public value;

  

    function setVars(uint _num) external payable {

        num = _num;

        sender = msg.sender;

        value = msg.value;

    }

}

  

contract DelegateCall {

    uint public num;

    address public sender;

    uint public value;

  

    function setVars(address _test, uint _num) external payable {

        // 这里进行委托调用

        // _test.delegatecall(

        //     abi.encodeWithSignature("setVars(uint56)", _num)

        // );

        // 上述写法是使用签名进行编码

        // 另一种写法,这里使用Select进行编码

        (bool success, bytes memory data) = _test.delegatecall(

            abi.encodeWithSelect(TestDelegateCall.setVars.selector, _num);

        );

        require(success, "delegatecall failed");

    }

}
  • 就是可以让我们得调用委托到下一个合约中,底层相当于把被调用合约代码拿到调用合约中使用,类似调用库合约。
  • 被调用的合约的值不能被改变,我们只是使用被调用合约的逻辑来改变当前合约的状态变量的值
  • 虽然不用被调用合约的状态变量值,但必须要设置,因为要使他们的变量结构是一样的。

工厂合约

用合约部署合约(之前讲过一种通过内联汇编部署新合约的方法) 通过new语句新建合约的方法

JavaScript
// SPDX-License-Identifier:MIT

  

pragma solidity 0.8.7;

  

contract Account {

    address public bank;

    address public owner;

  

    constructor(address _owner) payable {

        bank = msg.sender;

        owner = _owner;

    }

}

  

contract AccountFactory {

    Account[] public accounts;  // 用一个数组去记录所有创建过的账户合约

  

    function createAccount(address _owner) external payable{

        // 返回的是账户合约的地址,可以用账户合约类型去接收它。

        Account account = new Account{value: 111}(_owner);  // 创建的时候也是可以接收铸币的,这里同样可以使用大括号进行接收

        accounts.push(account);

    }

}

库合约

我们可以把常用的算法抽象成为库合约。

JavaScript
// SPDX-License-Identifier:MIT

  

pragma solidity 0.8.7;

  

library Math {

    // 这里得定义为内部可视,因为库合约一般都是在合约内部使用的,定义为外部可视是没有任何意义的

    function max(uint x, uint y) internal pure returns (uint) {

        return x >= y ? x : y;

    }

}

  

contract Test {

    function testMax(uint x, uint y) external pure returns (uint) {

        return Math.max(x, y);

    }

}

  

library ArrayLib {

    function find(uint[] storage arr, uint x) internal view returns (uint) {

        for (uint i = 0; i < arr.length; i++) {

            if(arr[i] == x) {

                return i;

            }

        }

        revert("not found");

    }

}

  

contract TestArray {

    using ArrayLib for uint[]; // 使这个类型具有这个库的所有方法;

    uint[] public arr = [3,2,1];

  

    function testFind() external view returns (uint i) {

        // return ArrayLib.find(arr, 2);

        return arr.find(2);

    }

}

哈希运算

JavaScript
// SPDX-License-Identifier:MIT

  

pragma solidity 0.8.7;

  

contract HashFunc {

    function hash(string memory text, uint num, address addr) external pure returns (bytes32) {

        // 哈希算法的一种特定内部函数,传入这三个参数前先进行打包,打包之后会形成一个bytes的返回值,这个返回值是不定长的,然后再通过哈希

        return keccak256(abi.encodePacked(text, num, addr));

  

        // 测试encode与encodePacked的区别

        function encode(string memory text0, string memory text1) external pure returns (memory bytes){  // 这里不定长的需要加上memory

            return abi.encode(text0. text1);

        }

  

        function encodePacked(string memory text0, string memory text1) external pure returns (memory bytes){

            return abi.encodePacked(text0, text1);

        }

    }

}
  • encode:进行了补零
  • encodePacked:直接返回得16进制

不进行补零会容易出现一些错误,比如("AAAA","BBB")和("AAA","ABBB")的encodePacked返回值是没有任何变化的。这样就有可能造成哈希错误(碰撞) 所以以后在需要哈希的时候尽量使用encode进行打包

验证签名

  1. 消息签名
  2. 消息进行哈希
  3. 再把私钥和哈希后的消息进行签名,在链下完成
  4. 恢复签名
JavaScript
// SPDX-License-Identifier:MIT

  

pragma solidity 0.8.7;

  

contract VerifySig {

    // 最后我们要验证恢复后的地址和签名人的地址是否是相等的

    function vertify(address _signer, string memory _message, bytes memory _sig)

        external pure returns (bool)

    {

        bytes32 messageHash = getMessageHash(_message);  // 1.

        bytes32 ethSignedMessageHash = getEthSinedMessageHash(messageHash);  // 2.

        // 3.4.

        return recover(ethSignedMessageHash, _sig) == _signer;

    }

    function getMessageHash(string memory _message) public pure returns (bytes32) {

        return keccak256(abi.encodePacked(_message));

    }

  

    function getEthSinedMessageHash(bytes32 _messageHash) public pure returns (bytes32) {

        return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", _messageHash));

    }

  

    function recover(bytes32 _ethSignedMessageHash, bytes memory _sig)

        public pure returns (address)

    {

        (bytes32 r, bytes32 s, uint8 v) = _split(_sig);

        return ecrecover(_ethSignedMessageHash, v, r, s);

    }

  

    function _split(bytes memory _sig) internal pure returns (bytes32 r, bytes32 s, uint8 v){

        require(_sig.length == 65, "invalid signature length");

  

        assembly {

            r := mload(add(_sig, 32))

            s := mload(add(_sig, 64))

            v := byte(0, mload(add(_sig, 96)))

        }

    }

}

阶段例子--权限控制合约

JavaScript
// SPDX-License-Identifier:MIT

  

pragma solidity 0.8.7;

  

contract AccessControl {

    event GrantRole(bytes32 indexed role, address indexed account);

    event RevokeRole(bytes32 indexed role, address indexed account);

  

    // role => account => bool

    mapping(bytes32 => mapping(address => bool)) public roles;

    // 我们采用哈希值做名称是因为string比bytes32消耗的gas要多得多

    bytes32 private constant ADMIN = keccak256(abi.encodePacked("ADMIN"));

    bytes32 private constant USER = keccak256(abi.encodePacked("USER"));

  

    modifier onlyRole(bytes32 _role) {

        require(roles[_role][msg.sender]. "not authorized");

        _;

    }

  

    // 初始时,将管理员权限赋给合约的部署者

    constructor() {

        _grantRole(ADMIN, msg.sender);

    }

  

    // 升级角色的函数,内部的不做权限检查

    function _grantRole(bytes32 _role, address _account) internal {

        roles[_role][_account] = true;  // 这里我们修改了状态变量的值,按照智能合约的编写习惯来说,我们修改值后就一定要报出一个事件

        emit GrantRole(_role, _account);

    }

    // 外部的只能由管理员才能调用,所以这里设计并加上函数修改器

    function grantrole(bytes32 _role, address _account) external onlyRole(ADMIN) {

        _grantRole(_role, _account);

    }

    // 撤销权限的函数

    // ... -->false

}

合约自毁

selfdestruct删除合约,强制发送铸币到一个地址。

JavaScript
// SPDX-License-Identifier:MIT

  

pragma solidity 0.8.7;

  

contract Kill {

    constructor() payable {}

    function kill() external {

        // 强制删除,就算对方合约拒绝接受,也会收到

        selfdesctruct(payable(msg.sender));

    }

  

    function testCall() external pure returns (uint) {

        return 123;

    }

}

阶段例子--小猪存钱罐

可以通过任何人地址发送以太坊铸币,存钱罐的拥有者才可以取出,取出之后,它就会像真正的小猪存钱罐一样,会被打碎,就是自毁掉。

JavaScript
// SPDX-License-Identifier:MIT

  

pragma solidity 0.8.7;

  

contract PiggyBank {

    event Deposit(uint amount);

    event Withdraw(uint amount);

  

    address public owner = msg.sender;

  

    receive() external payable {

        emit Deposit(msg.value);

    }

  

    function withdraw() external {

        require(msg.sender == owner, "not owner");

        emit Withdraw(address(this).balance);

        selfdesctruct(payable(msg.sender));

    }

}