ERC-6808 Key-Bound Fungible Token (Fungible Key Bound Token)
Final · Tokens · created 2023-03-31
An optional security standard that binds two key wallets to an ERC-20 token so that transfers and approvals happen only under conditions the key wallets have allowed in advance. Even if a wallet is compromised, you can move the assets to safety or reset the settings.
Why it is needed
A self-custody wallet puts all authority on a single private key. Once a seed phrase or private key leaks, an attacker can move the tokens immediately, and there is no way to reverse it. According to the original document, from January 2021 to March 2022 the US Federal Trade Commission received more than 46,000 crypto fraud reports, and consumer losses exceeded 1 billion dollars. Decentralized alternatives such as hardware wallets require purchase and shipping, which limits adoption, and centralized services require creating an account and providing personal information. With an existing ERC-20 token, there is no means at all to protect the assets the moment the wallet is compromised. This standard tries to build an optional security feature into the asset itself rather than the wallet, giving a second chance to protect the tokens even if one of the three wallets is compromised.
What to implement
event AccountSecured(address _account, uint256 _amount): Emitted when an account activates security with addBindings. _amount is the balance at that point.event AccountResetBinding(address _account): Emitted when the holder resets the key wallet settings with resetBindings.event SafeFallbackActivated(address _account): Emitted when the holder decides to move all assets to a single key wallet with safeFallback.event AccountEnabledTransfer(address _account, uint256 _amount, uint256 _time, address _to, bool _allFunds): Emitted when a key wallet allows transfer conditions (amount, time, destination, whether the full balance) with allowTransfer.event AccountEnabledApproval(address _account, uint256 _time, uint256 _numberOfTransfers): Emitted when a key wallet sets the approval time window and the permitted number of transfers with allowApproval.event Ingress(address _account, uint256 _amount): Emitted when an account newly becomes a token holder. _amount is the current balance.event Egress(address _account, uint256 _amount): Emitted when an account sends all its tokens and is no longer a holder. _amount is the balance just before.function addBindings (address _keyWallet1, address _keyWallet2) external returns (bool): Binds two key wallets to the holding wallet and puts the account into a secured state. It MUST emit the AccountSecured event.function getBindings(address _account) external view returns (AccountHolderBindings memory): Returns the two key wallets bound to the account as a struct.function resetBindings() external returns (bool): Called from a key wallet to reset the key wallet settings of the holding account. Used when a key wallet is compromised.function safeFallback() external returns (bool);: Called from a key wallet to move all tokens of the holding account to another key wallet. Used when the holding wallet is compromised.function allowTransfer(uint256 _amount, uint256 _time, address _to, bool _allFunds) external returns (bool);: Called from a key wallet to allow in advance the amount, validity period, and destination address of transfers the holding wallet can make.function getTransferableFunds(address _account) external view returns (TransferConditions memory);: Returns the transfer allowance conditions set for the account as a struct.function allowApproval(uint256 _time, uint256 _numberOfTransfers) external returns (bool): Called from a key wallet to allow the time during which the holding wallet can use approve-type functions and the number of transfers by a third party.function getApprovalConditions(address _account) external view returns (ApprovalConditions memory);: Returns the approval allowance conditions (time, number of transfers) set for the account as a struct.function isSecureWallet(address _account) external view returns (bool): Returns whether the account is a secured account with key wallets bound to it.function transfer(address _to, uint256 _amount) external returns (bool): Transfers tokens. It must revert if the account is secured and has not been allowed through allowTransfer.function approve(address _spender, uint256 _amount) external returns (bool): Approves a spending limit for the spender. A secured account must first go through allowApproval, and the configured transfer count limit also applies.function increaseAllowance(address _spender, uint256 _addedValue) external returns (bool): Increases the spender's allowance. It must revert if allowApproval was not used or the allowed time has passed.function decreaseAllowance(address _spender, uint256 _subtractedValue) external returns (bool): Decreases the spender's allowance. The conditions are the same as increaseAllowance, and it must revert if the amount to subtract is larger than the current allowance.function transferFrom(address _from, address _to, uint256 _amount) external returns (bool): An approved spender moves tokens on behalf of the holder. Each call deducts from the remaining permitted transfer count, and when it reaches 0 the approval is revoked.
In depth
The 3-wallet structure and addBindings
When the holding wallet (Holding Wallet) that holds the tokens calls addBindings(_keyWallet1, _keyWallet2), the account becomes secured and the AccountSecured event is emitted. After that, the assets stay in the holding wallet but the authority to spend them is held by the key wallets, so responsibility is divided. It SHOULD revert if the caller is not a holder, the account is already secured, the two key wallets are the same, a key wallet is the same as the caller, a key wallet is the zero address, or a key wallet is already in use as a key wallet of another holding account. If addBindings is never used, it behaves as a regular ERC-20 by default.
Specifying conditions in allowTransfer
A key wallet calls allowTransfer(_amount, _time, _to, _allFunds) before transfer to allow a transfer. If _amount is 0 there is no amount limit, if _time is 0 there is no time limit, and if _to is the zero address there is no destination limit. If _allFunds is true, it allows the full balance to be sent to anyone at any time, regardless of the other parameters. It must revert if the caller is not a key wallet or _amount is greater than the holding account's balance, and on success it MUST emit the AccountEnabledTransfer event. If a secured account calls transfer without this allowance, the transfer must be reverted.
allowApproval and approval control
When a key wallet calls allowApproval(_time, _numberOfTransfers), the holding wallet can use approve, increaseAllowance and decreaseAllowance for _time, and the number of transfers the spender can make is limited to _numberOfTransfers (0 means unlimited within the allowance limit). Calling approve again overwrites both the allowance and the permitted number of transfers. Each time transferFrom is called, it decrements the remaining count and checks whether it has reached 0. When it reaches 0, it revokes the approval with an approve of amount 0. If a secure account calls an approve-type function without allowApproval, or after the configured time has passed, the call must revert.
Two recovery mechanisms: safeFallback and resetBindings
safeFallback is called from a key wallet when the holding wallet has been compromised or access to it has been lost. All tokens in the holding account move to a key wallet other than the one that made the call, and a SafeFallbackActivated event is emitted. resetBindings is called from a key wallet when a key wallet has been compromised. It removes the bound key wallets and emits an AccountResetBinding event. After the reset, the token behaves like a regular ERC-20, and the holding wallet can bind new key wallets by calling addBindings again. Both functions must revert if the caller is not a key wallet.
ERC-20 compatibility and optional security
FKBT is designed to inherit all the characteristics of ERC-20 (requires: 20). The aim is to let it work out of the box in existing dApps that use fungible tokens, and existing tokens and wallets continue to work unchanged, unaffected by the introduction of FKBT. Having allowTransfer and allowApproval return a bool on success and revert on failure is also a decision made for consistency with ERC-20. In addition, callers must handle a false value of returns (bool success) and must not assume that false is never returned.
Query functions and holder-tracking events
getBindings returns the two key wallets of an account, getTransferableFunds returns the transfer permission conditions (amount, time, to, allFunds), and getApprovalConditions returns the approval permission conditions (time, numberOfTransfers), each as a struct. The time in getApprovalConditions is the deadline, expressed as a block.timestamp value, until which approve-type functions can be called. getNumberOfTransfersAllowed reports the number of transfers remaining for the spender, and isSecureWallet reports whether an account is a secure account. When an account newly becomes a holder, an Ingress event is emitted, and when it sends its entire balance and stops being a holder, an Egress event is emitted. For reference, the original Test Cases record the average gas usage per function (addBindings 154,991, among others).
Things to watch out for
- Two key wallets must always be registered together: addBindings registers two key wallets at once. This also reduces fees and prevents human error, but if multiple wallets could be added freely, a critical situation could arise in safeFallback, so every KBT operates with a three-wallet scheme when security is activated.
- safeFallback moves assets to a key wallet other than the caller: If assets went to the key wallet that called safeFallback, an attacker could call that function and take all the assets. Sending them to the other key wallet, the one that did not make the call, removes the single point of failure.
- Only key wallets can call resetBindings: If the holding wallet could call resetBindings, an attacker who compromised only the holding wallet could reset the bindings and immediately take the tokens. For that reason, only _keyWallet1 and _keyWallet2 have reset authority.
- No defense if two wallets are compromised at the same time: If two of the three wallets are maliciously taken over, the owner can do nothing. However, a leak of up to one wallet is tolerated, so unlike existing standards, the owner gets a second chance.
- allowApproval is an additional defense against drainer-style attacks: When entrusting token use to a third party, limiting the time (_time) and the number of transfers (_numberOfTransfers) reduces exposure to common attacks such as malicious dApps that drain assets.