Skip to content

Latest commit

 

History

History
554 lines (317 loc) · 51.6 KB

R003.md

File metadata and controls

554 lines (317 loc) · 51.6 KB

Token Design Research

from Cooper Midroni of Windranger Labs

Table of Contents

Foreword

Tokens have always been a major part of the blockchain/web3 narrative. No doubt owing to the fact that the first successful blockchain application - Bitcoin - was first and foremost a token. Since then, the narrative surrounding tokens has shifted over the years. In the 2017 cycle tokens were seen as an accessible way for teams to raise capital from their user base. In the 2021 cycle tokens were used for access and authorization - a vehicle for digital communities to manifest around. In this way, our understanding of tokens grew beyond a a mere analog to equity. We now view them as universally programmable digital assets. Like all software, their functionality is limitless.

Today, users and founders alike think far more deeply about a token's features. Consideration of a project's tokenomics is an industry-wide norm. We care to ask such questions as: How many tokens will be issued and on what schedule? Who can upgrade the token's smart contract? How will my ownership be recognized in governance? Whereas 2017 was characterized by a rush to tokenization, 2021 brought about our collective hesitancy. We recognize the many nuanced ways that a token's design can influence its project, and treat as more of a science than an art.

Document Goals and Scope

This guide is intended as a handbook to understand essential token functions. Our aim is to educate you, the reader, about the many design decisions implicit in token creation. For instance, there are important tradeoffs concerning immutability, upgrade flexibility and perceptions of security. The balance between token supply and demand is another such area. We will explore all these and more by looking at seven categories of token functions: Transfer, Balance, Supply, Governance, Upgradability, Access Control, and Security.

Goals

  • Provide an overview of the token landscape, starting with an analysis of successful projects.

  • Explain the purpose of common functions seen in the ERC-20 standard and its adopted extensions.

  • Motivate the use of such functions using a scenario-based, user-centric approach

Before we begin, a word on scope. As with any research, we must draw reasonable bounds around an expansive and evolving topic. In general, this document explains functions at the contract-level, without diving too deep into the operation of the Ethereum Virtual Machine (EVM). We focus greatly on the ERC-20 standard, and delve into its extension interfaces where we feel important. If you note a section that could benefit from further research or explanation, let us know through a pull request or the contact info posted below.

We must also acknowledge the volumes of work that both contributed and inspired to this work. We have done our best to add citations where possible.

We consider this a living document. If you'd like to continue the discussion, add to this work, or generally participate in BitDAO research please reach out at [email protected] or @midroni on Twitter.

Thank you to everyone who participated in the research, editing, and review of this document: Dow Jones, Alexander Chau, Praveen Mangalampalli

Ecosystem Analysis

We started our research by assessing the smart contracts of common tokens, resulting in the table below. We recommend that the reader refer back to the table as they progress through this document section by section. This comparison will be a helpful exercise to ground the concepts discussed.

There are two caveats to the table below:

  1. The list of functions here is surely incomplete. For methods not covered here we refer you to the Ethereum or OpenZeppelin documentation.

  2. Certain token implementations may not use the standard methods discussed here - but may use a custom or similar alternative. We do our best to note this and link to the alternatives.

LEGEND
Y Implemented
N Not Implemented
A Alternative implemented (link to docs)
Functions BIT Comp Uni Curve Aave USDC Recommendation
Transfer
Approve Y Y Y Y Y Y Must
Transfer Y Y Y Y Y Must
TransferFrom Y Y Y Y Y Must
Increase Allowance Y Y Y Must
Decrease Allowance Y Y Y Must
Permit Y Y
Balance
Snapshot Y
totalSupplyAt
balanceOfAt
Governance
Delegate Y Y Y Y Must
DelegateBySig Y Y Y Y Must
DelegatebyType Y Maybe
DelegatebyTypebySig Y Maybe
Token Supply
Mint Y Y Y Maybe
setMinter Y
Burn Y Y Maybe
Security
Blacklist Y Y
Pause Y Y
Rescue Y Y
Access Control
acceptAdmin Y ?
setPendingAdmin Y ?
Upgradability
Upgradability (Proxy) Y Y Maybe
Initialize Y Y

