Design Patterns for Smart Contracts - Security

BrianKim
Coinmonks
6 min readJul 11, 2023

--

Foreword

This is the final section in the 5-part series of how to solve reoccurring design flaws by reusable and conventional design patterns. By means of this, we will dissect in Security, a group of patterns that introduce safety measures to mitigate damage and assure a reliable contract execution.

Checks-Effects-Interaction

Problem

When a contract calls another contract, it hands over control to that other contract. The called contract can then, in turn, re-enter the contract by which it was called and try to manipulate its state or hijack the control flow through malicious code.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

// THIS CONTRACT CONTAINS A BUG - DO NOT USE
contract Fund {
/// @dev Mapping of ether shares of the contract.
mapping(address => uint256) shares;

/// Withdraw your share.
function withdraw() public {
// caller’s code is executed and can re-enter withdraw again
(bool success,) = msg.sender.call{value: shares[msg.sender]}("");

// INSECURE - user's shares must be reset before the external call
if (success)
shares[msg.sender] = 0;
}
}

Ether transfer can always include code execution, so the recipient could be a contract that calls back into withdraw. This would let it get multiple refunds and, basically, retrieve all the Ether in the contract.

Solution

The Checks-Effects-Interaction pattern is fundamental for coding functions and describes how function code should be structured to avoid side effects and unwanted execution behaviour.

The Checks-Effects-Interactions pattern ensures that all code paths through a contract complete all required checks of the supplied parameters before modifying the contract’s state (Checks), only then it makes any changes to the state (Effects), it may make calls to functions in other contracts after all planned state changes have been written to storage (Interactions).

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

contract Fund {
/// @dev Mapping of ether shares of the contract.
mapping(address => uint256) shares;

/// Withdraw your share.
function withdraw() public {
uint256 share = shares[msg.sender];
// 1. Checks
require(share > 0);

// 2. Effects
shares[msg.sender] = 0;

// 3. Interaction
payable(msg.sender).transfer(share);
}
}

The re-entrancy attack is especially harmful when using low level address.call, which forwards all remaining gas by default, giving the called contract more room for potentially malicious actions. Therefore, the use of low level address.call should be avoided whenever possible.

For sending funds address.send() and address.transfer() should be preferred, these functions minimize the risk of reentrancy through limited gas forwarding (the called contract is only given a stipend of 2,300 gas, which is currently only enough to log an event).

Emergency Stop (Circuit Breaker)

Problem

Since a deployed contract is executed autonomously on the Ethereum network, there is no option to halt its execution in case of a major bug or security issue.

Solution

One countermeasure and a quick response to unknown attacks are emergency stops or circuit breakers. They stop the execution of a contract or its parts when certain conditions are met.

A recommended scenario would be, that once a bug is detected, all critical functions would be halted, leaving only the possibility to withdraw funds.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

contract Pausable {
bool private paused;

event Paused(address account);
event Unpaused(address account);

error EnforcedPause();
error ExpectedPause();

modifier whenNotPaused() {
if (paused) {
revert EnforcedPause();
}
_;
}

modifier whenPaused() {
if (!paused) {
revert ExpectedPause();
}
_;
}

constructor() {
paused = false;
}

function _pause() internal virtual whenNotPaused {
paused = true;
emit Paused(msg.sender);
}

function _unpause() internal virtual whenPaused {
paused = false;
emit Unpaused(msg.sender);
}
}

Implement emergency stop on Staking contract:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "@openzeppelin/contracts/access/Ownable.sol";
import "./Pausable.sol";

contract Staking is Pausable, Ownable {
/** declare state variables */

function pause() external onlyOwner {
_pause();
}

function unpause() external onlyOwner {
_unpause();
}

function deposit() external payable whenNotPaused {
// some code
}

function withdraw() external whenNotPaused {
// some code
}

function emergencyWithdraw() external whenPaused {
// some code
}
}

Speed Bump

Problem

The simultaneous execution of sensitive tasks by a huge number of parties can bring about the downfall of a contract.

Solution

Contract sensitive tasks are slowed down on purpose, so when malicious actions occur, the damage is restricted and more time to counteract is available.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

