Smart Contract Design Patterns

Design patterns are the preferred solution in many development scenarios. This article will introduce five classic smart contract design patterns and give the Ethereum solidity implementation code: self-destructing contract, factory contract, name registry, mapping table iterator, and withdrawal model.

1. Self-destructing contract

The contract self-destruct mode is used to terminate a contract, which means that the contract will be permanently deleted from the blockchain. Once destroyed, it is impossible to call the function of the contract and no transaction is recorded in the ledger.

The question now is: "Why do I destroy the contract?".

There are many reasons, such as certain timed contracts, or those that must terminate once a milestone is reached. A typical case is a loan contract, which should be automatically destroyed after the loan is paid off; another case is a time-based auction contract, which should terminate after the auction - assuming we don't need to keep the auction history on-chain.

When dealing with a destroyed contract, there are some issues to be aware of:

  • After the contract is destroyed, transactions sent to that contract will fail
  • Any funds sent to the destroyed contract will be lost forever

To avoid loss of funds, you should ensure that the target contract still exists and remove all references to destroyed contracts before sending funds. Now let's look at the code:

contract SelfDesctructionContract {
   public address owner;
   public string someValue;
   modifier ownerRestricted {
      require(owner == msg.sender);
      _;
   } 
   // constructor
   function SelfDesctructionContract() {
      owner = msg.sender;
   }
   // a simple setter function
   function setSomeValue(string value){
      someValue = value;
   } 
   // you can call it anything you want
   function destroyContract() ownerRestricted {
     suicide(owner);
   }
}

As you can see, the destroyContract()method is responsible for destroying the contract.

Note that we use a custom ownerRestrictedmodifier to reveal the caller of the method, i.e. only the owner of the contract is allowed to destroy the contract.

2. Factory contract

Factory contracts are used to create and deploy "child" contracts. These subcontracts can be called "assets" and can represent real-life houses or cars.

The factory is used to store the address of the subcontract to be used when necessary. You might ask, why not store them in the web application database? This is because storing these address data in the factory contract means that it is stored on the blockchain, so it is more secure, and the damage to the database may cause the loss of asset addresses, resulting in the loss of references to these asset contracts. In addition to this, you also need to keep track of all newly created subcontracts in order to update the database synchronously.

A common use case for factory contracts is to sell assets and keep track of those assets (eg, who owns the assets). The payable modifier needs to be added to the function responsible for deploying the asset in order to sell the asset. code show as below:

contract CarShop {
   address[] carAssets;
   function createChildContract(string brand, string model) public payable {
      // insert check if the sent ether is enough to cover the car asset ...
      address newCarAsset = new CarAsset(brand, model, msg.sender);            
      carAssets.push(newCarAsset);   
   }
   function getDeployedChildContracts() public view returns (address[]) {
      return carAssets;
   }
}

contract CarAsset {
   string public brand;
   string public model;
   address public owner;
   function CarAsset(string _brand, string _model, address _owner) public {
      brand = _brand;
      model = _model;
      owner = _owner;
   }
}   

The code address newCarAsset = new CarAsset(...)will trigger a transaction to deploy the subcontract and return the address of that contract. Since the only connection between the factory contract and the asset contract is the variable address[] carAssets, it is important to save the address of the subcontract correctly.

3. Name registry

Suppose you are building a DApp that depends on multiple contracts, such as a blockchain-based online mall. This DApp uses ClothesFactoryContract, GamesFactoryContract, BooksFactoryContract and other contracts.

Now imagine writing the addresses of all these contracts in your application code. What if the addresses of these contracts change over time?

That's where the name registry comes in, a pattern that allows you to pin the address of just one contract in your code instead of tens, hundreds or even thousands of addresses. Its principle is to use a mapping table of contract name => contract address, so getAddress("ClothesFactory")the address of each contract can be found from within the DApp by calling. The benefit of using a name registry is that even if those contracts are updated, the DApp will not suffer any changes since we only need to modify the address of the contract in the mapping table.

code show as below:

contract NameRegistry {
   struct ContractDetails {
      address owner;
      address contractAddress;
      uint16 version;
   }
   mapping(string => ContractDetails) registry;
   function registerName(string name, address addr, uint16 ver) returns (bool) {
      // versions should start from 1
      require(ver >= 1);
      
      ContractDetails memory info = registry[name];
      require(info.owner == msg.sender);
      // create info if it doesn't exist in the registry
       if (info.contractAddress == address(0)) {
          info = ContractDetails({
             owner: msg.sender,
             contractAddress: addr,
             version: ver
          });
       } else {
          info.version = ver;
          info.contractAddress = addr;
       }
       // update record in the registry
       registry[name] = info;
       return true;
   }
    function getContractDetails(string name) constant returns(address, uint16) {
      return (registry[name].contractAddress, registry[name].version);
   }
}

Your DApp will use getContractDetails(name)to get the address and version of the specified contract.

4. Mapping table iterator

Many times we need to iterate over a mapping table, but since the mapping table in Solidity can only store values ​​and does not support iteration, the mapping table iterator pattern is very useful. It should be pointed out that as the number of members increases, the complexity of the iterative operation will increase, and the storage cost will also increase, so please avoid iteration as much as possible.

The implementation code is as follows:

contract MappingIterator {
   mapping(string => address) elements;
   string[] keys;
   function put(string key, address addr) returns (bool) {
      bool exists = elements[key] == address(0)
      if (!exists) {
         keys.push(key);
      }
      elements[key] = addr;
      return true;
    }
    function getKeyCount() constant returns (uint) {
       return keys.length;
    }
    function getElementAtIndex(uint index) returns (address) {
       return elements[keys[index]];
    }
    function getElement(string name) returns (address) {
       return elements[name];
    }
}

A common mistake in implementing put()functions is to traverse to check for the existence of a specified key. The correct way is elements[key] == address(0). While the practice of traversing the check is not entirely a mistake, it is not desirable because iteration becomes more expensive as the keys array grows, so iteration should be avoided if possible.

5. Withdrawal mode

Let's say you sell car tires, unfortunately all of the tires you sell have problems, and you decide to refund all buyers.

Suppose you keep track of all the buyers in the contract, and the contract has a refund() function that goes through all the buyers and returns the money one by one.

You can choose - use buyerAddress.transfer() or buyerAddress.send(). The difference between these two functions is that in the case of transaction exception, send() does not throw an exception, but just returns the boolean value false, while transfer() throws an exception.

Why is this important?

Assume that most buyers are external accounts (i.e. individuals), but some buyers are other contracts (maybe commercial). Suppose that among these buyer contracts, there is a contract whose developer makes a mistake in its fallback function and when called it throws an exception, the fallback() function is the default function in the contract, if the transaction is sent to The contract but does not specify any method, the contract's fallback() function will be called. Now, as soon as we call contractWithError.transfer() in the refund function, an exception is thrown and the iteration stops. Therefore, a fallback() exception on any one buyer's contract will cause the entire refund transaction to be rolled back, resulting in none of the buyers being refunded.

While refunding all buyers in one call can be achieved using send(), a better approach is to provide the withdrawFunds() method which will individually refund the caller as needed. Therefore, the wrong contract will not apply to other buyers to get a refund.

The implementation code is as follows:

contract WithdrawalContract {
   mapping(address => uint) buyers;
   function buy() payable {
      require(msg.value > 0);
      buyers[msg.sender] = msg.value;
   }
   function withdraw() {
      uint amount = buyers[msg.sender];
      require(amount > 0);
      buyers[msg.sender] = 0;      
      require(msg.sender.send(amount));
   }
}

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325857244&siteId=291194637