Network Security Internet Technology Development Database Servers Mobile Phone Android Software Apple Software Computer Software News IT Information

In addition to Weibo, there is also WeChat

Please pay attention

WeChat public account

Shulou

What is the design pattern of Solidity

2025-03-26 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Internet Technology >

Share

Shulou(Shulou.com)06/01 Report--

This article introduces the relevant knowledge of "what is the design pattern of Solidity". In the operation of actual cases, many people will encounter such a dilemma, so let the editor lead you to learn how to deal with these situations. I hope you can read it carefully and be able to achieve something!

Overview of Intelligent contract Design pattern

In 2019, IEEE included a paper by the University of Vienna entitled "Design Patterns For Smart Contracts In the Ethereum Ecosystem". This paper analyzes those hot Solidity open source projects, combined with previous research results, sorted out 18 design patterns.

These design patterns cover many aspects such as security, maintainability, life cycle management, authentication and so on.

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 (Security)

The primary consideration of intelligent contract writing is security.

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

If there is a lack of defense, those "malicious" external code may destroy your contract. For example, external calls can break the contract state by causing code to be executed repeatedly through malicious callbacks, which is known as Reentrance Attack (replay attack).

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

With regard to replay attacks, here is a concise example.

The AddService contract is a simple counter. Each external contract can call the addByOne of the AddService contract to increment the field _ count by one, and use require to force each external contract to call the function at most once.

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

Contract AddService {uint private _ count; mapping (address= > bool) private _ adders; function addByOne () public {/ / forces that require can only be called once per address (_ ads [msg.sender] = = false, "You have added already"); / / count _ count++; / / call account callback function AdderInterface adder = AdderInterface (msg.sender); adder.notify () / / add the address to the called collection _ ads [msg.sender] = true;}} contract AdderInterface {function notify () public;}

If the AddService is so deployed, a malicious attacker can easily control the number of _ count in the AddService, invalidating the counter completely.

An attacker only needs to deploy a contract BadAdder to call AddService through it, 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 in the callback function notify, in turn, continues to call AddService. Due to the poor code design of AddService, the require conditional check statement is easily bypassed, and the attacker can hit the _ count field directly, causing it to be added repeatedly at will.

The sequence diagram of the attack process is as follows:

In this example, it is difficult for AddService to know the callback logic of the caller, but still trusts the external call, and the attacker takes advantage of AddService's poor coding, leading to tragedy.

The actual business meaning is removed in this example, and the result of the attack 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 beyond recognition.

It takes a good blacksmith to make good steel. If you want to shield such attacks, the contract needs to follow a good coding pattern. Here are two design patterns that can effectively eliminate such attacks.

Checks-Effects-Interaction-make sure the state is complete, and then make an external call

This mode is a coding style constraint and can effectively avoid replay attacks. In general, a function may contain three parts:

Checks: parameter verification

Effects: modify contract status

Interaction: external interaction

This model requires contracts to organize the code in the order of Checks-Effects-Interaction. Its advantage is that before making the external call, the Checks-Effects has completed all the work related to the state of the contract itself, making the state complete and logically self-consistent, so that the external call cannot take advantage of the incomplete state to attack.

Reviewing the previous AddService contract, we did not follow this rule and called the external code when its own status was not updated. The external code can naturally insert a knife horizontally so that the _ adders [msg.sender] = true will not be called forever, thus invalidating the require statement. We review the original code from the perspective of checks-effects-interaction:

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

As long as the order is adjusted slightly to meet the Checks-Effects-Interaction pattern, 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 ()

Because the _ adders mapping has been modified, the line of defense when a malicious attacker wants to call addByOne,require recursively will work to block malicious calls.

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

Mutex-No recursion

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

