diff --git a/neps/nep-0452.md b/neps/nep-0452.md new file mode 100644 index 000000000..3fb1c6472 --- /dev/null +++ b/neps/nep-0452.md @@ -0,0 +1,243 @@ +--- +NEP: 452 +Title: Linkdrop Standard +Author: Ben Kurrek , Ken Miyachi +DiscussionsTo: https://gov.near.org/t/official-linkdrop-standard/32463/1 +Status: Draft +Type: Standards Track +Category: Contract +Version: 1.0.0 +Created: 24-Jan-2023 +Updated: 19-Apr-2023 +--- + +## Summary + +A standard interface for linkdrops that support $NEAR, fungible tokens, non-fungible tokens, and is extensible to support new types in the future. + +Linkdrops are a simple way to send assets to someone by providing them with a link. This link can be embedded into a QR code, sent via email, text or any other means. Within the link, there is a private key that allows the holder to call a method that can create an account and send it assets. Alternatively, if the holder has an account, the assets can be sent there as well. + +By definition, anyone with an access key can interact with the blockchain and since there is a private key embedded in the link, this removes the need for the end-user to have a wallet. + +## Motivation + +Linkdrops are an extremely powerful tool that enable seamless onboarding and instant crypto experiences with the click of a link. The original [near-linkdrop](https://github.com/near/near-linkdrop) contract provides a minimal interface allowing users to embed $NEAR within an access key and create a simple Web2 style link that can then be used as a means of onboarding. This simple $NEAR linkdrop is not enough as many artists, developers, event coordinators, and applications want to drop more digital assets such as NFTs, FTs, tickets etc. + +As linkdrop implementations start to push the boundaries of what’s possible, new data structures, methods, and interfaces are being developed. There needs to be a standard data model and interface put into place to ensure assets can be claimed independent of the contract they came from. If not, integrating any application with linkdrops will require customized solutions, which would become cumbersome for the developer and deteriorate the user onboarding experience. The linkdrop standard addresses these issues by providing a simple and extensible standard data model and interface. + +The initial discussion can be found [here](https://gov.near.org/t/official-linkdrop-standard/32463/1). + +## Specification + +### Example Scenarios + +_Pre-requisite Steps_: Linkdrop creation: +The linkdrop creator that has an account with some $NEAR: + +- creates a keypair locally (`pubKey1`, `privKey1`). (The keypair is not written to chain at this time) +- calls a method on a contract that implements the linkdrop standard in order to create the drop. The `pubKey1` and desired $NEAR amount are both passed in as arguments. +- The contract maps the `pubKey1` to the desired balance for the linkdrop (`KeyInfo` record). +- The contract then adds the `pubKey1` as a function call access key with the ability to call `claim` and `create_account_and_claim`. This means that anyone with the `privKey1` (see above), can sign a transaction on behalf of the contract (signer id set to contract id) with a function call to call one of the mentioned functions to claim the assets. + +#### Claiming a linkdrop without a NEAR Account + +A user with _no_ account can claim the assets associated with an existing public key, already registered in the linkdrop contract: + +- generates a new keypair (`pubKey2`, `privKey2`) locally. (This new keypair is not written to chain) +- chooses a new account ID such as benji.near. +- calls `create_account_and_claim`. The transaction is signed on behalf of the linkdrop contract (`signer_id` is set to the contract address) using `privKey1`. + - the args of this function call will contain both `pubKey2` (which will be used to create a full access key for the new account) and the account ID itself. + - the linkdrop contract will delete the access key associated with `pubKey1` so that it cannot be used again. + - the linkdrop contract will create the new account and transfer the funds to it alongside any other assets. +- the user will be able to sign transactions on behalf of the new account using `privKey2`. + +#### Claiming a linkdrop with a NEAR Account + +A user with an _existing_ account can claim the assets with an existing public key, already registered in the linkdrop contract: + +- calls `claim`. The transaction is signed on behalf of the linkdrop contract (`signer_id` is set to the contract address) using `privKey1`. + - the args of this function call will simply contain the user's existing account ID. + - the linkdrop contract will delete the access key associated with `pubKey1` so that it cannot be used again. + - the linkdrop contract will transfer the funds to that account alongside any other assets. + +```ts +/// Information about a specific public key. +type KeyInfo = { + /// How much Gas should be attached when the key is used to call `claim` or `create_account_and_claim`. + /// It is up to the smart contract developer to calculate the required gas (which can be done either automatically on the contract or on the client-side). + required_gas: string, + + /// yoctoNEAR$ amount that will be sent to the account that claims the linkdrop (either new or existing) + /// when the key is successfully used. + yoctonear: string, + + /// If using the NFT standard extension, a set of NFTData can be linked to the public key + /// indicating that all those assets will be sent to the account that claims the linkdrop (either new or + /// existing) when the key is successfully used. + nft_list: NFTData[] | null, + + /// If using the FT standard extension, a set of FTData can be linked to the public key + /// indicating that all those assets will be sent to the account that claims the linkdrop (either new or + /// existing) when the key is successfully used. + ft_list: FTData[] | null + + /// ... other types can be introduced and the standard is easily extendable. +} + + +/// Data outlining a specific Non-Fungible Token that should be sent to the claiming account +/// (either new or existing) when a key is successfully used. +type NFTData = { + /// the id of the token to transfer + token_id: string, + + /// The valid NEAR account indicating the Non-Fungible Token contract. + contract_id: string +} + + +/// Data outlining Fungible Tokens that should be sent to the claiming account +/// (either new or existing) when a key is successfully used. +type FTData = { + /// The number of tokens to transfer, wrapped in quotes and treated + /// like a string, although the number will be stored as an unsigned integer + /// with 128 bits. + amount: string, + + /// The valid NEAR account indicating the Fungible Token contract. + contract_id: string +} + +/****************/ +/* VIEW METHODS */ +/****************/ + +/// Allows you to query for the amount of $NEAR tokens contained in a linkdrop corresponding to a given public key. +/// +/// Requirements: +/// * Panics if the key does not exist. +/// +/// Arguments: +/// * `key` the public counterpart of the key used to sign, expressed as a string with format ":" (e.g. "ed25519:6TupyNrcHGTt5XRLmHTc2KGaiSbjhQi1KHtCXTgbcr4Y") +/// +/// Returns a string representing the $yoctoNEAR amount associated with a given public key +function get_key_balance(key: string) -> string; + +/// Allows you to query for the `KeyInfo` corresponding to a given public key. This method is preferred over `get_key_balance` as it provides more information about the key. +/// +/// Requirements: +/// * Panics if the key does not exist. +/// +/// Arguments: +/// * `key` the public counterpart of the key used to sign, expressed as a string with format ":" (e.g. "ed25519:6TupyNrcHGTt5XRLmHTc2KGaiSbjhQi1KHtCXTgbcr4Y") +/// +/// Returns `KeyInfo` associated with a given public key +function get_key_information(key: string) -> KeyInfo; + +/******************/ +/* CHANGE METHODS */ +/******************/ + +/// Transfer all assets linked to the signer’s public key to an `account_id`. +/// If the transfer fails for whatever reason, it is up to the smart contract developer to +/// choose what should happen. For example, the contract can choose to keep the assets +/// or send them back to the original linkdrop creator. +/// +/// Requirements: +/// * The predecessor account *MUST* be the current contract ID. +/// * The `account_id` MUST be an *initialized* NEAR account. +/// * The assets being sent *MUST* be associated with the signer’s public key. +/// * The assets *MUST* be sent to the `account_id` passed in. +/// +/// Arguments: +/// * `account_id` the account that should receive the linkdrop assets. +/// +/// Returns `true` if the claim was successful meaning all assets were sent to the `account_id`. +function claim(account_id: string) -> Promise; + +/// Creates a new NEAR account and transfers all assets linked to the signer’s public key to +/// the *newly created account*. If the transfer fails for whatever reason, it is up to the +/// smart contract developer to choose what should happen. For example, the contract can +/// choose to keep the assets or return them to the original linkdrop creator. +/// +/// Requirements: +/// * The predecessor account *MUST* be the current contract ID. +/// * The assets being sent *MUST* be associated with the signer’s public key. +/// * The assets *MUST* be sent to the `new_account_id` passed in. +/// * The newly created account *MUST* have a new access key added to its account (either +/// full or limited access) in the same receipt that the account was created in. +/// * The Public key must be in a binary format with base58 string serialization with human-readable curve. +/// The key types currently supported are secp256k1 and ed25519. Ed25519 public keys accepted are 32 bytes +/// and secp256k1 keys are the uncompressed 64 format. +/// +/// Arguments: +/// * `new_account_id`: the valid NEAR account which is being created and should +/// receive the linkdrop assets +/// * `new_public_key`: the valid public key that should be used for the access key added to the newly created account (serialized with borsh). +/// +/// Returns `true` if the claim was successful meaning the `new_account_id` was created and all assets were sent to it. +function create_account_and_claim(new_account_id: string, new_public_key: string) -> Promise; +``` + +## Reference Implementation + +Below are some references for linkdrop contracts. + +- [Link Drop Contract](https://github.com/near/near-linkdrop) + +- [Keypom Contract](https://github.com/keypom/keypom) + +## Security Implications + +1. Linkdrop Creation + Linkdrop creation involves creating keypairs that, when used, have access to assets such as $NEAR, FTs, NFTs, etc. These keys should be limited access and restricted to specific functionality. For example, they should only have permission to call `claim` and `create_account_and_claim`. Since the keys allow the holder to sign transactions on behalf of the linkdrop contract, without the proper security measures, they could be used in a malicious manner (for example executing private methods or owner-only functions). + + Another important security implication of linkdrop creation is to ensure that only one key is mapped to a set of assets at any given time. Externally, assets such as FTs, and NFTs belong to the overall linkdrop contract account rather than a specific access key. It is important to ensure that specific keys can only claim assets that they are mapped to. + +2. Linkdrop Key Management + Key management is a critical safety component of linkdrops. The linkdrop contract should implement a key management strategy for keys such that a reentrancy attack does not occur. For example, one strategy may be to "lock" or mark a key as "in transfer" such that it cannot be used again until the transfer is complete. + +3. Asset Refunds & Failed Claims + Given that linkdrops could contain multiple different assets such as NFTs, or fungible tokens, sending assets might happen across multiple blocks. If the claim was unsuccessful (such as passing in an invalid account ID), it is important to ensure that all state is properly managed and assets are optionally refunded depending on the linkdrop contract's implementation. + +4. Fungible Tokens & Future Data + Fungible token contracts require that anyone receiving tokens must be registered. For this reason, it is important to ensure that storage for accounts claiming linkdrops is paid for. This concept can be extended to any future data types that may be added. You must ensure that all the pre-requisite conditions have been met for the asset that is being transferred. + +5. Tokens Properly Sent to Linkdrop Contract + Since the linkdrop contract facilitates the transfer of assets including NFTs, and FTs, it is important to ensure that those tokens have been properly sent to the linkdrop contract prior to claiming. In addition, since all the tokens are in a shared pool, you must ensure that the linkdrop contract cannot claim assets that do not belong to the key that is being used to claim. + +It is also important to note that not every linkdrop is valid. Drops can expire, funds can be lazily sent to the contract (as seen in the case of fungible and non-fungible tokens) and the supply can be limited. + +## Alternatives + +#### Why is this design the best in the space of possible designs? + +This design allows for flexibility and extensibility of the standard while providing a set of criteria that cover the majority of current linkdrop use cases. The design was heavily inspired by current, functional NEPs such as the Fungible Token and Non-Fungible Token standards. + +#### What other designs have been considered and what is the rationale for not choosing them? + +A generic data struct that all drop types needed to inherit from. This struct contained a name and some metadata in the form of stringified JSON. This made it easily extensible for any new types down the road. The rationale for not choosing this design was both simplicity and flexibility. Having one data struct requires keys to be of one type only. In reality, there can be many at once. In addition, having a generic, open-ended metadata field could lead to many interpretations and different designs. We chose to use a KeyInfo struct that can be easily extensible and can cover all use-cases by having optional vectors of different data types. The proposed standard is simple, supports drops with multiple assets, and is backwards compatible with all previous linkdrops, and can be extended very easily. + +A standard linkdrop creation interface. A standardized linkdrop creation interface would provide data models and functions to ensure linkdrops were created and stored in a specific format. The rationale for not choosing this design was that is was too restrictive. Standardizing linkdrop creation adds complexity and reduces flexibility by restricting linkdrop creators in the process in which linkdrops are created, and potentially limiting linkdrop functionality. The functionality of the linkdrop creation, such as refunding of assets, access keys, and batch creation, should be chosen by the linkdrop creator and live within the linkdrop creator platform. Further, linkdrop creation is often not displayed to end users and there is not an inherent value proposition for a standardized linkdrop creation interface from a client perspective. + +#### What is the impact of not doing this? + +The impact of not doing this is creating a fragmented ecosystem of linkdrops, increasing the friction for user onboarding. Linkdrop claim pages (e.g. wallet providers) would have to implement custom integrations for every linkdrop provider platform. Inherently this would lead to a bad user experience when new users are onboarding and interacting with linkdrops in general. + +## Future possibilities + +- Linkdrop creation interface + +- Bulk linkdrop management (create, update, claim) + +- Function call data types (allowing for funder defined functions to be executed when a linkdrop is claimed) + +- Optional configurations added to KeyInfo which can include multi-usekeys, time-based claiming etc… + +- Standard process for how links connect to claim pages (i.e a standardized URL such as an app’s baseUrl/contractId= [LINKDROP_CONTRACT]&secretKey=[SECRET_KEY] + +- Standard for deleting keys and refunding assets. + +## Copyright + +Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/).