Skip to main content
Guide

A guide to understanding smart contracts

In the world of blockchain and decentralized applications, smart contracts stand out as a pivotal innovation. Defined as self-executing contracts with the terms of the agreement directly written into lines of code, they automate and enforce the execution of contract clauses, making transactions more secure and efficient. Originally proposed by Nick Szabo in the 1990s, smart contracts have found their natural habitat in blockchain ecosystems, becoming a cornerstone of various applications ranging from finance to supply chain management.

The Architecture of Smart Contracts

Developers write smart contracts using specialized programming languages. Solidity, a statically-typed programming language designed for developing smart contracts that run on the Ethereum Virtual Machine (EVM), is the most widely used. Other blockchain networks have their own languages, like Michelson for Tezos and Clarity for Stacks, each with unique syntax and capabilities.

Smart contracts interact with the blockchain to read and write data, change states, and trigger transactions. They operate in a decentralized environment, meaning every node in the network must agree on the outcome of the contract's execution. This requires careful consideration of gas fees (transaction costs) and optimization of code to ensure efficient execution, especially on networks like Ethereum where network congestion can lead to high fees.

A variety of tools assist developers in creating, testing, and deploying smart contracts. Truffle is a popular development framework for Ethereum, providing a suite of tools for smart contract development. Remix is an open-source web and desktop application that simplifies the process of smart contract development with Solidity. Hardhat is another development environment for Ethereum, offering advanced features like network simulation and console.log debugging.

Advantages from a Developer's Lens

Smart contracts eliminate the need for intermediaries, automating the execution of contract terms when predefined conditions are met. This results in faster transaction times and reduced chances of human error. Once deployed, a smart contract’s code and data are stored on the blockchain, making them immutable and tamper-proof. This ensures that the contract will execute exactly as written. The code of smart contracts is visible on the blockchain, allowing anyone to verify the contract’s functionality and security. This transparency builds trust among parties involved in the contract. By automating processes and removing intermediaries, smart contracts can significantly reduce transaction costs.

Technical Challenges and Solutions

  • Debugging and Maintenance

Smart contracts are notoriously difficult to debug and update once they’re deployed on the blockchain. Developers must rigorously test their code using tools like Ganache, a personal blockchain for Ethereum development that you can use to deploy contracts, develop your applications, and run tests. It is available both as a desktop application and a command-line tool.

  • Scalability and Performance

The decentralized nature of blockchain networks means that every node in the network must process every transaction, which can lead to scalability issues and slow performance. Layer 2 solutions like Optimism and Arbitrum for Ethereum aim to address these issues by processing transactions off-chain before settling on the main blockchain.

  • Security Concerns

The immutable nature of smart contracts also means that any vulnerabilities in the code can be exploited, and once funds are stolen, they cannot be recovered. Developers must follow secure coding practices, conduct thorough testing, and potentially employ third-party auditing services to ensure the security of their smart contracts.

Real-World Applications and Code Sample

DeFi: Decentralized Finance (DeFi) is perhaps the most notable use case of smart contracts, enabling automated lending, borrowing, and trading of crypto assets. Below is a simplified example of a smart contract written in Solidity that represents a basic decentralized bank:

pragma solidity 0.8.17;

import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "../abstracts/AStartonNativeMetaTransaction.sol";
import "../abstracts/AStartonContextMixin.sol";
import "../abstracts/AStartonPausable.sol";

/// @title StartonERC20Base
/// @author Starton
/// @notice ERC20 tokens that can be paused, burned, have a access management and handle meta transactions
contract StartonERC20Base is ERC20Burnable, AStartonPausable, AStartonContextMixin, AStartonNativeMetaTransaction {
constructor(
string memory definitiveName,
string memory definitiveSymbol,
uint256 definitiveSupply,
address initialOwnerOrMultiSigContract
) ERC20(definitiveName, definitiveSymbol) {
// Set all default roles for initialOwnerOrMultiSigContract
_setupRole(DEFAULT_ADMIN_ROLE, initialOwnerOrMultiSigContract);
_setupRole(PAUSER_ROLE, initialOwnerOrMultiSigContract);

// Mint definitiveSupply to initialOwnerOrMultiSigContract
_mint(initialOwnerOrMultiSigContract, definitiveSupply);

// Intialize the EIP712 so we can perform metatransactions
_initializeEIP712(definitiveName);
}

/**
* @dev Stop the transfer if the contract is paused
* @param from The address that will send the token
* @param to The address that will receive the token
* @param amount The of token to be transfered
*/
function _beforeTokenTransfer(
address from,
address to,
uint256 amount
) internal virtual override whenNotPaused {
super._beforeTokenTransfer(from, to, amount);
}

/**
* @dev Specify the _msgSender in case the forwarder calls a function to the real sender
* @return The sender of the message
*/
function _msgSender() internal view virtual override(Context, AStartonContextMixin) returns (address) {
return super._msgSender();
}
}

