--- eip: 5169 title: Client Script URI for Token Contracts description: Add a scriptURI to point to an executable script associated with the functionality of the token. author: James (@JamesSmartCell), Weiwu (@weiwu-zhang) discussions-to: https://ethereum-magicians.org/t/eip-5169-client-script-uri-for-token-contracts/9674 status: Review type: Standards Track category: ERC created: 2022-05-03 requires: 20, 165, 721, 777, 1155 --- ## Abstract This EIP provides a contract interface adding a `scriptURI()` function for locating executable scripts associated with the token. ## Motivation Often, smart contract authors want to provide some user functionality to their tokens through client scripts. The idea is made popular with function-rich NFTs. It's important that a token's contract is linked to its client script, since the client script may carry out trusted tasks such as creating transactions for the user. This EIP allows users to be sure they are using the correct script through the contract by providing a URI to an official script, made available with a call to the token contract itself (`scriptURI`). This URI can be any RFC 3986-compliant URI, such as a link to an IPFS multihash, GitHub gist, or a cloud storage provider. Each contract implementing this EIP implements a `scriptURI` function which returns the download URI to a client script. The script provides a client-side executable to the hosting token. Examples of such a script could be: - A 'miniDapp', which is a cut-down DApp tailored for a single token. - A 'TokenScript' which provides TIPS from a browser wallet. - A 'TokenScript' that allows users to interact with contract functions not normally provided by a wallet, eg 'mint' function. - An extension that is downloadable to the hardware wallet with an extension framework, such as Ledger. - JavaScript instructions to operate a smartlock, after owner receives authorization token in their wallet. ### Overview With the discussion above in mind, we outline the solution proposed by this EIP. For this purpose, we consider the following variables: - `SCPrivKey`: The private signing key to administrate a smart contract implementing this EIP. Note that this doesn't have to be a new key especially added for this EIP. Most smart contracts made today already have an administration key to manage the tokens issued. It can be used to update the `scriptURI`. - `newScriptURI`: an array of URIs for different ways to find the client script. We can describe the life cycle of the `scriptURI` functionality: - Issuance 1. The token issuer issues the tokens and a smart contract implementing this EIP, with the admin key for the smart contract being `SCPrivKey`. 2. The token issuer calls `setScriptURI` with the `scriptURI`. - Update `scriptURI` 1. The token issuer stores the desired `script` at all the new URI locations and constructs a new `scriptURI` structure based on this. 2. The token issuer calls `setScriptURI` with the new `scriptURI` structure. ## Specification The keywords “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY” and “OPTIONAL” in this document are to be interpreted as described in RFC 2119. We define a scriptURI element using the `string[]`. Based on this, we define the smart contract interface below: ```solidity interface IERC5169 { /// @dev This event emits when the scriptURI is updated, /// so wallets implementing this interface can update a cached script event ScriptUpdate(string[] memory newScriptURI); /// @notice Get the scriptURI for the contract /// @return The scriptURI function scriptURI() external view returns(string[] memory); /// @notice Update the scriptURI /// emits event ScriptUpdate(scriptURI memory newScriptURI); function setScriptURI(string[] memory newScriptURI) external; } ``` The interface MUST be implemented under the following constraints: - The smart contract implementing `IERC5169` MUST store variables `address owner` in its state. - The smart contract implementing `IERC5169` MUST set `owner=msg.sender` in its constructor. - The `ScriptUpdate(...)` event MUST be emitted when the ```setScriptURI``` function updates the `scriptURI`. - The `setScriptURI(...)` function MUST validate that `owner == msg.sender` *before* executing its logic and updating any state. - The `setScriptURI(...)` function MUST update its internal state such that `currentScriptURI = newScriptURI`. - The `scriptURI()` function MUST return the `currentScriptURI` state. - The `scriptURI()` function MAY be implemented as pure or view. - Any user of the script learned from `scriptURI` MUST validate the script is either at an immutable location, its URI contains its hash digest, or it implements the separate `Authenticity for Client Script` EIP, which asserts authenticity using signatures instead of a digest. ## Rationale This method avoids the need for building secure and certified centralized hosting and allows scripts to be hosted anywhere: IPFS, GitHub or cloud storage. ## Backwards Compatibility This standard is backwards-compatible with most existing token standards, including the following commonly-used ones: - [ERC-20](./eip-20.md) - [ERC-721](./eip-721.md) - [ERC-777](./eip-777.md) - [ERC-1155](./eip-1155.md) ## Test Cases ### Test Contract ```solidity import "@openzeppelin/contracts/access/Ownable.sol"; import "./IERC5169.sol"; contract ERC5169 is IERC5169, Ownable { string[] private _scriptURI; function scriptURI() external view override returns(string[] memory) { return _scriptURI; } function setScriptURI(string[] memory newScriptURI) external onlyOwner override { _scriptURI = newScriptURI; emit ScriptUpdate(newScriptURI); } } ``` ### Test Cases ```ts const { expect } = require('chai'); const { BigNumber, Wallet } = require('ethers'); const { ethers, network, getChainId } = require('hardhat'); describe('ERC5169', function () { before(async function () { this.ERC5169 = await ethers.getContractFactory('ERC5169'); }); beforeEach(async function () { // targetNFT this.erc5169 = await this.ERC5169.deploy(); }); it('Should set script URI', async function () { const scriptURI = [ 'uri1', 'uri2', 'uri3' ]; await expect(this.erc5169.setScriptURI(scriptURI)) .emit(this.erc5169, 'ScriptUpdate') .withArgs(scriptURI); const currentScriptURI = await this.erc5169.scriptURI(); expect(currentScriptURI.toString()).to.be.equal(scriptURI.toString()); }); ``` ## Reference Implementation An intuitive implementation is the STL office door token. This NFT is minted and transferred to STL employees. The TokenScript attached to the token contract via the `scriptURI()` function contains instructions on how to operate the door interface. This takes the form of: 1. Query for challenge string (random message from IoT interface eg 'Apples-5E3FA1'). 2. Receive and display challenge string on Token View, and request 'Sign Personal'. 3. On obtaining the signature of the challenge string, send back to IoT device. 4. IoT device will unlock door if ec-recovered address holds the NFT. With `scriptURI()` the experience is greatly enhanced as the flow for the user is: 1. Receive NFT. 2. Use authenticated NFT functionality in the wallet immediately. The project with contract, TokenScript and IoT firmware is in use by Smart Token Labs office door and numerous other installations. An example implementation contract: [ERC-5169 Contract Example](../assets/eip-5169/contract/ExampleContract.sol) and TokenScript: [ERC-5169 TokenScript Example](../assets/eip-5169/tokenscript/ExampleScript.xml). Links to the firmware and full sample can be found in the associated discussion linked in the header. The associated TokenScript can be read from the contract using `scriptURI()`. ### Script location While the most straightforward solution to facilitate specific script usage associated with NFTs, is clearly to store such a script on the smart contract. However, this has several disadvantages: 1. The smart contract signing key is needed to make updates, causing the key to become more exposed, as it is used more often. 2. Updates require smart contract interaction. If frequent updates are needed, smart contract calls can become an expensive hurdle. 3. Storage fee. If the script is large, updates to the script will be costly. A client script is typically much larger than a smart contract. For these reasons, storing volatile data, such as token enhancing functionality, on an external resource makes sense. Such an external resource can be either be hosted centrally, such as through a cloud provider, or privately hosted through a private server, or decentralized hosted, such as the interplanetary filesystem. While centralized storage for a decentralized functionality goes against the ethos of web3, fully decentralized solutions may come with speed, price or space penalties. This EIP handles this by allowing the function `ScriptURI` to return multiple URIs, which could be a mix of centralized, individually hosted and decentralized locations. While this EIP does not dictate the format of the stored script, the script itself could contain pointers to multiple other scripts and data sources, allowing for advanced ways to expand token scripts, such as lazy loading. The handling of integrity of such secondary data sources is left dependent on the format of the script. ## Security Considerations **When a server is involved** When the client script does not purely rely on connection to a blockchain node, but also calls server APIs, the trustworthiness of the server API is called into question. This EIP does not provide any mechanism to assert the authenticity of the API access point. Instead, as long as the client script is trusted, it's assumed that it can call any server API in order to carry out token functions. This means the client script can mistrust a server API access point. **When the scriptURI doesn't contain integrity (hash) information** We separately authored `Authenticity for Client Script` EIP to guide on how to use digital signatures efficiently and concisely to ensure authenticity and integrity of scripts not stored at a URI which is a digest of the script itself. ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md).