Token Functions

Contract Interfaces

Key to making informed decisions on token design is understanding what tokens exactly are. A token is a smart contract with a standard interface. This interface allows any external contract or user account to interact with your contract in a common way, by understanding the expected variables and methods. The ERC-20 standard is the original token standard, which - thanks to its widespread adoption - has defined a way for tokens to be arbitrarily compatible in applications such as wallets and decentralized exchanges. This creates the user experience of ecosystem-wide interoperability that is the hallmark of web3.

Despite the goal of standardization, development effort to add new token functionality is ongoing. This allows us to discover new features, which are ultimately accepted as layered standards what tokens are capable of. For every feature implementation, there are tradeoffs to consider - whether it be increased gas costs or security risk.For this reason, designing a token must take all these dimensions into account.

Transfer Functions

The ERC-20 contract natively supports two token transferring scenarios. The first scenario allows you to transfer tokens using your own account. The second scenario allows you to authorize a third party (typically another account/smart contract) to transfer tokens on your behalf. They are enabled by the following methods:

Scenario 1 Self-Transfer
methods transfer()

transfer (address _to, uint256 _value)

Primary Use Case: Used to transfer tokens from owned by your account to a target recipient.

Scenario: Alice calls transfer() to send her own tokens to a target recipient, Bob.

Requirements:

  • The account owner must have the required funds to transfer. Check whether account balance is more than the amount being transferred.

  • The recipient account must not be the 'Null address'; 0 address. 0x0000000... which is used to burn tokens.

Advantages:

  • If the function call fails, the transfer function will return to its previous state. This means that in the event of failure, only gas fees are lost.

  • This function is an effective guard against re-entrancy attacks. It forwards only 2300 gas for contract execution, which is not sufficient for the recipient address/contract to once again call the contract.

Scenario 2 Authorized Party Transfer
methods approve()
transferFrom()

approve (address _spender, uint25 _value)

Primary Use Case: Used to authorize another account (spender) to transfer funds from your account's balance. The approve method sets an approved token amount (allowance) on the tokens that a spender can transfer out of your account.

Scenario: Alice calls approve(), authorizing authorizes Bob to transfer up to 10 tokens from her account.

Requirements:

  • The account owner's token balance must be greater than the allowance.

  • The spender being approved cannot be the null address.

(amount) of tokens a given account may transfer out of your account. It binds the approval to the msg.sender, which implies that the approve function has a transaction explicitly initiated by an externally owned account (EOA).

transferFrom (address _from, address _to, uint256 _value)

Primary Use Case: Used by a 3rd-party to transfer funds owned by another account to any recipient account, up to their authorized allowance.

Scenario: Bob calls transferFrom("0xAlice", "0xCarla", 10) on a token's smart contract in an attempt to send 10 of Alice's tokens to Carla.

Requirements:

  • The allowance of the calling account (eg. Bob) must be greater than the amount to transfer.

  • The calling account must be approved to spend the amount.

Challenge #1 Approval Front-Running
Attack Vectors :
Approval front-running : Results in more than intended tokens being approved for a given address. Scenario as below :
Alice allows Bob to transfer 100 BIT by calling approve on the token’s smart contract. These details are passed in as method arguments: approve("0xBob", 100);
After some time, Alice decides to change Bob’s approved allowance from 100 BIT to 10 BIT. Alice calls approve again, now passing the following arguments: approve("0xBob", 10);
Bob notices Alice's second transaction before it was mined and quickly sends another transaction, using the transferFrom method to transfer 100 BIT to himself: transferFrom("0xAlice", "0xBob", 100);
If Bob's transaction is executed before Alice's transaction, Bob will successfully transfer 100 BIT and gain the ability to transfer yet another 10 tokens. This happens as Bob can include higher transaction fees than Alice, leading to Bob’s transaction being confirmed earlier.
Before Alice notices that something went wrong, Bob calls transferFrom method again, this time to transfer 10 further BIT. This results in Bob acquiring 110 of Alice's tokens instead of only 10.

