diff --git a/EIPS/eip-6809.md b/EIPS/eip-6809.md new file mode 100644 index 00000000000000..b642cc4e8a7de3 --- /dev/null +++ b/EIPS/eip-6809.md @@ -0,0 +1,517 @@ +--- +eip: 6809 +title: Non-Fungible Key Bound Token +description: An interface for Non-Fungible Key Bound Tokens, also known as an NFKBT. +author: Mihai Onila (@MihaiORO), Nick Zeman (@NickZCZ), Narcis Cotaie (@NarcisCRO) +discussions-to: https://ethereum-magicians.org/t/non-fungible-key-bound-token-kbt/13625 +status: Draft +type: Standards Track +category: ERC +created: 2023-03-31 +requires: 721 +--- + +## 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](#addbindings-function) function. This function designates **Key Wallets**[^1], which are responsible for conducting a **Safe Transfer**[^2]. During this process, **NFKBTs** are safely approved so they can be spent by the user or an on-chain third-party entity. + +The premise of **NFKBTs** is to provide fully optional security features built directly into the non-fungible asset, via the concept of _allow_ found in the [allowTransfer](#allowtransfer-function) and [allowApproval](#allowapproval-function) 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](./eip-721.md). 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](#addbindings-function) 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/museums. **NFKBTs** allow tokens to be more 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 **KBTs**. 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 continue to be 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 **KBTs** explored were the main motivation factors leading to their creation; + +1. **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. + +2. **Limitless Scalability:** Centralized solutions require the creation of an account and their availability may be restricted based on location. **NFKBTs** 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. **NFKBTs** can be used anywhere around the world by anyone who so wishes, provided they have access to the internet. + +3. **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 **NFKBTs**. + +4. **Default Functionality:** If the user would like to use **NFKBTs** 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. + +5. **Unmatched Security:** By calling the [addBindings](#addbindings-function) function a **Key Wallet**[^1] is now required for the [allowTransfer](#allowtransfer-function) or [allowApproval](#allowapproval-function) function. The [allowTransfer](#allowtransfer-function) function requires 4 parameters, `_tokenId`[^8], `_time`[^9], `_address`[^10], and `_anyToken`[^11], where as the [allowApproval](#allowapproval-function) function has 2 parameters, `_time`[^12] and `_numberOfTransfers`[^13]. In addition to this, **NFKBTs** have a [safeFallback](#safefallback-function) and [resetBindings](#resetbindings-function) 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. + +6. **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](#safefallback-function) 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](#resetbindings-function) function from `_keyWallet1`[^15] or `_keyWallet2`[^16]. This resets the **NFKBTs** security feature and allows the **Holding Wallet** to call the [addBindings](#addbindings-function) function again. New **Key Wallets** can therefore be added and a single point of failure can be prevented. + +7. **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. **NFKBTs** ensure user confidentially as everything can be done remotely under a pseudonym on the blockchain. + +8. **Low-Cost Security:** The cost of using **NFKBTs** 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. + +9. **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 **NFKBTs**, working hand in hand with Ethereum’s change to a _PoS_[^14] network. + +10. **User Experience:** The security feature can be activated by a simple call to the [addBindings](#addbindings-function) 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 **NFKBTs** 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` from `returns (bool success)`. Callers MUST NOT assume that `false` 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` through `allowTransfer` 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` through `allowTransfer` 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 the `allowApproval` 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 the `allowApproval` 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 **NFKBTs** 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](#allowtransfer-function) and [allowApproval](#allowapproval-function) 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](#security-considerations) section. + +## Backwards Compatibility + +**KBTs** 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 **NFKBTs**. + +## Test Cases + +The [assets](../assets/eip-6809/README.md) directory has all the [tests](../assets/eip-6809/test/kbt721.js). + +Average Gas used (_GWEI_): + +- `addBindings` - 155,096 +- `resetBindings` - 30,588 +- `safeFallback` - 72,221 (depending on how many NFTs the holder has) +- `allowTransfer` - 50,025 +- `allowApproval` - 44,983 + +## Reference Implementation + +The implementation is located in the [assets](../assets/eip-6809/README.md) directory. There's also a [diagram](../assets/eip-6809/Contract%20Interactions%20diagram.svg) with the contract interactions. + +## Security Considerations + +**NFKBTs** 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](#addbindings-function) 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](#safefallback-function) situation to occur. For this reason, all **KBTs** 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 **NFKBTs** 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-function). + +[safeFallback](#safefallback-function): 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](#safefallback-function) from one of the **Key Wallets**[^1]. **NFKBTs** are then redirected from the **Holding Wallet** to the other **Key Wallet**. + +By redirecting the **NFKBTs** it prevents a single point of failure. If an attacker were to call [safeFallback](#safefallback-function) and the **NFKBTs** redirected to the **Key Wallet**[^1] that called the function, they would gain access to all the **NFKBTs**. + +Scenario: **Key Wallet**[^1] has been compromised, call [resetBindings](#resetbindings-function). + +[resetBindings](#resetbindings-function): 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](#resetbindings-function), removing the bound **Key Wallets** and resetting the security features. The **NFKBTs** will now function as a traditional ERC-721 until [addBindings](#addbindings-function) 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](#resetbindings-function) function is because a **Holding Wallet**[^3] having the ability to call [resetBindings](#resetbindings-function) could result in an immediate loss of **NFKBTs**. The attacker would only need to gain access to the **Holding Wallet** and call [resetBindings](#resetbindings-function). + +In the scenario that 2 of the 3 wallets have been compromised, there is nothing the owner of the **NFKBTs** 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](#allowtransfer-function) 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 **NFKBTs** 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](#allowtransfer-function) 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](#allowapproval-function) function provides extra security when allowing on-chain third parties to use your **NFKBTs** 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](#allowapproval-function) 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](../LICENSE.md). + +[^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 **NFKBTs**. +[^3]: The **Holding Wallet** refers to the wallet containing the **NFKBTs**. +[^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. diff --git a/assets/eip-6809/.gitignore b/assets/eip-6809/.gitignore new file mode 100644 index 00000000000000..9bded852be91b4 --- /dev/null +++ b/assets/eip-6809/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +build/ +.env +.vscode +package-lock.json diff --git a/assets/eip-6809/Contract Interactions diagram.svg b/assets/eip-6809/Contract Interactions diagram.svg new file mode 100644 index 00000000000000..42ab31b03a3ba7 --- /dev/null +++ b/assets/eip-6809/Contract Interactions diagram.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/eip-6809/README.md b/assets/eip-6809/README.md new file mode 100644 index 00000000000000..79d4632748f2e7 --- /dev/null +++ b/assets/eip-6809/README.md @@ -0,0 +1,11 @@ +# EIP 6809 implementation + +This project is a reference implementation of EIP-6809. + +Try running some of the following tasks: + +```shell +npm i +truffle compile +truffle test +``` diff --git a/assets/eip-6809/contracts/IKBT721.sol b/assets/eip-6809/contracts/IKBT721.sol new file mode 100644 index 00000000000000..10828e79dffb18 --- /dev/null +++ b/assets/eip-6809/contracts/IKBT721.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.17; + +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); +} diff --git a/assets/eip-6809/contracts/KBT721.sol b/assets/eip-6809/contracts/KBT721.sol new file mode 100644 index 00000000000000..760948a12bbd95 --- /dev/null +++ b/assets/eip-6809/contracts/KBT721.sol @@ -0,0 +1,400 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.17; + +import "./IKBT721.sol"; +import "../node_modules/@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; +import "../node_modules/@openzeppelin/contracts/access/Ownable.sol"; + +contract KBT721 is IKBT721, ERC721Enumerable, Ownable { + mapping(address => AccountHolderBindings) private _holderAccounts; + mapping(address => FirstAccountBindings) private _firstAccounts; + mapping(address => SecondAccountBindings) private _secondAccounts; + + mapping(address => TransferConditions) private _transferConditions; + mapping(address => ApprovalConditions) private _approvalConditions; + + mapping(address => mapping(address => uint256)) + private _numberOfTransfersAllowed; + + constructor( + string memory _name, + string memory _symbol + ) ERC721(_name, _symbol) {} + + function addBindings( + address _keyWallet1, + address _keyWallet2 + ) external virtual override returns (bool) { + address sender = _msgSender(); + require(balanceOf(sender) > 0, "[200] KBT721: Wallet is not a holder"); + require( + _holderAccounts[sender].firstWallet == address(0) && + _holderAccounts[sender].secondWallet == address(0), + "[201] KBT721: Key wallets are already filled" + ); + require( + _keyWallet1 != address(0) && _keyWallet2 != address(0), + "[202] KBT721: Does not follow 0x standard" + ); + require( + _keyWallet1 != _keyWallet2, + "[205] KBT721: Key wallet 1 must be different than key wallet 2" + ); + require( + _keyWallet1 != sender, + "[206] KBT721: Key wallet 1 must be different than the sender" + ); + require( + sender != _keyWallet2, + "[207] KBT721: Key wallet 2 must be different than the sender" + ); + require( + _firstAccounts[_keyWallet1].accountHolderWallet == address(0), + "[203] KBT721: Key wallet 1 is already registered" + ); + require( + _secondAccounts[_keyWallet2].accountHolderWallet == address(0), + "[204] KBT721: Key wallet 2 is already registered" + ); + _holderAccounts[sender] = AccountHolderBindings({ + firstWallet: _keyWallet1, + secondWallet: _keyWallet2 + }); + _firstAccounts[_keyWallet1] = FirstAccountBindings({ + accountHolderWallet: sender, + secondWallet: _keyWallet2 + }); + _secondAccounts[_keyWallet2] = SecondAccountBindings({ + accountHolderWallet: sender, + firstWallet: _keyWallet1 + }); + emit AccountSecured(sender, balanceOf(sender)); + return true; + } + + function getBindings( + address _account + ) external view virtual override returns (AccountHolderBindings memory) { + return _holderAccounts[_account]; + } + + function resetBindings() external virtual override returns (bool) { + address accountHolder = _getAccountHolder(); + require( + accountHolder != address(0), + "[300] KBT721: Key authorization failure" + ); + delete _firstAccounts[_holderAccounts[accountHolder].firstWallet]; + delete _secondAccounts[_holderAccounts[accountHolder].secondWallet]; + delete _holderAccounts[accountHolder]; + emit AccountResetBinding(accountHolder); + return true; + } + + function safeFallback() external virtual override returns (bool) { + address accountHolder = _getAccountHolder(); + address otherSecureWallet = _getOtherSecureWallet(); + require( + accountHolder != address(0), + "[400] KBT721: Key authorization failure" + ); + + uint256 noOfTokens = balanceOf(accountHolder); + uint256 i = 0; + while (i++ < noOfTokens) { + uint256 tempTokenId = tokenOfOwnerByIndex(accountHolder, 0); + _transfer(accountHolder, otherSecureWallet, tempTokenId); + } + + emit SafeFallbackActivated(accountHolder); + + return true; + } + + function allowTransfer( + uint256 _tokenId, + uint256 _time, + address _to, + bool _anyToken + ) external virtual returns (bool) { + address accountHolder = _getAccountHolder(); + + require( + accountHolder != address(0), + "[500] KBT721: Key authorization failure" + ); + if (_tokenId > 0) { + address _owner = ownerOf(_tokenId); + require(_owner == accountHolder, "[501] KBT721: Invalid tokenId."); + } + + _time = _time > 0 ? (block.timestamp + _time) : 0; + + _transferConditions[accountHolder] = TransferConditions({ + tokenId: _tokenId, + time: _time, + to: _to, + anyToken: _anyToken + }); + + emit AccountEnabledTransfer( + accountHolder, + _tokenId, + _time, + _to, + _anyToken + ); + + return true; + } + + function getTransferableFunds( + address _account + ) external view returns (TransferConditions memory) { + return _transferConditions[_account]; + } + + function allowApproval( + uint256 _time, + uint256 _numberOfTransfers + ) external virtual returns (bool) { + address accountHolder = _getAccountHolder(); + require( + accountHolder != address(0), + "[600] KBT721: Key authorization failure" + ); + + _time = block.timestamp + _time; + + _approvalConditions[accountHolder].time = _time; + _approvalConditions[accountHolder] + .numberOfTransfers = _numberOfTransfers; + + emit AccountEnabledApproval(accountHolder, _time, _numberOfTransfers); + + return true; + } + + function getApprovalConditions( + address _account + ) external view returns (ApprovalConditions memory) { + return _approvalConditions[_account]; + } + + function getNumberOfTransfersAllowed( + address _account, + address _spender + ) external view returns (uint256) { + return _numberOfTransfersAllowed[_account][_spender]; + } + + function isSecureWallet(address _account) public view returns (bool) { + return + _holderAccounts[_account].firstWallet != address(0) && + _holderAccounts[_account].secondWallet != address(0); + } + + function isSecureToken( + uint256 _tokenId + ) public view virtual override returns (bool) { + address _owner = ownerOf(_tokenId); + + return isSecureWallet(_owner); + } + + // region ERC721 overrides + + function transferFrom( + address _from, + address _to, + uint256 _tokenId + ) public virtual override(ERC721, IERC721) { + address _sender = _msgSender(); + address _owner = ownerOf(_tokenId); + + if (_sender == _owner && isSecureWallet(_owner)) { + require( + _hasAllowedTransfer(_owner, _tokenId, _to), + "[100] KBT721: Sender is a secure wallet and doesn't have approval for the token" + ); + } + + super.transferFrom(_from, _to, _tokenId); + + if (_sender == _owner) { + delete _transferConditions[_owner]; + } else { + if (_numberOfTransfersAllowed[_owner][_sender] != 0) { + if (_numberOfTransfersAllowed[_owner][_sender] == 1) { + _setApprovalForAll(_owner, _sender, false); + } + _numberOfTransfersAllowed[_owner][_sender] -= 1; + } + } + } + + function safeTransferFrom( + address _from, + address _to, + uint256 _tokenId + ) public virtual override(ERC721, IERC721) { + safeTransferFrom(_from, _to, _tokenId, ""); + } + + function safeTransferFrom( + address _from, + address _to, + uint256 _tokenId, + bytes memory data + ) public virtual override(ERC721, IERC721) { + address _sender = _msgSender(); + address _owner = ownerOf(_tokenId); + + if (_sender == _owner && isSecureWallet(_owner)) { + require( + _hasAllowedTransfer(_owner, _tokenId, _to), + "[100] KBT721: Owner is a secure wallet and doesn't have approval for the token" + ); + } + + super.safeTransferFrom(_from, _to, _tokenId, data); + + if (_sender == _owner) { + delete _transferConditions[_owner]; + } else { + if (_numberOfTransfersAllowed[_owner][_sender] != 0) { + if (_numberOfTransfersAllowed[_owner][_sender] == 1) { + _setApprovalForAll(_owner, _sender, false); + } + _numberOfTransfersAllowed[_owner][_sender] -= 1; + } + } + } + + function approve( + address _to, + uint256 _tokenId + ) public virtual override(ERC721, IERC721) { + address _owner = ownerOf(_tokenId); + + if (isSecureWallet(_owner)) { + require( + _approvalConditions[_owner].time > 0, + "[101] KBT721: Spending of funds is not authorized." + ); + require( + _isApprovalAllowed(_owner), + "[102] KBT721: Time has expired for the spending of funds" + ); + } + + super.approve(_to, _tokenId); + + _numberOfTransfersAllowed[_owner][_to] = _approvalConditions[_owner] + .numberOfTransfers; + + delete _approvalConditions[_owner]; + } + + function setApprovalForAll( + address _operator, + bool _approved + ) public virtual override(ERC721, IERC721) { + address _sender = _msgSender(); + if (isSecureWallet(_sender)) { + require( + _approvalConditions[_sender].time > 0, + "[101] KBT721: Spending of funds is not authorized." + ); + require( + _isApprovalAllowed(_sender), + "[102] KBT721: Time has expired for the spending of funds" + ); + } + + super.setApprovalForAll(_operator, _approved); + + _numberOfTransfersAllowed[_sender][_operator] = _approvalConditions[ + _sender + ].numberOfTransfers; + + delete _approvalConditions[_sender]; + } + + function _afterTokenTransfer( + address _from, + address _to, + uint256 _firstTokenId, + uint256 _batchSize + ) internal virtual override { + if (_from != address(0)) { + // region update secureAccounts + if (isSecureWallet(_from) && balanceOf(_from) == 0) { + delete _firstAccounts[_holderAccounts[_from].firstWallet]; + delete _secondAccounts[_holderAccounts[_from].secondWallet]; + delete _holderAccounts[_from]; + } + // endregion + if (balanceOf(_from) == 0) { + emit Egress(_from, _firstTokenId); + } + } + + if (_to != address(0) && balanceOf(_to) == _batchSize) { + emit Ingress(_to, _firstTokenId); + } + } + + // endregion + + function _hasAllowedTransfer( + address _account, + uint256 _tokenId, + address _to + ) internal view returns (bool) { + TransferConditions memory conditions = _transferConditions[_account]; + + if (conditions.anyToken) { + return true; + } + + if ( + (conditions.tokenId == 0 && + conditions.time == 0 && + conditions.to == address(0)) || + (conditions.tokenId > 0 && conditions.tokenId != _tokenId) || + (conditions.time > 0 && conditions.time < block.timestamp) || + (conditions.to != address(0) && conditions.to != _to) + ) { + return false; + } + + return true; + } + + function _isApprovalAllowed(address account) internal view returns (bool) { + return _approvalConditions[account].time >= block.timestamp; + } + + function _getAccountHolder() internal view returns (address) { + address sender = _msgSender(); + return + _firstAccounts[sender].accountHolderWallet != address(0) + ? _firstAccounts[sender].accountHolderWallet + : ( + _secondAccounts[sender].accountHolderWallet != address(0) + ? _secondAccounts[sender].accountHolderWallet + : address(0) + ); + } + + function _getOtherSecureWallet() internal view returns (address) { + address sender = _msgSender(); + address accountHolder = _getAccountHolder(); + + return + _holderAccounts[accountHolder].firstWallet == sender + ? _holderAccounts[accountHolder].secondWallet + : _holderAccounts[accountHolder].firstWallet; + } +} diff --git a/assets/eip-6809/contracts/MyFirstKBT.sol b/assets/eip-6809/contracts/MyFirstKBT.sol new file mode 100644 index 00000000000000..c8b0596ca03fed --- /dev/null +++ b/assets/eip-6809/contracts/MyFirstKBT.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.17; + +import "./KBT721.sol"; + +contract MyFirstKBT is KBT721 { + constructor() KBT721("MyFirstKBT", "FirstKBT") {} + + function safeMint( + address to, + uint256 tokenId + ) external virtual onlyOwner returns (bool) { + _safeMint(to, tokenId); + + return true; + } +} diff --git a/assets/eip-6809/migrations/1_deploy_contracts.js b/assets/eip-6809/migrations/1_deploy_contracts.js new file mode 100644 index 00000000000000..f70c4f14e0aca3 --- /dev/null +++ b/assets/eip-6809/migrations/1_deploy_contracts.js @@ -0,0 +1,5 @@ +const FirstKBT = artifacts.require("MyFirstKBT"); + +module.exports = function (deployer) { + deployer.deploy(FirstKBT); +}; diff --git a/assets/eip-6809/package.json b/assets/eip-6809/package.json new file mode 100644 index 00000000000000..6c5339980ffba5 --- /dev/null +++ b/assets/eip-6809/package.json @@ -0,0 +1,10 @@ +{ + "dependencies": { + "@openzeppelin/test-helpers": "^0.5.16", + "@truffle/hdwallet-provider": "^2.1.7", + "dotenv": "^16.0.3" + }, + "devDependencies": { + "@openzeppelin/contracts": "^4.8.2" + } +} diff --git a/assets/eip-6809/test/kbt721.js b/assets/eip-6809/test/kbt721.js new file mode 100644 index 00000000000000..add32a5a178018 --- /dev/null +++ b/assets/eip-6809/test/kbt721.js @@ -0,0 +1,321 @@ +const { + BN, // Big Number support + constants, // Common constants, like the zero address and largest integers + expectEvent, // Assertions for emitted events + expectRevert, // Assertions for transactions that should fail + time, +} = require('@openzeppelin/test-helpers'); +const { web3 } = require('@openzeppelin/test-helpers/src/setup'); + +const FirstKBT = artifacts.require("MyFirstKBT"); + +contract('FirstKBT', (accounts) => { + + let instance; + + before(async () => { + instance = await FirstKBT.new(); + }); + + const [deploymentAccount, accountHolder, secondAccountHolder, firstAccount, secondAccount, + thirdAccount, fourthAccount, spenderAccount] = accounts; + + const tokenIds = [11, 22, 33, 34, 35]; + const secondTokenIds = [44, 55, 66]; + + it('1. Check the MyFirstKBT after deployment', async () => { + const name = await instance.name(); + assert.equal(name, name, 'Name should be "MyFirstKBT" after deployment'); + }); + + it("2. accountHolder and secondAccountHolder should have 0 tokens", async function () { + let balance = await instance.balanceOf(accountHolder); + assert.equal(balance.toNumber(), 0, "accountHolder has more than 0"); + + balance = await instance.balanceOf(secondAccountHolder); + assert.equal(balance.toNumber(), 0, "secondAccountHolder has more than 0"); + }); + + it("3. addBindings : CANNOT add secure wallets if balance is 0", async function () { + await expectRevert( + instance.addBindings(firstAccount, secondAccount, { from: accountHolder }), + "[200] KBT721: Wallet is not a holder" + ); + const accountHolderBindings = await instance.getBindings(accountHolder); + assert.equal(accountHolderBindings.firstWallet, constants.ZERO_ADDRESS, "First account is not empty"); + assert.equal(accountHolderBindings.secondWallet, constants.ZERO_ADDRESS, "Second account is not empty"); + }); + + it("4. mint: Owner should be able to mint tokens for accountHolder", async function () { + for (i = 0; i < tokenIds.length; i++) { + let tokenId = tokenIds[i]; + const event = await instance.safeMint(accountHolder, tokenId, { from: deploymentAccount }); + printGasUsed(event, 'safeMint'); + + let owner = await instance.ownerOf(tokenId); + assert.equal(owner, accountHolder, "Owner should be able to mint."); + + const token = await instance.tokenOfOwnerByIndex(accountHolder, i); + assert.equal(token.toNumber(), tokenId, "TokenId does not match"); + } + + const balance = await instance.balanceOf(accountHolder); + assert.equal(balance.toNumber(), tokenIds.length, "accountHolder should have tokens"); + }); + + it("5. transfer: CAN 1 token be transferred from accountHolder to secondAccountHolder", async function () { + const tokenId = 11; + const balanceAccountHolderOrig = await instance.balanceOf(accountHolder); + const balanceSecondAccountHolderOrig = await instance.balanceOf(secondAccountHolder); + + const event = await instance.safeTransferFrom(accountHolder, secondAccountHolder, tokenId, { from: accountHolder }); + printGasUsed(event, 'safeTransferFrom'); + + const balanceAccountHolder = await instance.balanceOf(accountHolder); + assert.equal(balanceAccountHolder.toNumber(), balanceAccountHolderOrig.toNumber() - 1, "transfer failed: accountHolder balance is wrong"); + + const balanceSecondAccountHolder = await instance.balanceOf(secondAccountHolder); + assert.equal(balanceSecondAccountHolder.toNumber(), balanceSecondAccountHolderOrig.toNumber() + 1, "transfer failed: secondAccountHolder balance is wrong"); + + const owner = await instance.ownerOf(tokenId); + assert.equal(owner, secondAccountHolder, "transfer failed: new owner is not secondAccountHolder"); + }); + + it("6. addBindings : CAN add secure wallets | EVENT: AccountSecured was emitted", async function () { + emitted = await instance.addBindings(firstAccount, secondAccount, { from: accountHolder }); + printGasUsed(emitted, 'addBindings'); + balance = await instance.balanceOf(accountHolder); + expectEvent(emitted, "AccountSecured", { _account: accountHolder, _noOfTokens: balance }); + + accountHolderBindings = await instance.getBindings(accountHolder); + assert.equal(accountHolderBindings.firstWallet, firstAccount, "First account was not set as a secure wallet"); + assert.equal(accountHolderBindings.secondWallet, secondAccount, "Second account was not set as a secure wallet"); + }); + + it("7. isSecureToken : Token 11 should not be secure, Token 22 should be secure", async function () { + isSecure = await instance.isSecureToken(11); + assert.isFalse(isSecure, "Token 11 is not a secure token"); + + isSecure = await instance.isSecureToken(22); + assert.isTrue(isSecure, "Token 22 is not a secure token"); + }); + + it("8. addBindings : CANNOT add secure wallets a second time", async function () { + expectRevert( + instance.addBindings(thirdAccount, fourthAccount, { from: accountHolder }), + "[201] KBT721: Key wallets are already filled" + ); + }); + + it("9. addBindings : CANNOT add secure wallets that are already secure wallets to another account", async function () { + await mintTokensTmp(secondTokenIds, instance, secondAccountHolder, deploymentAccount); + + expectRevert( + instance.addBindings(firstAccount, fourthAccount, { from: secondAccountHolder }), + "[203] KBT721: Key wallet 1 is already registered" + ); + expectRevert( + instance.addBindings(thirdAccount, secondAccount, { from: secondAccountHolder }), + "[204] KBT721: Key wallet 2 is already registered" + ); + }); + + it("10. addBindings : accountHolder is a secure wallet", function () { + instance.isSecureWallet(accountHolder).then(function (result) { + assert.equal(result, true, "accountHolder is not a secure wallet"); + }); + }); + + it("11. addBindings : firstAccount is NOT a secure wallet", function () { + instance.isSecureWallet(firstAccount).then(function (result) { + assert.equal(result, false, "firstAccount is a secure wallet"); + }); + }); + + it("12. transfer : accountHolder CANNOT transfer token 22 before allowTransfer", async function () { + tokenId = 22; + await expectRevert(instance.safeTransferFrom(accountHolder, secondAccountHolder, tokenId, { from: accountHolder }), "[100] KBT721: Owner is a secure wallet and doesn't have approval for the token"); + }); + + it("13. allowTransfer : firstAccount CAN Unlock token 22 | EVENT: AccountEnabledTransfer", async function () { + tokenId = 22; + emitted = await instance.allowTransfer(tokenId, 0, constants.ZERO_ADDRESS, false, { from: firstAccount }); + printGasUsed(emitted, 'allowTransfer'); + expectEvent(emitted, "AccountEnabledTransfer", { _account: accountHolder, _tokenId: new BN(tokenId), _time: new BN(0), _to: constants.ZERO_ADDRESS, _anyToken: false }); + result = await instance.getTransferableFunds(accountHolder); + assert.equal(result.tokenId, tokenId, "accountHolder does not have " + tokenId + " token unlocked"); + expectRevert(instance.allowTransfer(44, 0, constants.ZERO_ADDRESS, false, { from: firstAccount }), "[501] KBT721: Invalid tokenId."); + }); + + it("14. transfer : accountHolder CAN transfer token 22 to secondAccountHolder", async function () { + tokenId = 22; + initialBalance = (await instance.balanceOf(secondAccountHolder)).toNumber(); + + await instance.safeTransferFrom(accountHolder, secondAccountHolder, tokenId, { from: accountHolder }); + + balance = (await instance.balanceOf(secondAccountHolder)).toNumber(); + assert.equal(balance, initialBalance + 1, "transfer failed"); + }); + + it("15. approve : account holder CAN'T approve (without Authorize Spending UNLOCKED)", async function () { + tokenId = 34; + expectRevert(instance.approve(spenderAccount, tokenId, { from: accountHolder }), "[101] KBT721: Spending of funds is not authorized."); + }); + + it("16. allowApproval : firstAccount CAN Authorize Spending | EVENT: AccountEnabledApproval ", async function () { + tokenId = 34; + numberOfTransfers = 1; + + emitted = await instance.allowApproval(tokenId, numberOfTransfers, { from: firstAccount }); + printGasUsed(emitted, 'allowApproval'); + forTime = new BN(await time.latest()); + forTime = forTime.add(new BN(tokenId)); + forNumberOfTransfers = new BN(numberOfTransfers); + expectEvent(emitted, "AccountEnabledApproval", { _account: accountHolder, _time: forTime, _numberOfTransfers: forNumberOfTransfers }); + result = await instance.getApprovalConditions(accountHolder); + assert.isAbove(Number(result.time), 0, "accountHolder does not have Authorize Spending"); + }); + + it("17. transferFrom : spenderAccount CANNOT transferFrom accountHolder without Approval", function () { + tokenId = 34; + expectRevert(instance.transferFrom(accountHolder, secondAccountHolder, tokenId, { from: spenderAccount }), "ERC721: caller is not token owner or approved"); + }); + + it("18. approve : account holder CAN approve spenderAccount", async function () { + tokenId = 34; + owner = await instance.ownerOf(tokenId); + emitted = await instance.approve(spenderAccount, tokenId, { from: accountHolder }); + printGasUsed(emitted, 'approve'); + spender = await instance.getApproved(tokenId); + assert.equal(spender, spenderAccount, "Approval didn't went as planned.") + }); + + it("19. transferFrom : spenderAccount CAN transferFrom accountHolder", async function () { + tokenId = 34; + emitted = await instance.transferFrom(accountHolder, secondAccountHolder, tokenId, { from: spenderAccount }); + printGasUsed(emitted, 'transferFrom'); + }); + + it("20. transferFrom: spenderAccount CAN transferFrom accountHolder as much as they want", async function () { + tempTokenIds = [101, 102, 103]; + i = 0; + while (i < tempTokenIds.length) { + await instance.safeMint(accountHolder, tempTokenIds[i], { from: deploymentAccount }); + i++; + } + + await instance.allowApproval(100, tempTokenIds.length, { from: firstAccount }); + await instance.setApprovalForAll(spenderAccount, true, { from: accountHolder }); + i = 0; + while (i < tempTokenIds.length) { + emitted = await instance.transferFrom(accountHolder, secondAccountHolder, tempTokenIds[i], { from: spenderAccount }); + i++; + } + }); + + it("21. transferFrom: spenderAccount CAN transferFrom accountHolder but no more than he's allowed", async function () { + + tempTokenIds = [104, 105, 106]; + i = 0; + while (i < tempTokenIds.length) { + await instance.safeMint(accountHolder, tempTokenIds[i], { from: deploymentAccount }); + i++; + } + + await instance.allowApproval(100, tempTokenIds.length - 1, { from: firstAccount }); + await instance.setApprovalForAll(spenderAccount, true, { from: accountHolder }); + i = 0; + while (i < tempTokenIds.length - 1) { + emitted = await instance.transferFrom(accountHolder, secondAccountHolder, tempTokenIds[i], { from: spenderAccount }); + i++; + } + + expectRevert(instance.transferFrom(accountHolder, secondAccountHolder, tempTokenIds[i], { from: spenderAccount }), "ERC721: caller is not token owner or approved"); + }); + + it("22. transferFrom: when spenderAccount transfers ALL funds accountHolder becomes unsecure", async function () { + tempTokenIds = await getTokenIds(instance, accountHolder); + i = 0; + while (i < tempTokenIds.length) { + tempTokenId = tempTokenIds[i]; + + await instance.allowTransfer(tempTokenId, 0, constants.ZERO_ADDRESS, false, { from: firstAccount }); + await instance.transferFrom(accountHolder, secondAccountHolder, tempTokenId, { from: accountHolder }); + i++; + } + const binding = await instance.getBindings(accountHolder); + + assert(binding.firstWallet === constants.ZERO_ADDRESS && + binding.secondWallet === constants.ZERO_ADDRESS, + "accountHolder is still secure"); + + }); + + it("23. safeFallback : accountHolder becomes unsecure, other 2FA wallet has at least accountHolder funds | EVENT: SafeFallbackActivated", async function () { + const tokenId = 7; + await instance.safeMint(accountHolder, tokenId, { from: deploymentAccount }); + expectRevert(instance.safeFallback({ from: firstAccount }), "[400] KBT721: Key authorization failure"); + await instance.addBindings(firstAccount, secondAccount, { from: accountHolder }); + + balance = (await instance.balanceOf(accountHolder)).toNumber(); + emitted = await instance.safeFallback({ from: firstAccount }); + printGasUsed(emitted, 'safeFallback'); + expectEvent(emitted, "SafeFallbackActivated", { _account: accountHolder }); + + secondAccountBalance = (await instance.balanceOf(secondAccount)).toNumber(); + assert.isAtLeast(secondAccountBalance, balance, "second account doesn't have the full amount from account holder"); + + accountHolderBindings = await instance.getBindings(accountHolder); + assert.equal(accountHolderBindings.firstWallet, constants.ZERO_ADDRESS, "accountHolder is still secure"); + assert.equal(accountHolderBindings.secondWallet, constants.ZERO_ADDRESS, "accountHolder is still secure"); + }); + + it("24. resetBindings : accountHolder becomes unsecure | EVENT: AccountResetBinding", async function () { + await instance.safeMint(accountHolder, 100, { from: deploymentAccount }); + await instance.addBindings(firstAccount, secondAccount, { from: accountHolder }); + + emitted = await instance.resetBindings({ from: firstAccount }); + printGasUsed(emitted, 'resetBindings'); + expectEvent(emitted, "AccountResetBinding", { _account: accountHolder }); + + accountHolderBindings = await instance.getBindings(accountHolder); + assert.equal(accountHolderBindings.firstWallet, constants.ZERO_ADDRESS, "accountHolder is still secure"); + assert.equal(accountHolderBindings.secondWallet, constants.ZERO_ADDRESS, "accountHolder is still secure"); + }); + +}); + +async function mintTokensTmp(tokenIds, instance, accountHolder, deploymentAccount) { + for (i = 0; i < tokenIds.length; i++) { + let tokenId = tokenIds[i]; + await instance.safeMint(accountHolder, tokenId, { from: deploymentAccount }); + } +} + +async function listTokens(instance, accountHolder) { + const noOfTokens = (await instance.balanceOf(accountHolder)).toNumber(); + let i = 0; + while (i < noOfTokens) { + let tempToken = (await instance.tokenOfOwnerByIndex(accountHolder, i)).toNumber(); + console.log(i + ": " + tempToken); + i++; + } +} + +async function getTokenIds(instance, accountHolder) { + const noOfTokens = (await instance.balanceOf(accountHolder)).toNumber(); + let tokenIds = []; + let i = 0; + while (i < noOfTokens) { + let tempToken = (await instance.tokenOfOwnerByIndex(accountHolder, i)).toNumber(); + tokenIds.push(tempToken); + i++; + } + + return tokenIds; +} + +function printGasUsed(event, methodName) { + const gasUsed = event.receipt.gasUsed; + console.log(`GasUsed: ${gasUsed.toLocaleString()} for '${methodName}'`); +} diff --git a/assets/eip-6809/truffle-config.js b/assets/eip-6809/truffle-config.js new file mode 100644 index 00000000000000..4f9fd7c50012a2 --- /dev/null +++ b/assets/eip-6809/truffle-config.js @@ -0,0 +1,153 @@ +/** + * Use this file to configure your truffle project. It's seeded with some + * common settings for different networks and features like migrations, + * compilation, and testing. Uncomment the ones you need or modify + * them to suit your project as necessary. + * + * More information about configuration can be found at: + * + * https://trufflesuite.com/docs/truffle/reference/configuration + * + * Hands-off deployment with Infura + * -------------------------------- + * + * Do you have a complex application that requires lots of transactions to deploy? + * Use this approach to make deployment a breeze 🏖️: + * + * Infura deployment needs a wallet provider (like @truffle/hdwallet-provider) + * to sign transactions before they're sent to a remote public node. + * Infura accounts are available for free at 🔍: https://infura.io/register + * + * You'll need a mnemonic - the twelve word phrase the wallet uses to generate + * public/private key pairs. You can store your secrets 🤐 in a .env file. + * In your project root, run `$ npm install dotenv`. + * Create .env (which should be .gitignored) and declare your MNEMONIC + * and Infura PROJECT_ID variables inside. + * For example, your .env file will have the following structure: + * + * MNEMONIC = + * PROJECT_ID = + * + * Deployment with Truffle Dashboard (Recommended for best security practice) + * -------------------------------------------------------------------------- + * + * Are you concerned about security and minimizing rekt status 🤔? + * Use this method for best security: + * + * Truffle Dashboard lets you review transactions in detail, and leverages + * MetaMask for signing, so there's no need to copy-paste your mnemonic. + * More details can be found at 🔎: + * + * https://trufflesuite.com/docs/truffle/getting-started/using-the-truffle-dashboard/ + */ + +require('dotenv').config(); +// const { MNEMONIC, PROJECT_ID } = process.env; + +const HDWalletProvider = require('@truffle/hdwallet-provider'); + +//to fetch these keys from .env file +const privateKey = process.env.PRIVATE_KEY; +const infura_api_key = process.env.INFURA_API_KEY; +const etherscan_api_key = process.env.ETHERSCAN_API_KEY; + +module.exports = { + /** + * Networks define how you connect to your ethereum client and let you set the + * defaults web3 uses to send transactions. If you don't specify one truffle + * will spin up a managed Ganache instance for you on port 9545 when you + * run `develop` or `test`. You can ask a truffle command to use a specific + * network from the command line, e.g + * + * $ truffle test --network + */ + plugins: [ + 'truffle-plugin-verify' + ], + api_keys: { + etherscan: etherscan_api_key + }, + + networks: { + // Useful for testing. The `development` name is special - truffle uses it by default + // if it's defined here and no other network is specified at the command line. + // You should run a client (like ganache, geth, or parity) in a separate terminal + // tab if you use this network and you must also set the `host`, `port` and `network_id` + // options below to some value. + // + development: { + host: "127.0.0.1", // Localhost (default: none) + port: 7545, // Standard Ethereum port (default: none) + network_id: "*", // Any network (default: none) + }, + // + // An additional network, but with some advanced options… + // advanced: { + // port: 8777, // Custom port + // network_id: 1342, // Custom network + // gas: 8500000, // Gas sent with each transaction (default: ~6700000) + // gasPrice: 20000000000, // 20 gwei (in wei) (default: 100 gwei) + // from:
, // Account to send transactions from (default: accounts[0]) + // websocket: true // Enable EventEmitter interface for web3 (default: false) + // }, + // + // Useful for deploying to a public network. + // Note: It's important to wrap the provider as a function to ensure truffle uses a new provider every time. + goerli: { + provider: () => new HDWalletProvider(privateKey, `https://goerli.infura.io/v3/${infura_api_key}`), + network_id: 5, // Goerli's id + gas: 5000000, //gas limit + confirmations: 1, // # of confirmations to wait between deployments. (default: 0) + timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50) + skipDryRun: true // Skip dry run before migrations? (default: false for public nets ) + }, + // + // Useful for private networks + // private: { + // provider: () => new HDWalletProvider(MNEMONIC, `https://network.io`), + // network_id: 2111, // This network is yours, in the cloud. + // production: true // Treats this network as if it was a public net. (default: false) + // } + }, + + // Set default mocha options here, use special reporters, etc. + mocha: { + // timeout: 100000 + }, + + // Configure your compilers + compilers: { + solc: { + version: "0.8.17", // Fetch exact version from solc-bin (default: truffle's version) + // docker: true, // Use "0.5.1" you've installed locally with docker (default: false) + settings: { // See the solidity docs for advice about optimization and evmVersion + optimizer: { + enabled: true, + runs: 200 + }, + // evmVersion: "byzantium" + } + } + } + + // Truffle DB is currently disabled by default; to enable it, change enabled: + // false to enabled: true. The default storage location can also be + // overridden by specifying the adapter settings, as shown in the commented code below. + // + // NOTE: It is not possible to migrate your contracts to truffle DB and you should + // make a backup of your artifacts to a safe location before enabling this feature. + // + // After you backed up your artifacts you can utilize db by running migrate as follows: + // $ truffle migrate --reset --compile-all + // + // db: { + // enabled: false, + // host: "127.0.0.1", + // adapter: { + // name: "indexeddb", + // settings: { + // directory: ".db" + // } + // } + // } +};