How To Write Robust And Sustainable Smart Contracts

Good smart contracts are both robust and sustainable. Robustness describes the level of fault-tolerance in a smart contract and includes the ability to deal with critical failures without affecting users. Robust smart contract systems are also able to quickly detect problematic operations and respond effectively to avert problems. 

Sustainability describes the level of maintainability and flexibility of a smart contract. A sustainable contract can be adjusted and extended without incurring significant overhead or introducing vulnerabilities. It should also separate the policy (high-level components) from the implementation (low-level components) and allow for implementations to be swapped out safely and efficiently. 

Privileged admin controls and contract ownership

Building robust and sustainable contracts starts with implementing secure access control. With access controls, you can decide who can trigger specific operations in a smart contract and under what circumstances. This has benefits for robustness and sustainability in smart contract systems:

  • Robustness: Accounts with administrative privileges can execute security actions—such as freezing transfers—in response to exploits or bugs
  • Sustainability: Admin accounts can perform routine maintenance operations, like tweaking protocol parameters or upgrading a contract to a new implementation

It is, however, important to make sure access control mechanisms are reliable and secure: unauthorized accounts should never have access to a contract’s sensitive functions. To illustrate, poor access control can open up projects to infinite mint hacks if an adversary gains unauthorized access to token minting functions. 

The Ownable pattern is popular for controlling access to important functions in smart contracts. Projects implementing this pattern have a special account that “owns” a particular contract and is allowed to perform administrative operations. Protected functions typically feature an onlyOwner modifier and can only be called by the address set as Owner—calls to the function from other addresses simply revert. 

While contract ownership improves speed and efficiency of executing admin actions, it can increase centralization and introduce a single point of failure in a protocol. A more ideal approach is to assign administrative powers to multiple addresses. Here, you can define a whitelist of accounts with privileged roles—which can be updated—to ensure powers to perform maintenance operations aren’t concentrated in a single entity. 

You could still choose to have a single account with a wide range of administrative privileges (vs. using role-based access control), but it should be tied to a multisignature wallet address. Multisig wallets like Safe require approval from a predefined number of participants before executing a transaction, providing some protection against administrators going rogue or getting compromised.

Pauseability

While you can secure admin functions by restricting access to privileged accounts, protecting functions that are open to all users interacting with a protocol against abuse is more difficult. A common workaround is to write logic for “pausing” a smart contract’s functions if a critical bug is discovered. 

In this case, transactions that call paused functions revert while user operations involving other functions proceed normally. This feature can help avoid degraded user experience in emergency situations—for example, you may want to allow asset withdrawals from a lending protocol, but not deposits and borrows, while your team works on patching the vulnerability.  

Pauseability is particularly ideal because it allows protocol developers to mitigate the impact of security issues on the overall system. Without the ability to pause functions, the alternative may be to destroy the affected contract or freeze the entire protocol. 

We should, however, note that pausing a contract is a mitigative measure; a failsafe mechanism that improves your protocol’s capacity to recover from adversarial attacks and protect users. A pause feature is not a first line of defense against exploits (as one of our auditors put it: “if the attacker finds the bug first, then you’ve got a problem”).  

You can either code a pause feature for your contract from scratch or use an existing library, such as OpenZeppelin’s Pauseable.sol, to add a pause modifier to functions.  Below is a simple implementation of a pause feature in a smart contract by Steven Traykovski. It uses a global paused Boolean variable paired with a require statement as a precondition for function execution:

// A global boolean variable is used to control pause functionality called `paused`

   bool public paused;

// This variable is then implemented in the function with a `require` statement such that its value must resolve to "false" before the function can execute 

   function transfer1(address to, uint256 amount) external {

       require(paused == false, "Function Paused");

       require(balances[msg.sender] >= amount, "Not enough tokens");

       balances[msg.sender] -= amount;

       balances[to] += amount;

   }

// Another function must be added to change the value of the bool variable `paused`. A `require` statement is used to restrict access to the contract owner and prevent other users from calling the function

   function setPaused(bool _paused) public {

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

       paused = _paused;

   }

Using speed bumps and rate limits

A speed bump is a programmed delay on the execution of functions critical to the overall safety of a smart contract. For example, a project might delay user withdrawals or use a timelock to delay upgrades to a smart contract system. In both scenarios, the speed bump pattern ensures certain actions cannot be performed unless the predefined time window elapses. 

Speed bumps are necessary because, sometimes, the most feasible option for handling malicious actions is to slow down the attacker while you work to patch the vulnerability or implement mitigation measures. That said, speed bumps represent a tradeoff between speed, user experience, and security, so you’ll need to consider your project’s needs before using them. 

Speed bumps can also aid decentralization by protecting users against attempts to force undesirable changes on them. If a protocol’s governance enacts a new upgrade, users opposed to that change have time to exit funds from the system before the upgrade goes live. This is usually implemented by using a timelock that delays the execution of a governance proposal until some threshold is reached (eg. 48-72 hours after the proposal is approved). 