Attack Vectors :

  • Approval front-running : Results in more than intended tokens being approved for a given address. Scenario as below :
  1. Alice allows Bob to transfer 100 BIT by calling approve on the token's smart contract. These details are passed in as method arguments: approve("0xBob", 100);

  2. After some time, Alice decides to change Bob's approved allowance from 100 BIT to 10 BIT. Alice calls approve again, now passing the following arguments: approve("0xBob", 10);

  3. Bob notices Alice's second transaction before it was mined and quickly sends another transaction, using the transferFrom method to transfer 100 BIT to himself: transferFrom("0xAlice", "0xBob", 100);

  4. If Bob's transaction is executed before Alice's transaction, Bob will successfully transfer 100 BIT and gain the ability to transfer yet another 10 tokens. This happens as Bob can include higher transaction fees than Alice, leading to Bob's transaction being confirmed earlier.

  5. Before Alice notices that something went wrong, Bob calls transferFrom method again, this time to transfer 10 further BIT. This results in Bob acquiring 110 of Alice's tokens instead of only 10.
    |

To overcome this challenge we need to expand beyond the original ERC-20 standard. We can rely on the following methods from OpenZeppelin's IERC20 interface.

|

Scenario 3 Secure Authorized Party Transfer
methods increaseAllowance()
decreaseAllowance()

Whereas approve() passes an absolute value that will set the allowance, using increaseAllowance() or decreaseAllowance() will perform addition or subtraction. This prevents any front-running, as the new allowance will be relative to the current allowance.

increaseAllowance (address spender, uint256 addedValue)

Primary Use Case: allows an account to increase a spender's allowance while avoiding a front-running attack

Scenario: Alice calls 'increaseAllowance("0xBob", 10)' to increase Bob's total allowance by 10 tokens.

Requirements:

  • Spender cannot be the zero address

decreaseAllowance (address spender, uint256 subtractedValue)

Primary Use Case: allows an account to decrease a spender's allowance while avoiding a front-running attack

Scenario: Alice calls 'decreaseAllowance("0xBob", 10)' to decrease Bob's total allowance by 10 tokens.

Requirements:

  • Spender cannot be the zero address

  • Spender must have allowance for the caller of at least subtractedValue

Challenge #2 Msg.Sender Dependency
It’s fair to say that the approve and transferFrom methods are precisely what enabled ERC-20 tokens to become so deeply embedded across Ethereum’s dapps. Providing contracts with an allowance of tokens creates a feeling of effortless integration and interoperability throughout the ecosystem. However, this system still has its limitations. For instance, approve must always be called by the account owner (AKA msg.sender), which imposes a gas requirement on the owner and splits the user experience across two transactions (one for calling approve and another for the contract call to transferFrom).

However, a solution to this dilemma was created in EIP-2612, which added a new function; permit. This new solution takes advantage of Ethereum meta-transactions.

As every address on Ethereum has a private/public key, it should suffice that a user can send a signed message using their private key to approve a transaction. This abstraction allows a user to submit both their signature and a transaction to a 3rd-party known as a relayer. Relayers submit the user's transaction to the network, covering gas costs while typically accepting a fee from tokens used within the transaction. For instance, a relayer could execute a DAI-USDT token swap while accepting fees in DAI. The development of meta-transactions can only be leveraged through the use of permit.

Scenario 4 Permit Meta-Transactions
methods permit()

permit (address owner, address spender, ...)

Primary Use Case: Allows users to modify the token allowance of a 3rd-party contract through the use of signed messages (meta-transactions).

Scenario: Uniswap V2 pools now support meta-transaction approvals via the permit method. Alice sends her DAI-USDT token swap transaction along with a signed message to a relayer. The relayer submits this transaction to the network, taking a small amount of Alice's DAI to cover gas costs.

Requirements:

  • The permit function has a complex set of requirements which have already been well documented. We encourage you to look at EIP-2612 for more details.

Advantages:

  1. Allows any operation involving an ERC-20 token to be paid for using the token itself, instead of relying on ETH

  2. Reduces total number of transactions.

Balance Functions

The methods we've described so far allow for a fairly functional token - one that can be traded by its owner and an approved list of contracts that is managed by the owner.

