-
Notifications
You must be signed in to change notification settings - Fork 445
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add ERC: Permissionless Script Registry #530
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
c653850
submit script registry EIP
JamesSmartCell 383bca4
Change filename
JamesSmartCell 5fa5938
Update name
JamesSmartCell bdc981d
Update with new ERC number
JamesSmartCell 3034aac
Update submission and add test cases
JamesSmartCell 6ab72ef
Update links
JamesSmartCell e51f584
Update ERC-7738 with recommendations and implement paging version of …
JamesSmartCell 6b708f7
Update erc-7738.md
JamesSmartCell dfe46d1
Update erc-7738.md
JamesSmartCell 1734c4a
Update erc-7738.md
JamesSmartCell a40e0aa
Update erc-7738.md
JamesSmartCell fc2aee2
Merge branch 'master' into master
JamesSmartCell File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
--- | ||
eip: 7738 | ||
title: Permissionless Script Registry | ||
description: Permissionless registry to fetch executable scripts for contracts | ||
author: Victor Zhang (@zhangzhongnan928), James Brown (@JamesSmartCell) | ||
discussions-to: https://ethereum-magicians.org/t/erc-7738-permissionless-script-registry/20503 | ||
status: Draft | ||
type: Standards Track | ||
category: ERC | ||
created: 2024-07-01 | ||
requires: 173 | ||
--- | ||
## Abstract | ||
|
||
This EIP provides a means to create a standard registry for locating executable scripts associated with the token. | ||
|
||
## Motivation | ||
|
||
[ERC-5169](./eip-5169.md) provides a client script lookup method for contracts. This requires the contract to have implemented the `ERC-5169` interface at the time of construction (or allow an upgrade path). | ||
|
||
This proposal outlines a contract that can supply prototype and certified scripts. The contract would be a multichain singleton instance that would be deployed at identical addresses on supported chains. | ||
|
||
### Overview | ||
|
||
The registry contract will supply a set of URI links for a given contract address. These URI links point to script programs that can be fetched by a wallet, viewer or mini-dapp. | ||
|
||
The pointers can be set permissionlessly using a setter in the registry contract. | ||
|
||
## 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. | ||
|
||
The contract MUST implement the `IERC7738` interface. | ||
The contract MUST emit the `ScriptUpdate` event when the script is updated. | ||
The contract SHOULD order the `scriptURI` returned so that the `ERC-173` `owner()` of the contract's script entries are returned first (in the case of simple implementations the wallet will pick the first `scriptURI` returned). | ||
The contract SHOULD provide a means to page through entries if there are a large number of scriptURI entries. | ||
|
||
```solidity | ||
interface IERC7738 { | ||
/// @dev This event emits when the scriptURI is updated, | ||
/// so wallets implementing this interface can update a cached script | ||
event ScriptUpdate(address indexed contractAddress, string[] newScriptURI); | ||
|
||
/// @notice Get the scriptURI for the contract | ||
/// @return The scriptURI | ||
function scriptURI(address contractAddress) external view returns (string[] memory); | ||
|
||
/// @notice Update the scriptURI | ||
/// emits event ScriptUpdate(address indexed contractAddress, scriptURI memory newScriptURI); | ||
function setScriptURI(address contractAddress, string[] memory scriptURIList) external; | ||
} | ||
``` | ||
|
||
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174. | ||
|
||
## Rationale | ||
|
||
This method allows contracts written without the [ERC-5169](./eip-5169.md) interface to associate scripts with themselves, and avoids the need for a centralised online server, with subsequent need for security and the requires an organisation to become a gatekeeper for the database. | ||
JamesSmartCell marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
## Test Cases | ||
|
||
Hardhat contract and test scripts for two implementations can be found in the asset folder. | ||
|
||
## Reference Implementation | ||
|
||
```solidity | ||
import "@openzeppelin/contracts/access/Ownable.sol"; | ||
|
||
contract DecentralisedRegistry is IERC7738 { | ||
struct ScriptEntry { | ||
mapping(address => string[]) scriptURIs; | ||
address[] addrList; | ||
} | ||
|
||
mapping(address => ScriptEntry) private _scriptURIs; | ||
|
||
function setScriptURI( | ||
address contractAddress, | ||
string[] memory scriptURIList | ||
) public { | ||
require (scriptURIList.length > 0, "> 0 entries required in scriptURIList"); | ||
bool isOwnerOrExistingEntry = Ownable(contractAddress).owner() == msg.sender | ||
|| _scriptURIs[contractAddress].scriptURIs[msg.sender].length > 0; | ||
_scriptURIs[contractAddress].scriptURIs[msg.sender] = scriptURIList; | ||
if (!isOwnerOrExistingEntry) { | ||
_scriptURIs[contractAddress].addrList.push(msg.sender); | ||
} | ||
|
||
emit ScriptUpdate(contractAddress, msg.sender, scriptURIList); | ||
} | ||
|
||
// Return the list of scriptURI for this contract. | ||
// Order the return list so `Owner()` assigned scripts are first in the list | ||
function scriptURI( | ||
address contractAddress | ||
) public view returns (string[] memory) { | ||
//build scriptURI return list, owner first | ||
address contractOwner = Ownable(contractAddress).owner(); | ||
address[] memory addrList = _scriptURIs[contractAddress].addrList; | ||
uint256 i; | ||
|
||
//now calculate list length | ||
uint256 listLen = _scriptURIs[contractAddress].scriptURIs[contractOwner].length; | ||
for (i = 0; i < addrList.length; i++) { | ||
listLen += _scriptURIs[contractAddress].scriptURIs[addrList[i]].length; | ||
} | ||
|
||
string[] memory ownerScripts = new string[](listLen); | ||
|
||
// Add owner scripts | ||
uint256 scriptIndex = _addScriptURIs(contractOwner, contractAddress, ownerScripts, 0); | ||
|
||
// Add remainder scripts | ||
for (uint256 i = 0; i < addrList.length; i++) { | ||
scriptIndex = _addScriptURIs(addrList[i], contractAddress, ownerScripts, scriptIndex); | ||
} | ||
|
||
return ownerScripts; | ||
} | ||
|
||
function _addScriptURIs( | ||
address user, | ||
address contractAddress, | ||
string[] memory ownerScripts, | ||
uint256 scriptIndex | ||
) internal view returns (uint256) { | ||
for (uint256 j = 0; j < _scriptURIs[contractAddress].scriptURIs[user].length; j++) { | ||
string memory thisScriptURI = _scriptURIs[contractAddress].scriptURIs[user][j]; | ||
if (bytes(thisScriptURI).length > 0) { | ||
ownerScripts[scriptIndex++] = thisScriptURI; | ||
} | ||
} | ||
return scriptIndex; | ||
} | ||
} | ||
``` | ||
|
||
## Security Considerations | ||
|
||
The scripts provided could be authenticated in various ways: | ||
|
||
1. The target contract which the setter specifies implements the [ERC-173](./eip-173.md) `Ownable` interface. Once the script is fetched, the signature can be verified to match the Owner(). In the case of TokenScript this can be checked by a dapp or wallet using the TokenScript SDK, the TokenScript online verification service, or by extracting the signature from the XML, taking a keccak256 of the script and ecrecover the signing key address. | ||
2. If the contract does not implement Ownable, further steps can be taken: | ||
a. The hosting app/wallet can acertain the deployment key using 3rd party API or block explorer. The implementing wallet, dapp or viewer would then check the signature matches this deployment key. | ||
b. Signing keys could be pre-authenticated by a hosting app, using an embedded keychain. | ||
c. A governance token could allow a script council to authenticate requests to set and validate keys. | ||
|
||
If these criteria are not met: | ||
- For mainnet implementations the implementing wallet should be cautious about using the script - it would be at the app and/or user's discretion. | ||
- For testnets, it is acceptable to allow the script to function, at the discretion of the wallet provider. | ||
|
||
## Copyright | ||
|
||
Copyright and related rights waived via [CC0](../LICENSE.md). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
node_modules | ||
.env | ||
|
||
# Hardhat files | ||
/cache | ||
/artifacts | ||
|
||
# TypeChain files | ||
/typechain | ||
/typechain-types | ||
|
||
# solidity-coverage files | ||
/coverage | ||
/coverage.json | ||
|
||
# Hardhat Ignition default folder for deployments against a local node | ||
ignition/deployments/chain-31337 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
// SPDX-License-Identifier: MIT | ||
|
||
pragma solidity ^0.8.24; | ||
import "@openzeppelin/contracts/access/Ownable.sol"; | ||
import "./IERC7738.sol"; | ||
|
||
contract DecentralisedRegistry is IERC7738 { | ||
struct ScriptEntry { | ||
mapping(address => string[]) scriptURIs; | ||
address[] addrList; | ||
} | ||
|
||
uint256 public constant MAX_PAGE_SIZE = 500; | ||
mapping(address => ScriptEntry) private _scriptURIs; | ||
|
||
function setScriptURI( | ||
address contractAddress, | ||
string[] memory scriptURIList | ||
) public { | ||
require (scriptURIList.length > 0, "> 0 entries required in scriptURIList"); | ||
bool isOwnerOrExistingEntry = Ownable(contractAddress).owner() == msg.sender | ||
|| _scriptURIs[contractAddress].scriptURIs[msg.sender].length > 0; | ||
_scriptURIs[contractAddress].scriptURIs[msg.sender] = scriptURIList; | ||
if (!isOwnerOrExistingEntry) { | ||
_scriptURIs[contractAddress].addrList.push(msg.sender); | ||
} | ||
|
||
emit ScriptUpdate(contractAddress, msg.sender, scriptURIList); | ||
} | ||
|
||
function scriptURI( | ||
address contractAddress | ||
) public view returns (string[] memory) { | ||
return scriptURI(contractAddress, 1, MAX_PAGE_SIZE); | ||
} | ||
|
||
function scriptURI(address contractAddress, uint256 page, uint256 pageSize) public view returns (string[] memory ownerScripts) { | ||
require(page > 0 && pageSize > 0 && pageSize <= MAX_PAGE_SIZE, "Page >= 1 and pageSize <= MAX_PAGE_SIZE"); | ||
|
||
address contractOwner = Ownable(contractAddress).owner(); | ||
address[] memory addrList = _scriptURIs[contractAddress].addrList; | ||
uint256 startPoint = pageSize * (page - 1); | ||
|
||
uint256 listLen = _scriptURIs[contractAddress].scriptURIs[contractOwner].length; | ||
for (uint256 i = 0; i < addrList.length; i++) { | ||
listLen += _scriptURIs[contractAddress].scriptURIs[addrList[i]].length; | ||
} | ||
|
||
uint256 arrayLen = listLen < pageSize ? listLen : pageSize; | ||
ownerScripts = new string[](arrayLen); | ||
uint256 scriptIndex = 0; | ||
uint256 currentIndex = 0; | ||
|
||
if (startPoint >= listLen) { | ||
return new string[](0) ; | ||
} | ||
|
||
// Add owner scriptURIs | ||
(scriptIndex, currentIndex) = _addScriptURIs(contractOwner, contractAddress, startPoint, scriptIndex, pageSize, ownerScripts, currentIndex); | ||
|
||
// Add remainder of scriptURIs | ||
for (uint256 i = 0; i < addrList.length && scriptIndex < pageSize; i++) { | ||
(scriptIndex, currentIndex) = _addScriptURIs(addrList[i], contractAddress, startPoint, scriptIndex, pageSize, ownerScripts, currentIndex); | ||
} | ||
} | ||
|
||
function _addScriptURIs( | ||
address user, | ||
address contractAddress, | ||
uint256 startPoint, | ||
uint256 scriptIndex, | ||
uint256 pageSize, | ||
string[] memory ownerScripts, | ||
uint256 currentIndex | ||
) internal view returns (uint256, uint256) { | ||
for (uint256 j = 0; j < _scriptURIs[contractAddress].scriptURIs[user].length; j++) { | ||
string memory thisScriptURI = _scriptURIs[contractAddress].scriptURIs[user][j]; | ||
if (bytes(thisScriptURI).length > 0) { | ||
if (currentIndex >= startPoint) { | ||
ownerScripts[scriptIndex++] = thisScriptURI; | ||
} | ||
if (scriptIndex >= pageSize) { | ||
break; | ||
} | ||
} | ||
currentIndex++; | ||
} | ||
return (scriptIndex, currentIndex); | ||
} | ||
} |
130 changes: 130 additions & 0 deletions
130
assets/erc-7738/contracts/DecentralisedRegistryPermissioned.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
// SPDX-License-Identifier: MIT | ||
|
||
pragma solidity ^0.8.24; | ||
import "@openzeppelin/contracts/access/Ownable.sol"; | ||
import "./IERC7738.sol"; | ||
|
||
contract DecentralisedRegistryPermissioned is IERC7738 { | ||
struct ScriptEntry { | ||
string[] scriptURIs; | ||
address[] delegateSigners; // list of authenticated addresses approved by owner | ||
address owner; // provides a latch so that 3rd parties can create TokenScript entries | ||
} | ||
|
||
mapping(address => ScriptEntry) private _scriptURIs; | ||
|
||
event RegisterOwner( | ||
address indexed contractAddress, | ||
address indexed newOwner | ||
); | ||
event AddDelegateSigner( | ||
address indexed contractAddress, | ||
address indexed newDelegate | ||
); | ||
event RevokeDelegateSigner( | ||
address indexed contractAddress, | ||
address indexed revokedDelegate | ||
); | ||
|
||
function scriptURI( | ||
address contractAddress | ||
) public view returns (string[] memory) { | ||
return _scriptURIs[contractAddress].scriptURIs; | ||
} | ||
|
||
function setScriptURI( | ||
address contractAddress, | ||
string[] memory scriptURIList | ||
) public { | ||
// in order to set scriptURI array, the sender must adhere to the following rules: | ||
require( | ||
isDelegateOrOwner(contractAddress, msg.sender), | ||
"Not authorized" | ||
); | ||
|
||
emit ScriptUpdate(contractAddress, msg.sender, scriptURIList); | ||
_scriptURIs[contractAddress].scriptURIs = scriptURIList; | ||
} | ||
|
||
function registerOwner(address contractAddress) public { | ||
ScriptEntry storage existingEntry = _scriptURIs[contractAddress]; | ||
address contractOwner = Ownable(contractAddress).owner(); | ||
address sender = msg.sender; | ||
require(existingEntry.owner != sender, "Already set to this owner"); | ||
require( | ||
existingEntry.owner == address(0) || sender == contractOwner, | ||
"Not authorized" | ||
); | ||
emit RegisterOwner(contractAddress, sender); | ||
existingEntry.owner = sender; | ||
} | ||
|
||
function isDelegateOrOwner( | ||
address contractAddress, | ||
address check | ||
) public view returns (bool) { | ||
ScriptEntry memory existingEntry = _scriptURIs[contractAddress]; | ||
if (check == Ownable(contractAddress).owner()) { | ||
return true; | ||
} | ||
uint256 length = existingEntry.delegateSigners.length; | ||
for (uint256 i = 0; i < length; ) { | ||
if (existingEntry.delegateSigners[i] == check) { | ||
return true; | ||
} | ||
unchecked { | ||
i++; | ||
} | ||
} | ||
return false; | ||
} | ||
|
||
function getDelegateIndex( | ||
address contractAddress, | ||
address check | ||
) public view returns (int256) { | ||
ScriptEntry memory existingEntry = _scriptURIs[contractAddress]; | ||
uint256 length = existingEntry.delegateSigners.length; | ||
for (uint256 i = 0; i < length; ) { | ||
if (existingEntry.delegateSigners[i] == check) { | ||
return int256(i); | ||
} | ||
unchecked { | ||
i++; | ||
} | ||
} | ||
return -1; | ||
} | ||
|
||
function addDelegateSigner( | ||
address contractAddress, | ||
address newSigner | ||
) public { | ||
require( | ||
msg.sender == Ownable(contractAddress).owner(), | ||
"Contract Owner only" | ||
); | ||
require( | ||
getDelegateIndex(contractAddress, newSigner) < 0, | ||
"Already a delegate signer" | ||
); | ||
emit AddDelegateSigner(contractAddress, newSigner); | ||
_scriptURIs[contractAddress].delegateSigners.push(newSigner); | ||
} | ||
|
||
function revokeDelegateSigner( | ||
address contractAddress, | ||
address signer | ||
) public { | ||
int256 delegateIndex = getDelegateIndex(contractAddress, signer); | ||
require( | ||
msg.sender == Ownable(contractAddress).owner(), | ||
"Contract Owner only" | ||
); | ||
require(delegateIndex > -1, "Unable to revoke unknown signer"); | ||
emit RevokeDelegateSigner(contractAddress, signer); | ||
delete _scriptURIs[contractAddress].delegateSigners[ | ||
uint256(delegateIndex) | ||
]; | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are from ERC-5169 right? if so it should inherit the ERC-5169 interface instead
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ERC-5169 in entirety can't be applied here because although this interface follows the intent of ERC-5169, the functions in this interface have a different form; they require a contract address in addition to each method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it