A rate limit is similar to a speed bump and is often implemented in financial smart contracts to improve security. Rate limits could take several forms, such as limiting how much a user can withdraw or trade in a single transaction or limiting how many tokens a smart contract can mint in a time window. 

Like speed bumps, rate limits may affect user experience—for example, users may need to split large trades into a number of smaller transactions. But this may be a good tradeoff to make, as it reduces the amount of damage an attacker can do if a vulnerability is discovered. 

A caveat: Speed bumps and rate limits can give a false sense of security, and aren’t always implemented correctly. Both mechanisms are more complex than the ownable/pausable pattern, so this is to be expected. As with all smart contract security features, (including those described in this post), a speed bump or rate limit should be implemented as part of a comprehensive plan for improving a protocol’s resilience against adversarial attacks. 

Upgrading contracts with proxy patterns 

Contract code running on the blockchain is immutable, ruling out the possibility of adding new features or fixing a bug after deployment. It is possible, though, to simulate a logic upgrade without changing the code users interact with—this is achieved using ‘proxy patterns’. 

The proxy pattern makes use of two contracts: a ‘proxy contract’ that stores state (data) and an ‘implementation contract’ which stores the business logic. Users interact with the proxy contract, but the implementation contract is responsible for executing function invocations. 

This is possible because the proxy contract holds an address variable referencing the implementation contract and delegates all function calls to it using delegatecall. Because delegatecall is used, functions are executed in the context of the proxy contract and the implementation contract writes to the former’s storage. 

By separating your contract’s data and business logic, you can “upgrade” the contract without having to deploy a new contract instance and migrate data from the old contract. You only need to deploy a new implementation contract and point the proxy to it by changing the stored address variable. 

The transparent proxy pattern and Universal Upgradeable Proxy Standard (UUPS) are two common approaches to using upgradeable proxy contracts. With transparent proxies, the proxy contract stores the implementation contract’s address, the function for upgrading the proxy to a new implementation, and the address that can call the upgrade function (usually marked Owner). UUPS proxies are similar, except that the function for upgrading the proxy to a new implementation is stored in the current implementation itself. 

Proxy patterns make it possible to upgrade contracts without breaking existing interactions—users interact with the same proxy and are oblivious to changes in the implementation contract. Deploying new implementation contracts is also more cost-effective than copying state to a new contract instance. 

You should be aware, though, that proxy patterns are complex and can introduce several issues if implemented incorrectly. This includes problems, such as function selector clashes and storage collisions

Monitoring smart contracts and implementing automatic responses

Rigorous testing during the development phase may not always prevent the possibility of bugs surfacing in your smart contract after deploying to mainnet. This is why monitoring contract transactions is important—noticing suspicious operations gives you enough time to respond to possible (or ongoing) exploits. 

An ideal approach to contract monitoring is to track transactions using events. An emitted event should include details about the operation, including data passed as arguments—so that you have insights into what occurred. 

Examples of operations to monitor include:

  • Operations involving administrative actions (e.g., minting new tokens);
  • Abnormal increases in transaction volume;
  • Spikes in deposits/withdrawals; 
  • Suspicious transactions, etc. 

While you can manually listen for events (using something like Webhooks), it’s more effective to automate smart contract monitoring. Automated monitoring tools like Hal Notify provide a real-time notification  system that tracks on-chain activity, keeping you informed about contract states. 

It is also a good idea to combine transaction monitoring with an automated smart contract security tool like OpenZeppelin Defender.  With Defender, project teams can monitor contract transactions and automatically trigger protective measures, such as pausing the contract, if suspicious patterns are detected. 

Make smart contracts composable  

Composability is reusing or combining existing software components to build new applications. In the context of web3 development, composability allows developers to extend or modify a smart contract’s functionality by integrating code stored elsewhere—for example, with contract inheritance. 

Like other object-oriented programming languages, Solidity allows a contract (the “child” class) to inherit functions from another contract (the “parent” class). A smart contract can inherit multiple contracts (multiple inheritance), including abstract contracts, interface contracts, and libraries. 

Abstract contracts and interfaces are special types of contracts, as they only declare functions without implementing them. A contract that inherits an abstract contract or interface uses custom logic to implement functions defined in base classes.

In this sense, contract inheritance makes smart contract systems more flexible and sustainable by decoupling the declaration of a function from its implementation. If needed, you can modify or extend a contract’s logic by changing how it implements functions defined by interface or abstract contract. 

Contract inheritance also enhances simplicity and security in development by reducing the amount of code you must write. This is especially true if you integrate with battle-tested contract libraries that benefit from extensive auditing and testing by the open-source community.

All posts chevronRight icon

`