Say you wanted to implement a mechanism like weighted voting, or a lottery amongst token holders for an airdrop. These would require some ability to access the current state of the token and its holders across the entire chain. That is exactly what the following functions will assist with.

_snapshot()

Primary Use Case: Create a snapshot of the token and its balance across accounts.

Scenario: As the creator of BIT, Alice wants to understand who the major holders of her token are and how they change over time. Every day she calls _snapshot() to preserve an image of accounts holding her token and their balances over time.

Requirements:

  • _snapshot is an internal function. As the contract creator you can decide how and if to expose it externally. For more on this, see our section on Access & Control.

Advantages:

  • ERC20 with a snapshot mechanism

  • Snapshot contains the balances and total supply

  • Can be used to create mechanisms based on token balances such as trustless dividends or weighted voting

totalSupplyAt()

Primary Use Case: Get the total supply of a token at the time of a snapshot.

Scenario: Alice wants to manage her project's token emissions. She enters each day's snapshot ID into totalSupplyAt() to receive the total number of tokens in circulation. Tracking this each day will allow her to measure token issuance over time.

Requirements:

Advantages:

balanceOfAt()

Primary Use Case: Get the balance of an account at the time of a snapshot.

Scenario: Alice wants to monitor the token supply held by certain partners and whales in her ecosystem. Using balanceOfAt() she can enter their addresses and each day's snapshot ID to track their individual changes in balance.

Requirements:

Advantages:

Token Supply

In a macro-economic sense, supply and demand properties are essential for understanding an asset's relative value. An entire discipline of study is created to assess the management of token supply and demand: tokenomics. Tokenomics sees tokens as evolving past the traditional monetary perspectives on what currencies can do. In this expanded lens, we tend to put a greater emphasis on the use of demand levers and supply management.

A great example is Bitcoin, where tokens are only awarded to miners that propose valid blocks. This guarantees tokens are issued only when productive activities are performed, benefitting the network as a whole. Ethereum's Proof of Stake is an example of coupling supply and demand together. For Ethereum validators to be eligible to secure the network (and thus receive fees) they need to stake the ETH token. The more they stake, the more likely they are to be selected by the protocol. This incentive produces demand, while similarly guaranteeing that newly issuednew tokens go to those performing beneficial activities.

This is why cryptocurrencies are often referred to as programmable money. You can design a system of complex incentives to motivate any node behavior. And the following functions are essential to doing so.

Mint(address account, unit256 amount)

Primary Use Case: Generate a number of tokens for a given account.

Scenario: Alice is designing a protocol such that every time a user leaves a product rating, they are awarded with 10 tokens. Alice will use the _mint(0xUser, 10) function when a user performs this action.

Requirements:

  • 'to' account cannot be the zero address

Advantages:

  • Only certain addresses/accounts have the ability to mint tokens to other accounts

  • The mint function automatically updates the total supply prior to updating the balance of the receiving account

  • The mint function modularizes token creation in a way that can be implemented throughout a dapp or protocol

  • You can use logic prior to calling mint() to constrain issuance based on desired supply properties

Below we list some practical applications of the _mint method: _Mint Use Cases
Scenario Create initial supply
You can use the _mint() function inside a constructor to set the initial supply to a fixed set value.
contract ERC20FixedSupply is ERC20 {
constructor() public {
_mint(msg.sender, 1000);
}
}
Scenario Create a supply cap
You can introduce a supply cap as a global variable ‘totalsupply’ Minting should only be possible if the totalsupply is below the cap. This can be accomplished through a require statement:
require(totalSupply().add(value) <= cap);
Challenge #X Infinite Mint Attack
It is important to note that the mint function introduces a serious risk to projects if compromised. In the absence of a supply cap, it could allow an attacker to create an infinite supply of tokens. For this reason, restricting access to mint is essential. For more on this, see Access & Control

Burn(address account, unit256 amount)

Primary use case : Used to burn the token supply. An explicit choice to allow token burning.

Practical application: Token burn mechanisms to burn supply. Think of a stablecoin that wants to reduce its supply, or a staking contract in which someone want to remove the initial funds and burn the LP token.

Advantages: _burn allows users to send tokens to the cononical burn address 0x000...0000