Contract Mutex {bool locked; modifier noReentrancy () {/ / prevent recursive 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, assigning the locked variable to true. If some is called recursively at this time, the logic of the modifier will be activated again, and since the locked property is now true, the first line of code of the modifier will throw an error.

Maintainability (Maintaince)

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

How to deal with the business data already in the contract?

How to minimize the scope of impact of the upgrade so that the remaining features are not affected?

What about other contracts that depend on it?

Reviewing object-oriented programming, the core idea of object-oriented programming is to separate the changed things from the immutable things in order to block the spread of change in the system. Therefore, well-designed code is usually organized with a high degree of modularity, high cohesion and low coupling. The above problems can be solved by using this classical idea.

Data segregation-data and logic are separated

Before you learn about 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), and the other is to use data for calculation (compute function). If the compute is found to be wrong after the contract has been deployed for a period of time, for example, it should not be multiplied by 10, but by 20, it will lead to the question of how to upgrade the contract.

At this point, you can deploy a new contract and try to migrate the existing data to the new contract, but this is a heavy operation. On the one hand, you have to write the code for the migration tool, on the other hand, the original data is completely invalidated, occupying valuable node storage resources.

Therefore, it is necessary to modularize in advance when programming. If we regard "data" as something immutable and "logic" as something that may change, we can perfectly avoid the above problems. The Data Segregation (which means data separation) pattern implements this idea very well.

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

Combined with the previous example, we specifically transferred the data read and write operations to a contract DataRepository:

Contract DataRepository {uint private _ data; function setData (uint data) public {_ data = data;} function getData () public view returns (uint) {return _ data;}}

The computing function is put into a separate 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 very light. For example, when I want to replace Computer with ComputerV2, the original data can still be reused.

Satellite-decompose contract function

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

The Satellite model uses the principle of single responsibility to solve the above problems, and advocates putting the sub-functions of the contract into the sub-contract, and each sub-contract (also known as the satellite contract) corresponds to only one function. When a sub-function needs to be modified, just create a new sub-contract and update its address to the main contract, and the rest of the function will not be affected.

To take a simple example, the setVariable function of the following contract is to calculate the input data (compute function) and store the calculation result in the contract status _ variable:

Contract Base {uint public _ variable; function setVariable (uint data) public {_ variable = compute (data);} / calculate function compute (uint a) internal returns (uint) {return a * 10;}}

If, after deployment, you find that the compute function is miswritten and you want to multiply by a factor of 20, you have to redeploy the entire contract. However, if you initially follow the Satellite mode, you only need to deploy the appropriate subcontracts.

First, let's split the compute function into a separate satellite contract:

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

The main contract then relies on the subcontract to complete the setVariable:

Contract Base {uint public _ variable; function setVariable (uint data) public {_ variable = _ satellite.compute (data);} Satellite _ satellite; / / update 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 master contract relies on a sub-contract, the master contract needs to update the address reference to the sub-contract when the sub-contract is upgraded, which is done through updateXXX, such as the updateSatellite function above.

This kind of interface is maintainable and has nothing to do with the actual business. Too much exposure of such interfaces will affect the beauty of the contract and greatly reduce the experience of the caller. The Contract Registry design pattern solves this problem gracefully.

In this design mode, there will be a dedicated contract Registry to track each upgrade of the subcontract, and the master contract can query the Registyr contract to get the latest subcontract address. After the satellite contract is redeployed, the new address is updated through the Registry.update function.

If the contract Registry {address _ current; address [] _ previous; / / subcontract is upgraded, update the address function update (address newAddress) public {if (newAddress! = _ current) {_ previous.push (_ current); _ current = newAddress;}} function getCurrent () public view returns (address) {return _ current;}} via the update function

The main 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 invokes the latest contract

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

Contract SatelliteProxy {address _ current; function compute (uint a) public returns (uint) {Satellite satellite = Satellite (_ current); return satellite.compute (a);} / / if the subcontract is upgraded, update the address function update (address newAddress) public {if (newAddress! = _ current) {_ current = newAddress through the update function Contract Satellite {function compute (uint a) public returns (uint) {return a * 10;}}

The main contract depends on SatelliteProxy:

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 block chain on which it depends is destroyed. But in many cases, users want to shorten the life cycle of the contract. This section will introduce two simple modes to combine life ahead of time.

Mortal-allow contracts to self-destruct

There is a selfdestruct instruction in the bytecode to destroy the contract. So you only need to expose the self-destruct interface:

Contract Mortal {/ / self-destruct function destroy () public {selfdestruct (msg.sender);}} Automatic Deprecation-allows contracts to automatically stop service

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

Contract AutoDeprecated {uint private _ deadline; function setDeadline (uint time) public {_ deadline = time;} modifier notExpired () {require (now 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 ecology of the intelligent contract on the chain is relatively closed, and the data outside the chain can not be obtained, which affects the scope of application of the intelligent contract.

Out-of-chain data can greatly expand the scope of use of smart contracts, such as in the insurance industry, where claims can be automatically settled if they can read accidents that actually occur.

Getting external data is performed through an out-of-chain data layer called Oracle. When the contract of the business side tries to obtain external data, it will first store the query request in an Oracle dedicated contract; Oracle will monitor the contract, read the query request, execute the query, and call the business contract response API to obtain the result of the contract.

An Oracle contract is defined below:

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, which is called by Oracle to 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, which is used to read the response function oracleResponse (bytes response) onlyByOracle {/ / use the data}} "what is the design pattern of Solidity?". Thank you for reading. If you want to know more about the industry, you can follow the website, the editor will output more high-quality practical articles for you!

Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.

Views: 0

*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.

Share To

Internet Technology

Wechat

© 2024 shulou.com SLNews company. All rights reserved.

12
Report