Appearance
Abstract
A standard interface for Non-Fungible Key Bound Tokens (NFKBT/s), a subset of the more general Key Bound Tokens (KBT/s).
The following standardizes an API for tokens within smart contracts and provides basic functionality to the addBindings function. This function designates Key Wallets[^1], which are responsible for conducting a Safe Transfer[^2]. During this process, NFKBT's are safely approved so they can be spent by the user or an on-chain third-party entity.
The premise of NFKBT's is to provide fully optional security features built directly into the non-fungible asset, via the concept of allow found in the allowTransfer and allowApproval functions. These functions are called by one of the Key Wallets[^1] and allow the Holding Wallet[^3] to either call the already familiar transferFrom
and approve
function found in ERC-721. Responsibility for the NFKBT is therefore split. The Holding Wallet contains the asset and Key Wallets have authority over how the assets can be spent or approved. Default Behaviors[^4] of a traditional non-fungible ERC-721 can be achieved by simply never using the addBindings function.
We considered NFKBTs being used by every individual who wishes to add additional security to their non-fungible assets, as well as consignment to third-party wallets/brokers/banks/insurers/galleries. NFKBTs are resilient to attacks/thefts, by providing additional protection to the asset itself on a self-custodial level.
Motivation
In this fast-paced technologically advancing world, people learn and mature at different speeds. The goal of global adoption must take into consideration the target demographic is of all ages and backgrounds. Unfortunately for self-custodial assets, one of the greatest pros is also one of its greatest cons. The individual is solely responsible for their actions and adequately securing their assets. If a mistake is made leading to a loss of funds, no one is able to guarantee their return.
From January 2021 through March 2022, the United States Federal Trade Commission received more than 46,000[^5] crypto scam reports. This directly impacted crypto users and resulted in a net consumer loss exceeding $1 Billion[^6]. Theft and malicious scams are an issue in any financial sector and oftentimes lead to stricter regulation. However, government-imposed regulation goes against one of this space’s core values. Efforts have been made to increase security within the space through centralized and decentralized means. Up until now, no one has offered a solution that holds onto the advantages of both whilst eliminating their disadvantages.
We asked ourselves the same question as many have in the past, “How does one protect the wallet?”. After a while, realizing the question that should be asked is “How does one protect the asset?”. Creating the wallet is free, the asset is what has value and is worth protecting. This question led to the development of KBT's. A solution that is fully optional and can be tailored so far as the user is concerned. Individual assets remain protected even if the seed phrase or private key is publicly released, as long as the security feature was activated.
NFKBTs saw the need to improve on the widely used non-fungible ERC-721 token standard. The security of non-fungible assets is a topic that concerns every entity in the crypto space, as their current and future use cases are continuously explored. NFKBTs provide a scalable decentralized security solution that takes security one step beyond wallet security, focusing on the token's ability to remain secure. The security is on the blockchain itself, which allows every demographic that has access to the internet to secure their assets without the need for current hardware or centralized solutions. Made to be a promising alternative, NFKBTs inherit all the characteristics of an ERC-721. This was done so NFKBTs could be used on every dApp that is configured to use traditional non-fungible tokens.
During the development process, the potential advantages KBT's explored were the main motivation factors leading to their creation;
Completely Decentralized: The security features are fully decentralized meaning no third-party will have access to user funds when activated. This was done to truly stay in line with the premise of self-custodial assets, responsibility and values.
Limitless Scalability: Centralized solutions require the creation of an account and their availability may be restricted based on location. NFKBT's do not face regional restrictions or account creation. Decentralized security solutions such as hardware options face scalability issues requiring transport logistics, secure shipping and vendor. NFKBT's can be used anywhere around the world by anyone who so wishes, provided they have access to the internet.
Fully Optional Security: Security features are optional, customizable and removable. It’s completely up to the user to decide the level of security they would like when using NFKBT's.
Default Functionality: If the user would like to use NFKBT's as a traditional ERC-721, the security features do not have to be activated. As the token inherits all of the same characteristics, it results in the token acting with traditional non-fungible Default Behaviors[^4]. However, even when the security features are activated, the user will still have the ability to customize the functionality of the various features based on their desired outcome. The user can pass a set of custom and or Default Values[^7] manually or through a dApp.
Unmatched Security: By calling the addBindings function a Key Wallet[^1] is now required for the allowTransfer or allowApproval function. The allowTransfer function requires 4 parameters,
_tokenId
[^8],_time
[^9],_address
[^10], and_anyToken
[^11], where as the allowApproval function has 2 parameters,_time
[^12] and_numberOfTransfers
[^13]. In addition to this, NFKBT's have a safeFallback and resetBindings function. The combination of all these prevent and virtually cover every single point of failure that is present with a traditional ERC-721, when properly used.Security Fail-Safes: With NFKBTs, users can be confident that their tokens are safe and secure, even if the Holding Wallet[^3] or one of the Key Wallets[^1] has been compromised. If the owner suspects that the Holding Wallet has been compromised or lost access, they can call the safeFallback function from one of the Key Wallets. This moves the assets to the other Key Wallet preventing a single point of failure. If the owner suspects that one of the Key Wallets has been comprised or lost access, the owner can call the resetBindings function from
_keyWallet1
[^15] or_keyWallet2
[^16]. This resets the NFKBT's security feature and allows the Holding Wallet to call the addBindings function again. New Key Wallets can therefore be added and a single point of failure can be prevented.Anonymous Security: Frequently, centralized solutions ask for personal information that is stored and subject to prying eyes. Purchasing decentralized hardware solutions are susceptible to the same issues e.g. a shipping address, payment information, or a camera recording during a physical cash pick-up. This may be considered by some as infringing on their privacy and asset anonymity. NFKBT's ensure user confidentially as everything can be done remotely under a pseudonym on the blockchain.
Low-Cost Security: The cost of using NFKBT's security features correlate to on-chain fees, the current GWEI at the given time. As a standalone solution, they are a viable cost-effective security measure feasible to the majority of the population.
Environmentally Friendly: Since the security features are coded into the NFKBT, there is no need for centralized servers, shipping, or the production of physical object/s. Thus leading to a minimal carbon footprint by the use of NFKBT's, working hand in hand with Ethereum’s change to a PoS[^14] network.
User Experience: The security feature can be activated by a simple call to the addBindings function. The user will only need two other wallets, which will act as
_keyWallet1
[^15] and_keyWallet2
[^16], to gain access to all of the benefits NFKBT's offer. The optional security features improve the overall user experience and Ethereum ecosystem by ensuring a safety net for those who decide to use it. Those that do not use the security features are not hindered in any way. This safety net can increase global adoption as people can remain confident in the security of their assets, even in the scenario of a compromised wallet.
Specification
IKBT721
(Token Contract)
NOTES:
- The following specifications use syntax from Solidity
0.8.17
(or above) - Callers MUST handle
false
fromreturns (bool success)
. Callers MUST NOT assume thatfalse
is never returned!
solidity
interface IKBT721 {
event AccountSecured(address indexed _account, uint256 _noOfTokens);
event AccountResetBinding(address indexed _account);
event SafeFallbackActivated(address indexed _account);
event AccountEnabledTransfer(
address _account,
uint256 _tokenId,
uint256 _time,
address _to,
bool _anyToken
);
event AccountEnabledApproval(
address _account,
uint256 _time,
uint256 _numberOfTransfers
);
event Ingress(address _account, uint256 _tokenId);
event Egress(address _account, uint256 _tokenId);
struct AccountHolderBindings {
address firstWallet;
address secondWallet;
}
struct FirstAccountBindings {
address accountHolderWallet;
address secondWallet;
}
struct SecondAccountBindings {
address accountHolderWallet;
address firstWallet;
}
struct TransferConditions {
uint256 tokenId;
uint256 time;
address to;
bool anyToken;
}
struct ApprovalConditions {
uint256 time;
uint256 numberOfTransfers;
}
function addBindings(
address _keyWallet1,
address _keyWallet2
) external returns (bool);
function getBindings(
address _account
) external view returns (AccountHolderBindings memory);
function resetBindings() external returns (bool);
function safeFallback() external returns (bool);
function allowTransfer(
uint256 _tokenId,
uint256 _time,
address _to,
bool _allTokens
) external returns (bool);
function getTransferableFunds(
address _account
) external view returns (TransferConditions memory);
function allowApproval(
uint256 _time,
uint256 _numberOfTransfers
) external returns (bool);
function getApprovalConditions(
address account
) external view returns (ApprovalConditions memory);
function getNumberOfTransfersAllowed(
address _account,
address _spender
) external view returns (uint256);
function isSecureWallet(address _account) external returns (bool);
function isSecureToken(uint256 _tokenId) external returns (bool);
}
Events
AccountSecured
event
Emitted when the _account
is securing his account by calling the addBindings
function.
_amount
is the current balance of the _account
.
solidity
event AccountSecured(address _account, uint256 _amount)
AccountResetBinding
event
Emitted when the holder is resetting his keyWallets
by calling the resetBindings
function.
solidity
event AccountResetBinding(address _account)
SafeFallbackActivated
event
Emitted when the holder is choosing to move all the funds to one of the keyWallets
by calling the safeFallback
function.
solidity
event SafeFallbackActivated(address _account)
AccountEnabledTransfer
event
Emitted when the _account
has allowed for transfer _amount
of tokens for the _time
amount of block
seconds for _to
address (or if the _account
has allowed for transfer all funds though _anyToken
set to true
) by calling the allowTransfer
function.
solidity
event AccountEnabledTransfer(address _account, uint256 _amount, uint256 _time, address _to, bool _allFunds)
AccountEnabledApproval
event
Emitted when _account
has allowed approval for the _time
amount of block
seconds by calling the allowApproval
function.
solidity
event AccountEnabledApproval(address _account, uint256 _time)
Ingress
event
Emitted when _account
becomes a holder. _amount
is the current balance of the _account
.
solidity
event Ingress(address _account, uint256 _amount)
Egress
event
Emitted when _account
transfers all his tokens and is no longer a holder. _amount
is the previous balance of the _account
.
solidity
event Egress(address _account, uint256 _amount)
Interface functions
The functions detailed below MUST be implemented.
addBindings
function
Secures the sender account with other two wallets called _keyWallet1
and _keyWallet2
and MUST fire the AccountSecured
event.
The function SHOULD revert
if:
- the sender account is not a holder
- or the sender is already secured
- or the keyWallets are the same
- or one of the keyWallets is the same as the sender
- or one or both keyWallets are zero address (
0x0
) - or one or both keyWallets are already keyWallets to another holder account
solidity
function addBindings (address _keyWallet1, address _keyWallet2) external returns (bool)
getBindings
function
The function returns the keyWallets
for the _account
in a struct
format.
solidity
struct AccountHolderBindings {
address firstWallet;
address secondWallet;
}
solidity
function getBindings(address _account) external view returns (AccountHolderBindings memory)
resetBindings
function
Note: This function is helpful when one of the two keyWallets
is compromised.
Called from a keyWallet
, the function resets the keyWallets
for the holder
account. MUST fire the AccountResetBinding
event.
The function SHOULD revert
if the sender is not a keyWallet
.
solidity
function resetBindings() external returns (bool)
safeFallback
function
Note: This function is helpful when the holder
account is compromised.
Called from a keyWallet
, this function transfers all the tokens from the holder
account to the other keyWallet
and MUST fire the SafeFallbackActivated
event.
The function SHOULD revert
if the sender is not a keyWallet
.
solidity
function safeFallback() external returns (bool);
allowTransfer
function
Called from a keyWallet
, this function is called before a transferFrom
or safeTransferFrom
functions are called.
It allows to transfer a tokenId, for a specific time frame, to a specific address.
If the tokenId is 0 then there will be no restriction on the tokenId. If the time is 0 then there will be no restriction on the time. If the to address is zero address then there will be no restriction on the to address. Or if _anyToken
is true
, regardless of the other params, it allows any token, whenever, to anyone to be transferred of the holder.
The function MUST fire AccountEnabledTransfer
event.
The function SHOULD revert
if the sender is not a keyWallet
for a holder or if the owner of the _tokenId
is different than the holder
.
solidity
function allowTransfer(uint256 _tokenId, uint256 _time, address _to, bool _anyToken) external returns (bool);
getTransferableFunds
function
The function returns the transfer conditions for the _account
in a struct
format.
solidity
struct TransferConditions {
uint256 tokenId;
uint256 time;
address to;
bool anyToken;
}
solidity
function getTransferableFunds(address _account) external view returns (TransferConditions memory);
allowApproval
function
Called from a keyWallet
, this function is called before approve
or setApprovalForAll
functions are called.
It allows the holder
for a specific amount of _time
to do an approve
or setApprovalForAll
and limit the number of transfers the spender is allowed to do through _numberOfTransfers
(0 - unlimited number of transfers in the allowance limit).
The function MUST fire AccountEnabledApproval
event.
The function SHOULD revert
if the sender is not a keyWallet
.
solidity
function allowApproval(uint256 _time) external returns (bool)
getApprovalConditions
function
The function returns the approval conditions in a struct format. Where time
is the block.timestamp
until the approve
or setApprovalForAll
functions can be called, and numberOfTransfers
is the number of transfers the spender will be allowed.
solidity
struct ApprovalConditions {
uint256 time;
uint256 numberOfTransfers;
}
solidity
function getApprovalConditions(address _account) external view returns (ApprovalConditions memory);
transferFrom
function
The function transfers from _from
address to _to
address the _tokenId
token.
Each time a spender calls the function the contract subtracts and checks if the number of allowed transfers of that spender has reached 0, and when that happens, the approval is revoked using a set approval for all to false
.
The function MUST fire the Transfer
event.
The function SHOULD revert
if:
- the sender is not the owner or is not approved to transfer the
_tokenId
- or if the
_from
address is not the owner of the_tokenId
- or if the sender is a secure account and it has not allowed for transfer this
_tokenId
throughallowTransfer
function.
solidity
function transferFrom(address _from, address _to, uint256 _tokenId) external returns (bool)
safeTransferFrom
function
The function transfers from _from
address to _to
address the _tokenId
token.
The function MUST fire the Transfer
event.
The function SHOULD revert
if:
- the sender is not the owner or is not approved to transfer the
_tokenId
- or if the
_from
address is not the owner of the_tokenId
- or if the sender is a secure account and it has not allowed for transfer this
_tokenId
throughallowTransfer
function.
solidity
function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes memory data) external returns (bool)
safeTransferFrom
function, with data parameter
This works identically to the other function with an extra data parameter, except this function just sets data to "".
solidity
function safeTransferFrom(address _from, address _to, uint256 _tokenId) external returns (bool)
approve
function
The function allows _to
account to transfer the _tokenId
from the sender account.
The function also limits the _to
account to the specific number of transfers set in the ApprovalConditions
for that holder
account. If the value is 0
then the _spender
can transfer multiple times.
The function MUST fire an Approval
event.
If the function is called again it overrides the number of transfers allowed with _numberOfTransfers
, set in allowApproval
function.
The function SHOULD revert
if:
- the sender is not the current NFT owner, or an authorized operator of the current owner
- the NFT owner is secured and has not called
allowApproval
function - or if the
_time
, set in theallowApproval
function, has elapsed.
solidity
function approve(address _to, uint256 _tokenId) public virtual override(ERC721, IERC721)
setApprovalForAll
function
The function enables or disables approval for another account _operator
to manage all of sender assets.
The function also limits the _to
account to the specific number of transfers set in the ApprovalConditions
for that holder
account. If the value is 0
then the _spender
can transfer multiple times.
The function Emits an Approval
event indicating the updated allowance.
If the function is called again it overrides the number of transfers allowed with _numberOfTransfers
, set in allowApproval
function.
The function SHOULD revert
if:
- the sender account is secured and has not called
allowApproval
function - or if the
_spender
is a zero address (0x0
) - or if the
_time
, set in theallowApproval
function, has elapsed.
solidity
function setApprovalForAll(address _operator, bool _approved) public virtual override(ERC721, IERC721)
Rationale
The intent from individual technical decisions made during the development of NFKBTs focused on maintaining consistency and backward compatibility with ERC-721s, all the while offering self-custodial security features to the user. It was important that NFKBT's inherited all of ERC-721s characteristics to comply with requirements found in dApps which use non-fungible tokens on their platform. In doing so, it allowed for flawless backward compatibility to take place and gave the user the choice to decide if they want their NFKBTs to act with Default Behaviors[^4]. We wanted to ensure that wide-scale implementation and adoption of NFKBTs could take place immediately, without the greater collective needing to adapt and make changes to the already flourishing decentralized ecosystem.
For developers and users alike, the allowTransfer and allowApproval functions both return bools on success and revert on failures. This decision was done purposefully, to keep consistency with the already familiar ERC-721. Additional technical decisions related to self-custodial security features are broken down and located within the Security Considerations section.
Backwards Compatibility
KBT's are designed to be backward-compatible with existing token standards and wallets. Existing tokens and wallets will continue to function as normal, and will not be affected by the implementation of NFKBT's.
Test Cases
The assets directory has all the tests.
Average Gas used (GWEI):
addBindings
- 155,096resetBindings
- 30,588safeFallback
- 72,221 (depending on how many NFTs the holder has)allowTransfer
- 50,025allowApproval
- 44,983
Reference Implementation
The implementation is located in the assets directory. There's also a diagram with the contract interactions.
Security Considerations
NFKBT's were designed with security in mind every step of the way. Below are some design decisions that were rigorously discussed and thought through during the development process.
Key Wallets[^1]: When calling the addBindings function for an NFKBT, the user must input 2 wallets that will then act as _keyWallet1
[^15] and _keyWallet2
[^16]. They are added simultaneously to reduce user fees, minimize the chance of human error and prevent a pitfall scenario. If the user had the ability to add multiple wallets it would not only result in additional fees and avoidable confusion but would enable a potentially disastrous safeFallback situation to occur. For this reason, all KBT's work under a 3-wallet system when security features are activated.
Typically if a wallet is compromised, the non-fungible assets within are at risk. With NFKBT's there are two different functions that can be called from a Key Wallet[^1] depending on which wallet has been compromised.
Scenario: Holding Wallet[^3] has been compromised, call safeFallback.
safeFallback: This function was created in the event that the owner believes the Holding Wallet[^3] has been compromised. It can also be used if the owner losses access to the Holding Wallet. In this scenario, the user has the ability to call safeFallback from one of the Key Wallets[^1]. NFKBT's are then redirected from the Holding Wallet to the other Key Wallet.
By redirecting the NFKBT's it prevents a single point of failure. If an attacker were to call safeFallback and the NFKBT's redirected to the Key Wallet[^1] that called the function, they would gain access to all the NFKBT's.
Scenario: Key Wallet[^1] has been compromised, call resetBindings.
resetBindings: This function was created in the event that the owner believes _keyWallet1
[^15] or _keyWallet2
[^16] has been compromised. It can also be used if the owner losses access to one of the Key Wallets[^1]. In this instance, the user has the ability to call resetBindings, removing the bound Key Wallets and resetting the security features. The NFKBT's will now function as a traditional ERC-721 until addBindings is called again and a new set of Key Wallets are added.
The reason why _keyWallet1
[^15] or _keyWallet2
[^16] are required to call the resetBindings function is because a Holding Wallet[^3] having the ability to call resetBindings could result in an immediate loss of NFKBT's. The attacker would only need to gain access to the Holding Wallet and call resetBindings.
In the scenario that 2 of the 3 wallets have been compromised, there is nothing the owner of the NFKBT's can do if the attack is malicious. However, by allowing 1 wallet to be compromised, holders of non-fungible tokens built using the NFKBT standard are given a second chance, unlike other current standards.
The allowTransfer function is in place to guarantee a Safe Transfer[^2], but can also have Default Values[^7] set by a dApp to emulate Default Behaviors[^3] of a traditional ERC-721. It enables the user to highly specify the type of transfer they are about to conduct, whilst simultaneously allowing the user to unlock all the NFKBT's to anyone for an unlimited amount of time. The desired security is completely up to the user.
This function requires 4 parameters to be filled and different combinations of these result in different levels of security;
Parameter 1 _tokenId
[^8]: This is the ID of the NFKBT that will be spent on a transfer.
Parameter 2 _time
[^9]: The number of blocks the NFKBT can be transferred starting from the current block timestamp.
Parameter 3 _address
[^10]: The destination the NFKBT will be sent to.
Parameter 4 _anyToken
[^11]: This is a boolean value. When false, the transferFrom
function takes into consideration Parameters 1, 2 and 3. If the value is true, the transferFrom
function will revert to a Default Behavior[^4], the same as a traditional ERC-721.
The allowTransfer function requires _keyWallet1
[^15] or _keyWallet2
[^16] and enables the Holding Wallet[^3] to conduct a transferFrom
within the previously specified parameters. These parameters were added in order to provide additional security by limiting the Holding Wallet in case it was compromised without the user's knowledge.
The allowApproval function provides extra security when allowing on-chain third parties to use your NFKBT's on your behalf. This is especially useful when a user is met with common malicious attacks e.g. draining dApp.
This function requires 2 parameters to be filled and different combinations of these result in different levels of security;
Parameter 1 _time
[^12]: The number of blocks that the approval of a third-party service can take place, starting from the current block timestamp.
Parameter 2 _numberOfTransfers_
[^13]: The number of transactions a third-party service can conduct on the user's behalf.
The allowApproval function requires _keyWallet1
[^15] or _keyWallet2
[^16] and enables the Holding Wallet[^3] to allow a third-party service by using the approve
function. These parameters were added to provide extra security when granting permission to a third-party that uses assets on the user's behalf. Parameter 1, _time
[^12], is a limitation to when the Holding Wallet can approve
a third-party service. Parameter 2, _numberOfTransfers
[^13], is a limitation to the number of transactions the approved third-party service can conduct on the user's behalf before revoking approval.
Copyright
Copyright and related rights waived via CC0.
[^1]: The Key Wallet/s refers to _keyWallet1
or _keyWallet2
which can call the safeFallback
, resetBindings
, allowTransfer
and allowApproval
functions. [^2]: A Safe Transfer is when 1 of the Key Wallets safely approved the use of the NFKBT's. [^3]: The Holding Wallet refers to the wallet containing the NFKBT's. [^4]: A Default Behavior/s refers to behavior/s present in the preexisting non-fungible ERC-721 standard. [^5]: The number of crypto scam reports the United States Federal Trade Commission received, from January 2021 through March 2022. [^6]: The amount stolen via crypto scams according to the United States Federal Trade Commission, from January 2021 through March 2022. [^7]: A Default Value/s refer to a value/s that emulates the non-fungible ERC-721 Default Behavior/s. [^8]: The _tokenId
represents the ID of the NFKBT intended to be spent. [^9]: The _time
in allowTransfer
represents the number of blocks a transferFrom
can take place in. [^10]: The _address
represents the address that the NFKBT will be sent to. [^11]: The _anyToken
is a bool that can be set to true or false. [^12]: The _time
in allowApproval
represents the number of blocks an approve
can take place in. [^13]: The _numberOfTransfers
is the number of transfers a third-party entity can conduct via transferFrom
on the user's behalf. [^14]: A PoS protocol, Proof-of-Stake protocol, is a cryptocurrency consensus mechanism for processing transactions and creating new blocks in a blockchain. [^15]: The _keyWallet1
is 1 of the 2 Key Wallets set when calling the addBindings
function. [^16]: The _keyWallet2
is 1 of the 2 Key Wallets set when calling the addBindings
function.