Alternative to Burn Function

Tokens that do not include the Burn function cannot have tokens sent to the canonical burn address, even via manual transfer. Instead, transfers may be done to other commonly use burn addresses, such as:

0x000000000000000000000000000000000000dEaD A widely recognized as a burn wallet, cited by Mastering Ethereum and tagged by Etherscan.

0xdEAD000000000000000042069420694206942069 Popularized by Vitalik sending Shib tokens to this address.

Mathematically speaking, any low-entropy public key should be acceptable to use as a burn address, however deviating from what is commonly accepted could result in unwanted confusion or speculation.

Governance Functions

As mentioned, balance and snapshot methods can be used to layer functionality on top of your token. They provide 'demographic' data that allow you to engage with your token holder base. This data is most often used in one of the most widespread applications crypto-tokens; decentralized governance.

By governance, we mean the practice of using tokens to assist with decision making and reaching group consensus. Many projects allow their communities to self-govern with a simple 'one-token-one-vote' model applied to yes/no decision making. Platforms like Snapshot support polling on community proposals and keep track of both live and historic voting outcomes.

There are many challenges with governance, the most common of which is asymmetry in the distribution of token ownership. It's common for tokenized projects to have a vast majority of tokens held by the core team, investors, or a foundation. As a result, it can take a project several years to 'progressively decentralize' - putting more tokens in the hands of their user base over time.

Before this can happen, it is still the case that we want to empower a community with self-governance. To do so meaningfully would require that the community have enough voting power to steer the project. To this effect, we have adopted the concept of voting delegation. Delegation allows voting power to be allocated to select groups or individuals, without conferring underlying ownership of the token. Effective delegation requires that three scenarios be enabled:

  1. Basic Delegation:

  2. Gasless Delegation:

  3. Multi-Right Delegation:

The following methods exist within the ERC20Votes extension contract.

delegate (address delegatee)

Primary Use Case: Used to delegate votes from the sender to the delegatee.

Scenario: Alice has 100 BIT tokens. By calling delegate("0xBob") Alice will delegate her entire 100 tokens of voting power to Bob's address.

Requirements:

Advantages:

  • Delegation does not lock or transfer tokens to the delegatee wallet.

  • If a delegatee wallet is stolen or compromised, tokens can be re-delegated to another address.

Disadvantages:

  • Can only delegate to 1 holder at a time

  • Delegates the entire balance of tokens in msg.sender's account

delegateBySig(address delegatee)

Primary Use Case: Used to delegate votes from the sender to a delegatee through use of an offline signature.

Scenario: Alice has 100 BIT tokens. She wants to delegate her tokens to Bob, allowing him to vote on a recent proposal that has come out. She calls delegateBySig, which allows her to delegate to Bob without paying gas, while placing an expiry date on Bob's delegatee rights to her tokens.

Requirements:

  • This function relies on the offline signature creation process outlined in EIP712.

Advantages:

  • Similar to the use of the permit function discussed above, delegation here is enabled via a signed message, thus not requiring an explicit transaction for delegation - saving the user gas.

  • Contains an expiry field, which puts an automatically enforced time limit on the delegation period.

  • It is possible to delegate voting power through a 3rd-party contract (similar to transferFrom function). You can find an implementation example for this here.

Challenge #3 Separation of Token Powers
One of the challenges of decentralized governance is that it encourages us to see tokens only as votes. To the contrary, voting is but one of the many ‘rights’ that token ownership can confer during governance.
Another right is that of ‘proposition’. In a voting system with proposition, proposals are first assessed by community members in a staging area. Here, community members will upvote relevant and high-quality proposals to filter content that makes it to the community stage. The right of ‘proposition’ is the right to participate in this filtering process, which is entirely separate from voting itself.
The idea of multiple-right tokens has immediate implications for delegation. Under the current models we’ve explored (delegate and delegateBySig), delegating your tokens is equivalent to giving all your governance rights to the delegatee. It’s easy to imagine scenarios where a user wants to specialize, by retaining either their proposition or voting rights.
Scenario 5 Dual-Right Right Delegation
methods delegateByType()
Diagram showing delegation of just ONE right
Create proposal (token balance > proposal threshold)
Vote on a proposal
Token holders can delegate their voting power such that the delegatee can only vote with the delegated power but not create proposals.