This smart contract allows users to create, mint and burn tokens, keeping track of their balances.

NFTs: Non-Fungible Tokens (NFTs) represent unique assets on the blockchain. They are typically implemented using the ERC-721 token standard. Below is a simple example of an NFT smart contract:

// SPDX-License-Identifier: Apache-2.0

pragma solidity 0.8.17;

import "operator-filter-registry/src/DefaultOperatorFilterer.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "../abstracts/AStartonNativeMetaTransaction.sol";
import "../abstracts/AStartonContextMixin.sol";
import "../abstracts/AStartonOwnable.sol";
import "../abstracts/AStartonPausable.sol";
import "../abstracts/AStartonMintLock.sol";
import "../abstracts/AStartonMetadataLock.sol";

/// @title StartonERC721Base
/// @author Starton
/// @notice ERC721 tokens that can be blacklisted, paused, locked, burned, have a access management and handle meta transactions
contract StartonERC721Base is
ERC721Enumerable,
ERC721URIStorage,
ERC721Burnable,
AStartonPausable,
AStartonOwnable,
AStartonContextMixin,
AStartonNativeMetaTransaction,
AStartonMintLock,
AStartonMetadataLock,
DefaultOperatorFilterer
{
using Counters for Counters.Counter;

bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant METADATA_ROLE = keccak256("METADATA_ROLE");

Counters.Counter internal _tokenIdCounter;

string private _baseTokenURI;
string private _contractURI;

constructor(
string memory definitiveName,
string memory definitiveSymbol,
string memory initialBaseTokenURI,
string memory initialContractURI,
address initialOwnerOrMultiSigContract
) ERC721(definitiveName, definitiveSymbol) AStartonOwnable(initialOwnerOrMultiSigContract) {
// Set all default roles for initialOwnerOrMultiSigContract
_setupRole(PAUSER_ROLE, initialOwnerOrMultiSigContract);
_setupRole(MINTER_ROLE, initialOwnerOrMultiSigContract);
_setupRole(METADATA_ROLE, initialOwnerOrMultiSigContract);
_setupRole(LOCKER_ROLE, initialOwnerOrMultiSigContract);

_baseTokenURI = initialBaseTokenURI;
_contractURI = initialContractURI;
_isMintAllowed = true;
_isMetadataChangingAllowed = true;

// Intialize the EIP712 so we can perform metatransactions
_initializeEIP712(definitiveName);
}

/**
* @notice Mint a new token to the given address and set the token metadata while minting is not locked
* @param to The address that will receive the token
* @param uri The URI of the token metadata
* @custom:requires MINTER_ROLE
*/
function mint(address to, string memory uri) public virtual whenNotPaused mintingNotLocked onlyRole(MINTER_ROLE) {
_mint(to, _tokenIdCounter.current());
_setTokenURI(_tokenIdCounter.current(), uri);
_tokenIdCounter.increment();
}

/**
* @notice Set the URI of the contract if the metadata are not locked and the contract is not paused
* @param newContractURI The new URI of the contract
* @custom:requires METADATA_ROLE
*/
function setContractURI(string memory newContractURI)
public
virtual
whenNotPaused
metadataNotLocked
onlyRole(METADATA_ROLE)
{
_contractURI = newContractURI;
}

/**
* @notice Set the base URI of the token if the metadata are not locked and the contract is not paused
* @param newBaseTokenURI The new base URI of the token
* @custom:requires METADATA_ROLE
*/
function setBaseTokenURI(string memory newBaseTokenURI)
public
virtual
whenNotPaused
metadataNotLocked
onlyRole(METADATA_ROLE)
{
_baseTokenURI = newBaseTokenURI;
}

/**
* @notice Returns the metadata of the contract
* @return Contract URI of the token
*/
function contractURI() public view virtual returns (string memory) {
return _contractURI;
}

/**
* @notice Returns the metadata of token with the given token id
* @param tokenId The token id of the token
* @return Contract URI of the token
*/
function tokenURI(uint256 tokenId) public view virtual override(ERC721, ERC721URIStorage) returns (string memory) {
return super.tokenURI(tokenId);
}

/**
* @dev Call the inherited contract supportsInterface function to know the interfaces as EIP165 says
* @return True if the interface is supported
*/
function supportsInterface(bytes4 interfaceId)
public
view
virtual
override(ERC721, AccessControl, ERC721Enumerable, ERC721URIStorage)
returns (bool)
{
return super.supportsInterface(interfaceId);
}

/**
* @dev Stop approval of token if the contract is paused or the sender is blacklisted
* @param owner The owner of the token
* @param operator The operator of the token
* @param approved Approve or not the approval of the token
*/
function _setApprovalForAll(
address owner,
address operator,
bool approved
) internal virtual override whenNotPaused onlyAllowedOperatorApproval(operator) {
super._setApprovalForAll(owner, operator, approved);
}

/**
* @dev Stop transfer if the contract is paused or the sender is blacklisted
* @param from The address that will send the token
* @param to The address that will receive the token
* @param tokenId The ID of the token to be transferred
* @param batchSize The number of tokens to be transferred
*/
function _beforeTokenTransfer(
address from,
address to,
uint256 tokenId,
uint256 batchSize
) internal virtual override(ERC721, ERC721Enumerable) whenNotPaused {
super._beforeTokenTransfer(from, to, tokenId, batchSize);
}

/**
* @dev Fix the inheritence problem for the _burn between ERC721 and ERC721URIStorage
* @param tokenId Id of the token that will be burnt
*/
function _burn(uint256 tokenId) internal virtual override(ERC721, ERC721URIStorage) {
super._burn(tokenId);
}

/**
* @notice Returns the first part of the uri being used for the token metadata
* @return Base URI of the token
*/
function _baseURI() internal view virtual override returns (string memory) {
return _baseTokenURI;
}

/**
* @dev Specify the _msgSender in case the forwarder calls a function to the real sender
* @return The sender of the message
*/
function _msgSender() internal view virtual override(Context, AStartonContextMixin) returns (address) {
return super._msgSender();
}

function approve(address operator, uint256 tokenId)
public
virtual
override(IERC721, ERC721)
onlyAllowedOperatorApproval(operator)
{
super.approve(operator, tokenId);
}

function transferFrom(
address from,
address to,
uint256 tokenId
) public virtual override(IERC721, ERC721) onlyAllowedOperator(from) {
super.transferFrom(from, to, tokenId);
}

function safeTransferFrom(
address from,
address to,
uint256 tokenId
) public virtual override(IERC721, ERC721) onlyAllowedOperator(from) {
super.safeTransferFrom(from, to, tokenId);
}

function safeTransferFrom(
address from,
address to,
uint256 tokenId,
bytes memory data
) public virtual override(IERC721, ERC721) onlyAllowedOperator(from) {
super.safeTransferFrom(from, to, tokenId, data);
}
}

This smart contract allows users to mint their own NFTs, assigning a unique ID and URI to each token.

Governance: DAOs use smart contracts to give token holders a say in decision-making processes. A governance smart contract may allow token holders to create and vote on proposals.

Smart contracts represent a paradigm shift in how we approach agreements and transactions, providing automation, security, and efficiency. However, they also pose unique challenges, particularly in terms of security and scalability. By following best practices, utilizing the right tools, and staying informed on the latest trends and developments, developers can harness the full potential of smart contracts and contribute to the ongoing evolution of decentralized applications.

Read more

Starton Smart Contract Library
Consensys’ Smart Contract Best Practices
Solidity by Example

Loubna Benzaama

Lead technical writer


Created:

February 26, 2024

Reading time:

9 min