Design mode of Solidity written by smart contract | FISCO BCOS hypertext blockchain special session (Part 4)

Keywords: Blockchain Oracle Attribute Programming

Preface

With the development of blockchain technology, more and more enterprises and individuals begin to combine blockchain with their own business.

The unique advantages of blockchain, such as transparent and tamper proof data, can bring convenience to business. But at the same time, there are some hidden dangers. The data is open and transparent, which means that anyone can read it; it can't be tampered, which means that once the information is on the chain, it can't be deleted, even the contract code can't be changed.

In addition, every feature of the openness and callback mechanism of the contract can be used as a means of attack. A little carelessness will make the contract look like a virtual one, while the risk of the disclosure of the enterprise's secrets will be more serious. Therefore, before the business contract is put into the chain, the security and maintainability of the contract need to be fully considered in advance.

Fortunately, through a large number of solid language practices in recent years, developers continue to refine and summarize, and have formed some "design patterns" to guide and deal with common problems in daily development.

Overview of smart contract design mode

In 2019, IEEE included a paper entitled "Design Patterns For Smart Contracts In the Ethereum Ecosystem" from the University of Vienna. This paper analyzes those popular Solidity open source projects, and combines the previous research results to sort out 18 design patterns.

These design patterns cover security, maintainability, life cycle management, authentication and other aspects.

Next, this paper will introduce the most common design patterns from these 18 design patterns, which have been tested in the actual development experience.

Security

The primary consideration of smart contract writing is security.

In the blockchain world, there are countless malicious codes. If your contract contains cross contract calls, be very careful to make sure that the external calls are trusted, especially if the logic is not under your control.

If you don't have a sense of defense, your contract may be destroyed by "malicious" external code. For example, external calls can break the contract state by repeatedly executing the code through malicious callbacks. This attack technique is known as Reentrance Attack.

Here, we first introduce a small experiment of replay attack, so that readers can understand why external calls may cause the contract to be broken, and help them better understand the two design patterns that will be introduced to improve the security of the contract.

For replay attacks, here's a condensed example.

AddService contract is a simple counter. Each external contract can call addByOne of the AddService contract to add one to the field "count". At the same time, it is mandatory to call this function at most once for each external contract through require.

In this way, the "count" field accurately reflects how many contracts AddService has been called. At the end of the addByOne function, AddService calls the callback function notify for the external contract. The code of AddService is as follows:


contract AddService{

    uint private _count;
    mapping(address=>bool) private _adders;

    function addByOne() public {
        //Force that each address can only be called once
        require(_adders[msg.sender] == false, "You have added already");
        //count
        _count++;
        //Call the callback function of the account
        AdderInterface adder = AdderInterface(msg.sender);
        adder.notify();
        //Add address to called collection
        _adders[msg.sender] = true;   
    }
}

contract AdderInterface{
    function notify() public;  
}

If AddService is deployed in this way, a malicious attacker can easily control the number of ﹣ counts in AddService, making the counter completely invalid.

The attacker only needs to deploy a contract BadAdder, through which AddService can be called, and the attack effect can be achieved. The BadAdder contract is as follows:


contract BadAdder is AdderInterface{

    AddService private _addService = //...;
    uint private _calls;

    //Callback
    function notify() public{
        if(_calls > 5){
            return;
        }
        _calls++;
        //Attention !!!!!!
        _addService.addByOne();
    }

    function doAdd() public{
        _addService.addByOne();    
    }
}

BadAdder continues to call AddService in turn in the callback function notify. Due to the poor code design of AddService, the require d condition detection statement is easily bypassed, and the attacker can directly hit the "count" field, which can be added repeatedly arbitrarily.

The sequence diagram of attack process is as follows:

In this example, AddService is hard to know the callback logic of the caller, but it still believes in the external call, and the attacker takes advantage of AddService's poor code arrangement, leading to tragedy.

In this example, the actual business meaning is removed, and the attack result is only the distortion of the count value. A real replay attack can have serious consequences for the business. For example, in counting the number of votes, the number of votes will be changed to be totally different.

If you want to shield such attacks, the contract needs to follow a good coding mode. Here are two design modes that can effectively eliminate such attacks.

Checks effects interaction - make sure the status is complete before making external calls

This mode is coding style constraint, which can effectively avoid replay attack. In general, a function can have three parts:

  • Checks: parameter validation

  • Effects: modifying contract status

  • Interaction: external interaction

This pattern requires contracts to organize code in the order of checks effects interaction. Its advantage is that before the external call, checks effects has completed all the work related to the contract's own state, making the state complete and logical self consistent, so that the external call can not use the incomplete state to attack.