Diagram showing delegation of just ONE right

  1. Create proposal (token balance > proposal threshold)

  2. Vote on a proposal

Token holders can delegate their voting power such that the delegatee can only vote with the delegated power but not create proposals.

delegateByType(address delegatee, uint8 delegationType)

Primary Use Case: Used to delegate a specific governance power to a delegatee address.

Scenario: Alice wants to delegate her voting rights to Bob while retaining her right to proposition. She calls delegateByType("0xBob", 0) to send her voting rights to Bob.

Requirements:

  • Requires 2 or more recognized purposes of tokens.

Advantages:

  • Creates granularity in the governance actions granted to delegatee.

The concept of delegateByType allows us to imagine a more dynamic form of governance. One where DAO contributors can specialize and collect into focused operating groups. A developer can imbue their token with arbitrary powers, paving the way for more complex decision making within their organization.

Before wrapping up our section on Governance, we will mention one final method which is a fusion of the previous two.

delegateByTypeBySig(address delegatee, uint8 delegationType)

Primary Use Case: Similar to delegateByType, but uses signatures to perform off-chain delegations, similar to delegateBySig.

Scenario: Alice wants to delegate her voting rights to Bob while retaining her right to proposition. She calls delegateByType("0xBob", 0) to send her voting rights to Bob.

Requirements:

  • Requires 2 or more recognized purposes of tokens.

  • This function relies on the offline signature creation process outlined in EIP712.

Advantages:

  • Gasless

  • Breaks governance responsibilities up into granular powers

Contract Upgradability

By design, smart contracts are immutable, meaning they can't be altered or changed once deployed on the blockchain. This provides users of the contract with favorable guarantees - namely that the code or 'terms' of the smart contract will not change during use. While this protects users against potentially malicious behavior, it does so at the expense of making it difficult to upgrade already shipped code.

As the nature of software development is iterative, it's important that we still find ways to ship bug fixes and major contract improvements. On Ethereum, this is done through the use of proxy contracts. Key to understanding proxy contracts is the idea of unbundling the different 'jobs' of a smart contract. Imagine a 'monolithic' contract, which we use to manage both the state and the updating of that state through logic. It's possible to define a Storage contract which contains important mappings of data, and a Logic contract that is permissioned to alter that data.

This alone isn't enough to solve our upgradability problem. Say we want to add some additional logic to our application. In a sense, we are trying to upgrade the system from Logic (V1) to Logic (V2). We can't do this if the permissions for Logic (V1) is hard-coded into the Storage contract. This demands the use of a Proxy - a contract recognized by Storage that points to the correct permissioned Logic contract.

In the original deployment, Proxy will point to Logic (V1), granting it permissions to update the state of Storage. At the time of an upgrade, an admin can change Proxy to point to Logic (V2) - and with it, expand the functionality of the system of contracts behind our application! If you require more details on implementation or with coded examples, you can look at this guide.

But what does this look like from the user's perspective? In this set up, a user's transaction will be directed to the proxy contract. The proxy contract will relay the msg.sender, msg.value, and execution variables to the logic contract for execution. This provides the added benefit that users interact with the same contract regardless of how many upgrades developers ship.

There are a number of known considerations when deploying proxies.

  • Storage Collisions: Ethereum smart contract storage occurs in fixed-sized slots, enumerated from 0 onwards. In a standard computer program, memory (i.e. RAM) is controlled so that data does not 'collide' in memory space (original data d1 being overwritten by new data d2). Ethereum does not natively account for collisions, so when using proxies it is important to adhere to a recognized standard for doing so. One such standard is EIP 1967. If you'd like to better understand this topic, you can read here.

  • Constructor: As is typical in software, constructor methods can only be called once at the time of instantiation. In Solidity, this occurs at the time of contract deployment. Because the Logic contract is abstracted away in the proxy contract model, the Logic contract's constructor will never be executed. It is then necessary to manually create an 'initialize' function that can be called by Proxy. It is good practice to program that this method can only be called once, as though it were truly a constructor.

