This ERC proposes an extension to the ERC-20 token standard by adding Encumber—the ability for an account to grant another account exclusive right to move some portion of their balance. Encumber is a stronger version of ERC-20 allowances. While ERC-20 approve grants another account the permission to transfer a specified token amount, encumber grants the same permission while ensuring that the tokens will be available when needed.
Motivation
This extension adds flexibility to the ERC-20 token standard and caters to use cases where token locking is required, but it is preferential to maintain actual ownership of tokens. This interface can also be adapted to other token standards, such as ERC-721, in a straightforward manner
Token holders commonly transfer their tokens to smart contracts which will return the tokens under specific conditions. In some cases, smart contracts do not actually need to hold the tokens, but need to guarantee they will be available if necessary. Since allowances do not provide a strong enough guarantee, the only way to do guarantee token availability presently is to transfer the token to the smart contract. Locking tokens without moving them gives more clear indication of the rights and ownership of the tokens. This allows for airdrops and other ancillary benefits of ownership to reach the true owner. It also adds another layer of safety, where draining a pool of ERC-20 tokens can be done in a single transfer, iterating accounts to transfer encumbered tokens would be significantly more prohibitive in gas usage.
Specification
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119 and RFC 8174.
A compliant token MUST implement the following interface
/**
* @dev Interface of the ERC-7246 standard.
*/interfaceIERC7246{/**
* @dev Emitted when `amount` tokens are encumbered from `owner` to `taker`.
*/eventEncumber(addressindexedowner,addressindexedtaker,uintamount);/**
* @dev Emitted when the encumbrance of a `taker` to an `owner` is reduced by `amount`.
*/eventRelease(addressindexedowner,addressindexedtaker,uintamount);/**
* @dev Returns the total amount of tokens owned by `owner` that are currently encumbered.
* MUST never exceed `balanceOf(owner)`
*
* Any function which would reduce balanceOf(owner) below encumberedBalanceOf(owner) MUST revert
*/functionencumberedBalanceOf(addressowner)externalreturns(uint);/**
* @dev Returns the number of tokens that `owner` has encumbered to `taker`.
*
* This value increases when {encumber} or {encumberFrom} are called by the `owner` or by another permitted account.
* This value decreases when {release} and {transferFrom} are called by `taker`.
*/functionencumbrances(addressowner,addresstaker)externalreturns(uint);/**
* @dev Increases the amount of tokens that the caller has encumbered to `taker` by `amount`.
* Grants to `taker` a guaranteed right to transfer `amount` from the caller's balance by using `transferFrom`.
*
* MUST revert if caller does not have `amount` tokens available
* (e.g. if `balanceOf(caller) - encumbrances(caller) < amount`).
*
* Emits an {Encumber} event.
*/functionencumber(addresstaker,uintamount)external;/**
* @dev Increases the amount of tokens that `owner` has encumbered to `taker` by `amount`.
* Grants to `taker` a guaranteed right to transfer `amount` from `owner` using transferFrom
*
* The function SHOULD revert unless the owner account has deliberately authorized the sender of the message via some mechanism.
*
* MUST revert if `owner` does not have `amount` tokens available
* (e.g. if `balanceOf(owner) - encumbrances(owner) < amount`).
*
* Emits an {Encumber} event.
*/functionencumberFrom(addressowner,addresstaker,uintamount)external;/**
* @dev Reduces amount of tokens encumbered from `owner` to caller by `amount`
*
* Emits a {Release} event.
*/functionrelease(addressowner,uintamount)external;/**
* @dev Convenience function for reading the unencumbered balance of an address.
* Trivially implemented as `balanceOf(owner) - encumberedBalanceOf(owner)`
*/functionavailableBalanceOf(addressowner)publicviewreturns(uint);}
Rationale
The specification was designed to complement and mirror the ERC-20 specification to ease adoption and understanding. The specification is intentionally minimally proscriptive of this joining, where the only true requirement is that an owner cannot transfer encumbered tokens. However, the example implementation includes some decisions about where to connect with ERC-20 functions worth noting. It was designed for minimal invasiveness of standard ERC-20 definitions.
- The example has a dependency on approve to facilitate encumberFrom. This proposal allows for an implementer to define another mechanism, such as an approveEncumber which would mirror ERC-20 allowances, if desired.
- transferFrom(src, dst, amount) is written to first reduce the encumbrances(src, amount), and then subsequently spend from allowance(src, msg.sender). Alternatively, transferFrom could be implemented to spend from allowance simultaneously to spending encumbrances. This would require approve to check that the approved balance does not decrease beneath the amount required by encumbered balances, and also make creating the approval a prerequisite to calling encumber.
It is possible to stretch the Encumber interface to cover ERC-721 tokens by using the tokenId in place of amount param since they are both uint. The interface opts for clarity with the most likely use case (ERC-20), even if it is compatible with other formats.
Backwards Compatibility
This EIP is backwards compatible with the existing ERC-20 standard. Implementations must add the functionality to block transfer of tokens that are encumbered to another account.
Reference Implementation
This can be implemented as an extension of any base ERC-20 contract by modifying the transfer function to block the transfer of encumbered tokens and to release encumbrances when spent via transferFrom.
// An erc-20 token that implements the encumber interface by blocking transfers.
pragmasolidity^0.8.0;import{ERC20}from"../lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";import{IERC7246}from"./IERC7246.sol";contractEncumberableERC20isERC20,IERC7246{// Owner -> Taker -> Amount that can be taken
mapping(address=>mapping(address=>uint))publicencumbrances;// The encumbered balance of the token owner. encumberedBalance must not exceed balanceOf for a user
// Note this means rebasing tokens pose a risk of diminishing and violating this prototocol
mapping(address=>uint)publicencumberedBalanceOf;addresspublicminter;constructor(stringmemoryname,stringmemorysymbol)ERC20(name,symbol){minter=msg.sender;}functionmint(addressrecipient,uintamount)public{require(msg.sender==minter,"only minter");_mint(recipient,amount);}functionencumber(addresstaker,uintamount)external{_encumber(msg.sender,taker,amount);}functionencumberFrom(addressowner,addresstaker,uintamount)external{require(allowance(owner,msg.sender)>=amount);_encumber(owner,taker,amount);}functionrelease(addressowner,uintamount)external{_release(owner,msg.sender,amount);}// If bringing balance and encumbrances closer to equal, must check
functionavailableBalanceOf(addressa)publicviewreturns(uint){return(balanceOf(a)-encumberedBalanceOf[a]);}function_encumber(addressowner,addresstaker,uintamount)private{require(availableBalanceOf(owner)>=amount,"insufficient balance");encumbrances[owner][taker]+=amount;encumberedBalanceOf[owner]+=amount;emitEncumber(owner,taker,amount);}function_release(addressowner,addresstaker,uintamount)private{if(encumbrances[owner][taker]<amount){amount=encumbrances[owner][taker];}encumbrances[owner][taker]-=amount;encumberedBalanceOf[owner]-=amount;emitRelease(owner,taker,amount);}functiontransfer(addressdst,uintamount)publicoverridereturns(bool){// check but dont spend encumbrance
require(availableBalanceOf(msg.sender)>=amount,"insufficient balance");_transfer(msg.sender,dst,amount);returntrue;}functiontransferFrom(addresssrc,addressdst,uintamount)publicoverridereturns(bool){uintencumberedToTaker=encumbrances[src][msg.sender];boolexceedsEncumbrance=amount>encumberedToTaker;if(exceedsEncumbrance){uintexcessAmount=amount-encumberedToTaker;// check that enough enencumbered tokens exist to spend from allowance
require(availableBalanceOf(src)>=excessAmount,"insufficient balance");// Exceeds Encumbrance , so spend all of it
_spendEncumbrance(src,msg.sender,encumberedToTaker);_spendAllowance(src,dst,excessAmount);}else{_spendEncumbrance(src,msg.sender,amount);}_transfer(src,dst,amount);returntrue;}function_spendEncumbrance(addressowner,addresstaker,uint256amount)internalvirtual{uint256currentEncumbrance=encumbrances[owner][taker];require(currentEncumbrance>=amount,"insufficient encumbrance");uintnewEncumbrance=currentEncumbrance-amount;encumbrances[owner][taker]=newEncumbrance;encumberedBalanceOf[owner]-=amount;}}
Security Considerations
Parties relying on balanceOf to determine the amount of tokens available for transfer should instead rely on balanceOf(account) - encumberedBalance(account), or, if implemented, availableBalanceOf(account).
The property that encumbered balances are always backed by a token balance can be accomplished in a straightforward manner by altering transfer and transferFrom to block . If there are other functions that can alter user balances, such as a rebasing token or an admin burn function, additional guards must be added by the implementer to likewise ensure those functions prevent reducing balanceOf(account) below encumberedBalanceOf(account) for any given account.