Looking back at the AddService contract mentioned above, we didn't follow this rule. We called the external code without updating its status. Naturally, the external code can be inserted horizontally so that "_adders[msg.sender]=true is never called, thus invalidating the require statement. We review the original code from the perspective of checks effects interaction:


    //Checks
    require(_adders[msg.sender] == false, "You have added already");
    //Effects    
    _count++;
    //Interaction    
    AdderInterface adder = AdderInterface(msg.sender);
    adder.notify();
    //Effects
    _adders[msg.sender] = true;


As long as the order is slightly adjusted to meet the checks effects interaction mode, the tragedy can be avoided:

    //Checks
    require(_adders[msg.sender] == false, "You have added already");
    //Effects    
    _count++;
    _adders[msg.sender] = true;
    //Interaction    
    AdderInterface adder = AdderInterface(msg.sender);
    adder.notify(); 

Since the "adders" mapping has been modified, when a malicious attacker wants to call addByOne recursively, the defense line of "require" will play a role in blocking the malicious call.

Although this mode is not the only way to solve replay attacks, it is recommended that developers follow it.

Mutex - prohibit recursion

Mutex mode is also an effective way to solve replay attack. It prevents the function from being called recursively by providing a simple modifier:


contract Mutex {
    bool locked;
    modifier noReentrancy() {
        //Prevent recursion
        require(!locked, "Reentrancy detected");
        locked = true;
        _;
        locked = false;
    }

    //Calling this function will throw a Reentrancy detected error
    function some() public noReentrancy{
        some();
    }
}

In this example, the noReentrancy modifier is run before calling the some function, and the locked variable is assigned to true. If someone is called recursively at this time, the logic of the modifier will be activated again. Because the locked attribute is true at this time, the first line of code of the modifier will throw an error.

Maintainability

In the blockchain, once the contract is deployed, it cannot be changed. When there is a bug in the contract, you usually have to face the following problems:

  1. How to deal with the existing business data on the contract?

  2. How to minimize the scope of impact of the upgrade so that other functions are not affected?

  3. What about other contracts that depend on it?

In retrospect, the core idea of object-oriented programming is to separate the changing things from the unchanging things, so as to block the spread of changes in the system. Therefore, well-designed code is usually highly modular, highly cohesive and low coupling. Using this classic idea can solve the above problems.

Data segregation - separation of data from logic

Before understanding the design pattern, take a look at the following contract code:


contract Computer{

    uint private _data;

    function setData(uint data) public {
        _data = data;
    }

    function compute() public view returns(uint){
        return _data * 10;
    }
}

This contract contains two capabilities, one is to store data (setData function), the other is to use data for calculation (compute function). If the contract is deployed for a period of time, and it is found that the calculation is wrong, for example, it should not be multiplied by 10, but by 20, it will lead to the problem of how to upgrade the contract.

At this time, you can deploy a new contract and try to migrate the existing data to the new contract, but this is a very heavy operation. On the one hand, you need to write the code of the migration tool, on the other hand, the original data is completely invalid, which takes up precious node storage resources.

Therefore, it is necessary to modularize in advance. If we regard "data" as unchangeable things and "logic" as things that may change, we can avoid the above problems perfectly. The Data Segregation pattern implements this idea well.

This model requires a business contract and a data contract: the data contract is only for data access, which is stable; and the business contract completes the logical operation through the data contract.

In combination with the previous examples, we specifically transfer the data read / write operation to a contract data repository:


contract DataRepository{

    uint private _data;

    function setData(uint data) public {
        _data = data;
    }

    function getData() public view returns(uint){
        return _data;
    }
}

The calculation function is put into a single business contract:


contract Computer{
    DataRepository private _dataRepository;
    constructor(address addr){
        _dataRepository =DataRepository(addr);
    }

    //Business code
    function compute() public view returns(uint){
        return _dataRepository.getData() * 10;
    }    
}

In this way, as long as the data contract is stable, the upgrade of the business contract is lightweight. For example, when I want to replace Computer with Computer V2, the original data can still be reused.

Satellite - contract decomposition function

A complex contract usually consists of many functions. If all these functions are coupled in a contract, when a function needs to be updated, the whole contract has to be deployed, and normal functions will be affected.

Satellite model uses the principle of single responsibility to solve the above problems, and advocates to put the sub contract functions into the sub contract. Each sub contract (also known as satellite contract) only corresponds to one function. When a sub function needs to be modified, as long as a new sub contract is created and its address is updated to the main contract, other functions will not be affected.

For a simple example, the setVariable function of the following contract is to calculate the input data (compute function), and store the calculation results in the contract state ﹐ variable:


contract Base {
    uint public _variable;

    function setVariable(uint data) public {
        _variable = compute(data);
    }

    //Calculation
    function compute(uint a) internal returns(uint){
        return a * 10;        
    }
}

If you find that the compute function is wrong after deployment and you want to multiply it by 20, you need to redeploy the entire contract. But if you start with Satellite mode, you only need to deploy the corresponding child contracts.

First, we split the compute function into a separate satellite contract:


contract Satellite {
    function compute(uint a) public returns(uint){
        return a * 10;        
    }
}

Then, the master contract relies on the child contract to complete setVariable:


contract Base {
    uint public _variable;

    function setVariable(uint data) public {
        _variable = _satellite.compute(data);
    }

     Satellite _satellite;
    //Renew sub contract (satellite contract)
    function updateSatellite(address addr) public {
        _satellite = Satellite(addr);
    }
}

In this way, when we need to modify the compute function, we just need to deploy such a new contract and pass its address to Base.updateSatellite:


contract Satellite2{
    function compute(uint a) public returns(uint){
        return a * 20;        
    }    
}

Contract Registry - track the latest contracts

In Satellite mode, if a main contract depends on a sub contract, when upgrading the sub contract, the address reference of the main contract to the sub contract needs to be updated. This is done through updateXXX, such as the updateSatellite function earlier.

This kind of interface belongs to the maintenance interface, which has nothing to do with the actual business. Too much exposure to this kind of interface will affect the beauty of the main contract and greatly reduce the experience of the caller. The Contract Registry design pattern gracefully solves this problem.

In this design mode, there will be a special contract Registry to track each upgrade of the sub contract. The master contract can obtain the latest sub contract address by querying this Registyr contract. After the satellite contract is redeployed, the new address is updated through the Registry.update function.


contract Registry{

    address _current;
    address[] _previous;

    //When the sub contract is upgraded, the address is updated through the update function
    function update(address newAddress) public{
        if(newAddress != _current){
            _previous.push(_current);
            _current = newAddress;
        }
    } 

    function getCurrent() public view returns(address){
        return _current;
    }
}

The master contract relies on Registry to get the latest satellite contract address.


contract Base {
    uint public _variable;

    function setVariable(uint data) public {
        Satellite satellite = Satellite(_registry.getCurrent());
        _variable = satellite.compute(data);
    }

    Registry private _registry = //...;
}

Contract Relay - agent calls the latest contract

This design pattern solves the same problem as Contract Registry, that is, the main contract can call the latest sub contract without exposing the maintenance interface. In this mode, there is a proxy contract, which has the same interface with the sub contract and is responsible for passing the call request of the main contract to the real sub contract. After the satellite contract is redeployed, the new address is updated through the satellite proxy.update function.


contract SatelliteProxy{
    address _current;
    function compute(uint a) public returns(uint){
        Satellite satellite = Satellite(_current);   
        return satellite.compute(a);
    } 
    
    //When the sub contract is upgraded, the address is updated through the update function
    function update(address newAddress) public{
        if(newAddress != _current){
            _current = newAddress;
        }
    }   
}


contract Satellite {
    function compute(uint a) public returns(uint){
        return a * 10;        
    }
}

The master contract depends on satellite proxy:


contract Base {
    uint public _variable;

    function setVariable(uint data) public {
        _variable = _proxy.compute(data);
    }
    SatelliteProxy private _proxy = //...;
}

Lifecycle (lifecycle)

By default, the life cycle of a contract is almost infinite - unless the blockchain on which it depends is eliminated. But many times, users want to shorten the contract life cycle. In this section, we will introduce two simple models to terminate the contract life in advance.

Mortal - allow contract to self destruct

There is a selfdestruct instruction in the bytecode to destroy the contract. So just expose the self destruct interface:


contract Mortal{

    //Self destruction
    function destroy() public{
        selfdestruct(msg.sender);
    } 
}

Automatic reservation - allow the contract to automatically stop the service

If you want a contract to stop service after a specified period of time without human intervention, you can use automatic reservation mode.


contract AutoDeprecated{

    uint private _deadline;

    function setDeadline(uint time) public {
        _deadline = time;
    }

    modifier notExpired(){
        require(now <= _deadline);
        _;
    }

    function service() public notExpired{ 
        //some code    
    } 
}

When the user calls the service, the notExpired modifier will perform date detection first, so that once a specific time has passed, the call will be blocked in the notExpired layer due to expiration.

Authorization

There are many management interfaces in the previous article. If anyone can call these interfaces, it will cause serious consequences. For example, the self destruct function in the above article, assuming that anyone can access it, its severity is self-evident. Therefore, a set of rights control design mode which can ensure that only a specific account can access is particularly important.

Ownership