Challenge #X Initialize() Attack Vector
As the init function is a public method, it can be called by a malicious actor. Upgrading the contract using initialize() involves two steps: Deploying the proxy contract Calling initialize() to set the contract address In the time between these two steps, it is possible for a malicious actor to call initialize() with their own contract address.
Solution
Introduce a modifier to allow only the initializing address to call initialize() Set the implementation contract to initialize only once()

Though we hope to have imparted a basic understanding of proxy contracts, we want to acknowledge this topic is deep and expansive. We encourage you to do your own research, and especially to read the OpenZeppelin docs which dive into more details on the scenarios mentioned above.

Access Control Functions

Many of the functions discussed above are used to manage core features of the token contract. It's easy to imagine the devastation that could result from misuse of _mint(), _burn(), or any of the allowance functions. For this reason, the idea of who (or what contract) can control certain methods is essential. We call this entire category of focus 'access control'.

On the blockchain, the notion of access control is far more complex than in traditional software systems. In web2 applications, it's typical to have a 'superuser' account with complete control of the database. The superuser creates a level of redundancy for the entire application; they are able to restore a deleted user account or even remove users from the platform. While it's possible, and perhaps even desired to create this redundancy

When restricting functions to a limited set of accounts, you have access control. In the spectrum of complexity, a single account ownership model is on the simpler side, with a set based multi-tier level hierarchy towards the complex.

Single account ownership, where the admin set their successor with a confirmation transaction, gives out an admin and pending admin model.

Security

This topic of security is subtly different from access control. Whereas access control concerns the nuances of which parties can use certain token methods, the idea of security concerns giving some accounts transcendent powers over the whole token system. By this, we mean to introduce something like a 'superuser' into your token contract.

This could be a single party or account with rights to reverse or altogether prevent certain types of transactions. Or an individual with the ability to block activity on an account altogether. You'll notice on the comparable table at the beginning of the document, that USDC is the only token with these features. Being a USD-based stablecoin, USDC is under much greater scrutiny to prevent their tokens from being used in fraudulent or unsavory activities.

For less regulated entities, the implementation of some form of a 'superuser' may still be desirable. In these cases, it's important to point out that security doesn't demand rigid single party control. It's possible to employ a system relying on a multi-sig wallet, trusted delegates, or at its most decentralized, a smart contract controlled through governance. In any case, such rights must be delicately designed, as they may introduce a single-point-of-failure into your system.

blacklist(address _account)

Primary Use Case: Used to block an address from receiving and sending tokens, as well as preventing any transfer of funds.

Scenario: Alice is the admin of her token contract. She wants to prevent a known malicious actor, Bob, from participating in her ecosystem. Alice calls blackList("0xBob").

Requirements:

Advantages:

  • The funds of malicious or suspect actors (such as hackers) can be frozen

  • The address of known or accounts can be systemically blocked, preventing users from sending their tokens to harmful spam accounts

  • This action is reversible, as there is a 'unBlacklist()' method that can remove an address from the list.

Disadvantages:

  • Every transaction involves assessing whether an address is blacklisted. This increases the gas cost of transferring the token.

  • Introduces the possibility of centralized censorship

  • Though the function blacklists an address, a token can still be accessed through a proxy contract wallet (eg. a Gnosis Safe) that interacts via a separate contract.

The blackList function described above is derived from USDC's implementation here.

pause()

Primary Use Case: Used to (reversibly) halt a contract's ability to send transactions.

Scenario: Alice is the admin of her token contract. A contract in her ecosystem has a known bug that could be exploited. Alice calls pause() on the contract to prevent transaction activity until a fix is deployed.

Requirements:

  • The pause function can only be called by a designated owner of the token contract

  • Contract must not already be paused

Advantages:

  • In the event of a hack, a contract can be paused to prevent the drainage of funds

  • In the event that a critical bug is found in production, the smart contract can be paused until it is upgraded via proxy to point to an improved contract

Disadvantage:

  • Introduces an element of censorship and centralization

This description of Pause comes from Open Zeppelin's implementation of Pausable.sol.