contract SpeedBump {
struct Withdrawal {
uint256 amount;
uint256 requestedAt;
}

uint256 constant WAIT_PERIOD = 7 days;

mapping (address => uint256) private balances;
mapping (address => Withdrawal) private withdrawals;

// Each user's address can deposit only 1 time until fully withdrawn
function deposit() public payable {
bool hasDeposited = withdrawals[msg.sender].amount > 0;
if(!hasDeposited)
balances[msg.sender] += msg.value;
}

function requestWithdrawal() public {
if (balances[msg.sender] > 0) {
uint256 amountToWithdraw = balances[msg.sender];
balances[msg.sender] = 0;

withdrawals[msg.sender] = Withdrawal({
amount: amountToWithdraw,
requestedAt: block.timestamp
});
}
}

// Only fully withdrawn when WAIT_PERIOD expired
function withdraw() public {
if(withdrawals[msg.sender].amount > 0 &&
block.timestamp > withdrawals[msg.sender].requestedAt + WAIT_PERIOD)
{
uint256 amount = withdrawals[msg.sender].amount;
withdrawals[msg.sender].amount = 0;
payable(msg.sender).transfer(amount);
}
}
}

You can reference to TimelockController contract implementing by OpenZeppelin for the production-based contract version. A timelock is a smart contract that delays function calls of another smart contract after a predetermined amount of time has passed. Timelocks are mostly used in the context of governance to add a delay of administrative actions and are generally considered a strong indicator that a project is legitimate and demonstrates commitment to the project by project owners.

Rate Limit

Problem

A request rush on a certain task is not desired and can hinder the correct operational performance of a contract.

Solution

A rate limit regulates how often a function can be called consecutively within a specified time interval.

A usage scenario for smart contracts may be founded on operative considerations, in order to control the impact of (collective) user behaviour.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

contract RateLimit {
uint enabledAt = block.timestamp;

modifier enabledEvery(uint256 t) {
if (block.timestamp >= enabledAt) {
enabledAt = block.timestamp + t;
_;
}
}

function withdraw() public enabledEvery(1 minutes) {
// some code
}
}

The example above demonstrates the withdrawal execution rate limitation of a contract to prevent a rapid drainage of funds.

Mutex

Problem

Re-entrancy attacks can manipulate the state of a contract and hijack the control flow.

Solution

A mutex (from mutual exclusion) is known as a synchronization mechanism in computer science to restrict concurrent access to a resource. After re-entrancy attack scenarios emerged, this pattern found its application in smart contracts to protect against recursive function calls from external contracts.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

abstract contract ReentrancyGuard {
uint256 private constant _NOT_ENTERED = 1;
uint256 private constant _ENTERED = 2;

uint256 private _status;

error ReentrancyGuardReentrantCall();

constructor() {
_status = _NOT_ENTERED;
}

modifier nonReentrant() {
_nonReentrantBefore();
_;
_nonReentrantAfter();
}

function _nonReentrantBefore() private {
// On the first call to nonReentrant, _status will be _NOT_ENTERED
if (_status == _ENTERED) {
revert ReentrancyGuardReentrantCall();
}
// Any calls to nonReentrant after this point will fail
_status = _ENTERED;
}

function _nonReentrantAfter() private {
// By storing the original value once again, a refund is triggered (see
// https://eips.ethereum.org/EIPS/eip-2200)
_status = _NOT_ENTERED;
}
}

contract Mutex is ReentrancyGuard {
/** declare state variables */

// f is protected by a mutex, thus reentrant calls
// from within msg.sender.call cannot call f again
function f() external nonReentrant {
// some code
}
}

Balance Limit

Problem

There is always a risk that a contract gets compromised due to bugs in the code or yet unknown security issues within the contract platform.

Solution

It is generally a good idea to manage the amount of money at risk when coding smart contracts. This can be achieved by limiting the total balance held within a contract.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

contract LimitBalance {
uint256 public limit;

modifier limitedPayable() {
require(address(this).balance < limit);
_;
}

constructor(uint256 value) {
limit = value;
}

function deposit() external payable limitedPayable {
// some code
}
}

The pattern monitors the contract balance and rejects payments sent along a function invocation after exceeding a predefined quota limit.

It should be noted that this approach cannot prevent the admission of forcibly sent Ether, e.g. as beneficiary of a selfdestruct(address) call, or as recipient of validator duties rewards.

Conclusion

I have described the Security group of patterns in detail and I have provided exemplary code for better illustration. I recommend you use at least one of these patterns in your next Solidity project to test your understanding of this topic.

Keep in mind that even if your smart contract code is bug-free, even if you comply strictly with those patterns I have already mentioned, the compiler or the platform itself might have a bug. A list of some publicly known security-relevant bugs of the compiler can be found here and splendidly security considerations can be found here. I highly suggest that take a look at those docs, find more articles and blogs for security improvement and it is a best practice to always asking people to review your code.

Follow Me On Linkedin To Stay Connected

https://www.linkedin.com/in/ninh-kim-927571149/

--

--