For permission control, the owner mode can be used. This pattern ensures that only the owner of the contract can call certain functions. First there needs to be an Owned Contract:


contract Owned{

    address public _owner;

    constructor() {
        _owner = msg.sender;
    }    

    modifier onlyOwner(){
        require(_owner == msg.sender);
        _;
    }
}

What if a business contract wants a function to be called only by the owner? As follows:


contract Biz is Owned{
    function manage() public onlyOwner{
    }
}

In this way, when the manage function is called, the onlyOwner modifier will run first and check whether the caller is consistent with the contract owner, thus blocking unauthorized calls.

Action And Control

This kind of pattern is generally used for specific scenarios. In this section, we will mainly introduce the privacy based coding pattern and the design pattern of interaction with data outside the chain.

Commit - Reveal - delay secret disclosure

The data on the chain is open and transparent. Once some private data is on the chain, it can be seen by anyone, and can no longer be withdrawn.

The Commit And Reveal mode allows users to convert the data to be protected into unrecognized data, such as a string of hash values, until a certain time to reveal the meaning of the hash value and reveal the real original value.

Taking the voting scenario as an example, it is assumed that the voting content needs to be disclosed after all participants have finished voting, so as to prevent participants from being affected by the number of votes during this period. We can see the specific code used in this scenario:


contract CommitReveal {

    struct Commit {
        string choice; 
        string secret; 
        uint status;
    }

    mapping(address => mapping(bytes32 => Commit)) public userCommits;
    event LogCommit(bytes32, address);
    event LogReveal(bytes32, address, string, string);

    function commit(bytes32 commit) public {
        Commit storage userCommit = userCommits[msg.sender][commit];
        require(userCommit.status == 0);
        userCommit.status = 1; // comitted
        emit LogCommit(commit, msg.sender);
    }

    function reveal(string choice, string secret, bytes32 commit) public {
        Commit storage userCommit = userCommits[msg.sender][commit];
        require(userCommit.status == 1);
        require(commit == keccak256(choice, secret));
        userCommit.choice = choice;
        userCommit.secret = secret;
        userCommit.status = 2;
        emit LogReveal(commit, msg.sender, choice, secret);
    }
}

Oracle - read out of chain data

At present, the ecosystem of smart contracts on the chain is relatively closed, and the data outside the chain cannot be obtained, which affects the application scope of smart contracts.

For example, in the insurance industry, if the smart contract can read the real unexpected events, it can automatically perform claims settlement.

Getting external data is performed through an out of chain data layer called oracle. When the contract of the business party attempts to obtain external data, it will first store the query request into an Oracle special contract; Oracle will listen to the contract, read the query request, execute the query, and call the business contract response interface to get the result of the contract.

An Oracle contract is defined as follows:


contract Oracle {
    address oracleSource = 0x123; // known source

    struct Request {
        bytes data;
        function(bytes memory) external callback;
}

    Request[] requests;
    event NewRequest(uint);
    modifier onlyByOracle() {
        require(msg.sender == oracleSource); _;
    }

    function query(bytes data, function(bytes memory) external callback) public {
        requests.push(Request(data, callback));
        emit NewRequest(requests.length - 1);
    }

    //Callback function, called by Oracle
    function reply(uint requestID, bytes response) public onlyByOracle() {
        requests[requestID].callback(response);
    }
}

Business side contracts interact with Oracle contracts:


contract BizContract {
    Oracle _oracle;

    constructor(address oracle){
        _oracle = Oracle(oracle);
    }

    modifier onlyByOracle() {
        require(msg.sender == address(_oracle)); 
        _;
    }

    function updateExchangeRate() {
        _oracle.query("USD", this.oracleResponse);
    }

    //Callback function to read the response
    function oracleResponse(bytes response) onlyByOracle {
    // use the data
    }
}

Total node

The introduction of this paper covers Security, maintainability and other design patterns, some of which are partial principles, such as Security and maintenance design patterns; some are partial practices, such as authorization, Action And Control.

These design patterns, especially practice classes, do not cover all scenarios. With the in-depth exploration of the actual business, more and more specific scenarios and problems will be encountered. Developers can refine and sublimate these patterns to precipitate design patterns for certain problems.

The above design patterns are powerful weapons for programmers. Mastering them can deal with many known scenarios, but more importantly, they should master the method of refining design patterns, so that they can calmly deal with unknown fields. This process cannot be separated from the in-depth exploration of business and the in-depth understanding of software engineering principles.

FISCO BCOS code is completely open source and free of charge

Download address

https://github.com/FISCO-BCOS/FISCO-BCOS

Posted by little_webspinner on Sun, 19 Apr 2020 04:50:44 -0700