Skip to content
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

Asset bound nft ibex #3

Draft
wants to merge 25 commits into
base: assetBoundNFT
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
d509581
Initial draft
tbergmueller Apr 26, 2023
28d9f69
Finalize abstract
tbergmueller Apr 26, 2023
5549a41
Problem statement
tbergmueller Apr 26, 2023
905060e
Finalize motiviation
tbergmueller Apr 26, 2023
0d8753b
Terminology
tbergmueller Apr 26, 2023
81cb17c
Add figure1 and rework abstract
tbergmueller Apr 26, 2023
ab4540f
ERC-XXXX initial draft
tbergmueller Apr 26, 2023
54296a5
ERC specified and documented, extensions pending
tbergmueller Apr 26, 2023
fc7431e
Add interface definitions
tbergmueller Apr 26, 2023
8beb0ad
Add Reference implementation
tbergmueller Apr 27, 2023
f40a700
Replace wrapping anchor through mapping anchor
tbergmueller Apr 27, 2023
70a0783
License EIP under CC0 and RefImpl as MIT
tbergmueller Apr 27, 2023
583e883
Markdown Prettier formatting, specifying languages for code blocks.
ibex-technology Apr 28, 2023
c1e48ef
Rationale, use cases.
ibex-technology Apr 28, 2023
d6b5e2d
Finished Rationale except use cases.
ibex-technology Apr 28, 2023
bee7c45
Use case matrix
ibex-technology Apr 28, 2023
e3ef597
Markdown Prettier formatting, specifying languages for code blocks.
ibex-technology Apr 28, 2023
2fb5189
Rationale, use cases.
ibex-technology Apr 28, 2023
531c383
Finished Rationale except use cases.
ibex-technology Apr 28, 2023
8db2e82
Use case matrix
ibex-technology Apr 28, 2023
5c14983
Wording and structure revision
tbergmueller Apr 29, 2023
bf8271f
ERCXXXXLockable interface added.
ibex-technology Apr 29, 2023
1cb260c
Merge branch 'assetBoundNFT_Ibex' of https://github.com/authenticvisi…
ibex-technology Apr 29, 2023
5939e10
ERCXXXXLockable simplified with approval.
ibex-technology Apr 29, 2023
d58f489
Added IERCxxxxLockable and test cases.
ibex-technology Apr 29, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
564 changes: 564 additions & 0 deletions EIPS/eip_draft_asset_bound_non-fungible_tokens.md

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions assets/eip-draft_asset-bound_non-fungible_token/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
node_modules

#Hardhat files
cache
artifacts
typechain*

package-lock.json
21 changes: 21 additions & 0 deletions assets/eip-draft_asset-bound_non-fungible_token/LICENSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2023 Authentic Vision GmbH

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
12 changes: 12 additions & 0 deletions assets/eip-draft_asset-bound_non-fungible_token/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# ERCxxxx Reference implementation
This reference implementation is [MIT](LICENSE.md) licensed and can therefore be freely used in any project.

## Getting started
From this directory, run

```
npm install && npx hardhat test
```



444 changes: 444 additions & 0 deletions assets/eip-draft_asset-bound_non-fungible_token/contracts/ERCxxxx.sol

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.18;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";

import "./ERCxxxx.sol";
import "./IERCxxxxAttestationLimited.sol";
import "./IERCxxxxFloatable.sol";
import "./IERCxxxxLockable.sol";

contract ERCxxxxFull is ERCxxxx, IERCxxxxAttestationLimited, IERCxxxxFloatable, IERCxxxxLockable {

uint8 canStartFloatingMap;
uint8 canStopFloatingMap;

/// ###############################################################################################################################
/// ############################################################################################## IERCxxxxLockable
/// ###############################################################################################################################
mapping (uint256 => uint256) public lockCount;
mapping (uint256 => uint256) public lienCount;
mapping (uint256 => mapping (address => bool)) public tokenLocks;
mapping (uint256 => mapping (address => bool)) public tokenLiens;

function addLock(uint256 tokenId) public {
require(!tokenLocks[tokenId][msg.sender], "ERCxxxxLockable: caller has already added a lock for this token");
require(_isApprovedOrOwner(msg.sender, tokenId), "ERCxxxxLockable: Caller is not owner nor approved to operate the token");

tokenLocks[tokenId][msg.sender] = true;
lockCount[tokenId]++;

emit AnchorLockAdded(anchorByToken[tokenId], msg.sender, lockCount[tokenId]);
}

function removeLock(uint256 tokenId) public {
require(tokenLocks[tokenId][msg.sender], "ERCxxxxLockable: caller has not added a lock for this token");

tokenLocks[tokenId][msg.sender] = false;
lockCount[tokenId]--;

emit AnchorLockRemoved(anchorByToken[tokenId], msg.sender, lockCount[tokenId]);
}

function addLien(uint256 tokenId) public {
require(!tokenLiens[tokenId][msg.sender], "ERCxxxxLockable: caller has already added a lock for this token");
require(_isApprovedOrOwner(msg.sender, tokenId), "ERCxxxxLockable: Caller is not owner nor approved to operate the token");

tokenLiens[tokenId][msg.sender] = true;
lienCount[tokenId]++;

emit AnchorLienAdded(anchorByToken[tokenId], msg.sender, lienCount[tokenId]);
}

function removeLien(uint256 tokenId) public {
require(tokenLiens[tokenId][msg.sender], "ERCxxxxLockable: caller has not added a lien for this token");

tokenLiens[tokenId][msg.sender] = false;
lienCount[tokenId]--;

emit AnchorLienRemoved(anchorByToken[tokenId], msg.sender, lienCount[tokenId]);
}

function anchorIsLocked(bytes32 anchor) public view returns (bool) {
return lockCount[tokenByAnchor[anchor]] > 0;
}

function anchorHasLien(bytes32 anchor) public view returns (bool) {
return lienCount[tokenByAnchor[anchor]] > 0;
}

function tokenIsLocked(uint256 tokenId) public view returns (bool) {
return lockCount[tokenId] > 0;
}

function tokenHasLien(uint256 tokenId) public view returns (bool) {
return lienCount[tokenId] > 0;
}

/// ###############################################################################################################################
/// ############################################################################################## IERCxxxxAttestedTransferLimited
/// ###############################################################################################################################

mapping(bytes32 => uint256) attestedTransferLimitByAnchor;

uint256 public globalAttestedTransferLimitByAnchor;
AttestedTransferLimitUpdatePolicy public transferLimitPolicy;

/// @dev Counts the number of attested transfers by Anchor
bool public canFloat; // Indicates whether tokens can "float" in general, i.e. be transferred without attestation
bool public allFloating;

function requireValidLimitUpdate(uint256 oldValue, uint256 newValue) internal view {
if(newValue > oldValue) {
require(transferLimitPolicy == AttestedTransferLimitUpdatePolicy.FLEXIBLE || transferLimitPolicy == AttestedTransferLimitUpdatePolicy.INCREASE_ONLY, "EIP-xxxx: Updating attestedTransferLimit violates policy");
} else {
require(transferLimitPolicy == AttestedTransferLimitUpdatePolicy.FLEXIBLE || transferLimitPolicy == AttestedTransferLimitUpdatePolicy.DECREASE_ONLY, "EIP-xxxx: Updating attestedTransferLimit violates policy");
}
}

function updateGlobalAttestedTransferLimit(uint256 _nrTransfers)
public
onlyRole(MAINTAINER_ROLE)
{
requireValidLimitUpdate(globalAttestedTransferLimitByAnchor, _nrTransfers);
globalAttestedTransferLimitByAnchor = _nrTransfers;
emit GlobalAttestedTransferLimitUpdate(_nrTransfers, msg.sender);
}

function updateAttestedTransferLimit(bytes32 anchor, uint256 _nrTransfers)
public
onlyRole(MAINTAINER_ROLE)
{
uint256 currentLimit = attestedTransferLimit(anchor);
requireValidLimitUpdate(currentLimit, _nrTransfers);
attestedTransferLimitByAnchor[anchor] = _nrTransfers;
emit AttestedTransferLimitUpdate(_nrTransfers, anchor, msg.sender);
}

function attestedTransferLimit(bytes32 anchor) public view returns (uint256 limit) {
if(attestedTransferLimitByAnchor[anchor] > 0) { // Per anchor overwrites always, even if smaller than globalAttestedTransferLimit
return attestedTransferLimitByAnchor[anchor];
}
return globalAttestedTransferLimitByAnchor;
}

function attestatedTransfersLeft(bytes32 anchor) public view returns (uint256 nrTransfersLeft) {
// FIXME panics when attestationsUsedByAnchor > attestedTransferLimit
// since this should never happen, maybe ok?
return attestedTransferLimit(anchor) - attestationsUsedByAnchor[anchor];
}

/// ###############################################################################################################################
/// ############################################################################################## FLOATABILITY
/// ###############################################################################################################################
function canStartFloating(ERCxxxxAuthorization op) public
floatable()
onlyRole(MAINTAINER_ROLE)
{
canStartFloatingMap = createAuthorizationMap(op);
emit CanStartFloating(op, msg.sender);

}

function canStopFloating(ERCxxxxAuthorization op) public
floatable()
onlyRole(MAINTAINER_ROLE) {
canStopFloatingMap = createAuthorizationMap(op);
emit CanStopFloating(op, msg.sender);
}

modifier floatable() {
require(canFloat, "ERC-XXXX: Tokens not floatable");
_;
}

function allowFloating(bytes32 anchor, bool _doFloat)
public
{
if(_doFloat) {
require(roleBasedAuthorization(anchor, canStartFloatingMap), "ERC-XXXX: No permission to start floating");
} else {
require(roleBasedAuthorization(anchor, canStopFloatingMap), "ERC-XXXX: No permission to stop floating");
}

require(_doFloat != isFloating(anchor), "EIP-XXXX: allowFloating can only be called when changing floating state");
anchorIsReleased[anchor] = _doFloat;
emit AnchorFloatingState(anchor, tokenByAnchor[anchor], _doFloat);
}

function _beforeAttestationIsUsed(bytes32 anchor, address to) internal view virtual override(ERCxxxx) {
// empty, can be overwritten by derived conctracts.
require(attestatedTransfersLeft(anchor) > 0, "ERC-XXXX: No attested transfers left");

// Locks prevent use of attestation. Liens dont.
require(!anchorIsLocked(anchor), "ERC-XXXX: Anchor is locked, attestation can't transfer.");

super._beforeAttestationIsUsed(anchor, to);
}

function isFloating(bytes32 anchor) public view returns (bool){
return anchorIsReleased[anchor];
}

constructor(
string memory _name,
string memory _symbol,
ERCxxxxAuthorization _burnAuth,
ERCxxxxAuthorization _approveAuth,
bool _canFloat,
uint256 attestationLimitPerAnchor,
AttestedTransferLimitUpdatePolicy _limitUpdatePolicy)
ERCxxxx(_name, _symbol, _burnAuth, _approveAuth) {
canFloat = _canFloat;
transferLimitPolicy = _limitUpdatePolicy;
globalAttestedTransferLimitByAnchor = attestationLimitPerAnchor;
// TODO Parameter for "Float by default, i.e. each minted anchor is floating"
}
}
103 changes: 103 additions & 0 deletions assets/eip-draft_asset-bound_non-fungible_token/contracts/IERCxxxx.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.18;

/**
* @title IERCxxxx Asset-Bound Non-Fungible Tokens
* @author Thomas Bergmueller (@tbergmueller) <[email protected]>
* @notice Asset-bound Non-Fungible Tokens anchor a token 1:1 to a (physical or digital) asset and token transfers are authorized through attestation of control over the asset
* @dev See EIP-XXXX (todo link) for details
*/
interface IERCxxxx {
/// Used for several authorization mechansims, e.g. who can burn, who can set approval, ...
/// @dev Specifying the role in the ERC-XXX ecosystem. Used in conjunction with ERCxxxxAuthorization
enum ERCxxxxRole {
OWNER, // =0, The owner of the digital token
ISSUER, // =1, The issuer (ERC-XXXX contract) of the tokens, typically represented through a MAINTAINER_ROLE, the contract owner etc.
ASSET, // =2, The asset identified by the anchor
INVALID // =3, Reserved, do not use.
}

/// @dev Authorization, typically mapped to authorizationMaps, where each bit indicates whether a particular ERCxxxxRole is authorized
/// Typically used in constructor (hardcoded or params) to set burnAuthorization and approveAuthorization
/// Also used in optional ERC-XXXX updateBurnAuthorization, updateApproveAuthorization
enum ERCxxxxAuthorization {
NONE, // = 0, // None of the above
OWNER, // = (1<<OWNER), // The owner of the token, i.e. the digital representation
ISSUER, // = (1<<ISSUER), // The issuer of the tokens, i.e. this smart contract
ASSET, // = (1<<ASSET), // The asset, i.e. via attestation
OWNER_AND_ISSUER, // = (1<<OWNER) | (1<<ISSUER),
OWNER_AND_ASSET, // = (1<<OWNER) | (1<<ASSET),
ASSET_AND_ISSUER, // = (1<<ASSET) | (1<<ISSUER),
ALL // = (1<<OWNER) | (1<<ISSUER) | (1<<ASSET) // Owner + Issuer + Asset
}

event OracleUpdate(address indexed oracle, bool indexed trusted);
event AnchorTransfer(address indexed from, address indexed to, bytes32 indexed anchor, uint256 tokenId);
event AttestationUsed(address indexed to, bytes32 indexed anchor, bytes32 indexed attestationHash, uint256 totalUsedAttestationsForAnchor);
event ValidAnchorsUpdate(bytes32 indexed validAnchorHash, address indexed maintainer);

// state requesting methods
function anchorByToken(uint256 tokenId) external view returns (bytes32 anchor);
function tokenByAnchor(bytes32 anchor) external view returns (uint256 tokenId);
function attestationsUsedByAnchor(bytes32 anchor) external view returns (uint256 usageCount);
function decodeAttestationIfValid(bytes memory attestation) external view returns (address to, bytes32 anchor, bytes32 attestationHash);
function anchorIsReleased(bytes32 anchor) external view returns (bool isReleased);


/**
* @notice Adds or removes a trusted oracle, used when verifying signatures in `decodeAttestationIfValid()`
* @dev Emits OracleUpdate
* @param _oracle address of oracle
* @param _isTrusted true to add, false to remove
*/
function updateOracle(address _oracle, bool _isTrusted) external;

/**
* @notice Transfers the ownership of an NFT mapped to attestation.anchor to attestation.to address. Uses ERC721 safeTransferFrom and safeMint.
* @dev Permissionless, i.e. anybody invoke and sign a transaction. The transfer is authorized through the oracle-signed attestation.
* When using centralized "transaction-signers" (paying for gas), implement IERCxxxxAttestationLimited!
*
* Throws when attestation invalid or already used,
* Throws when attestation.to == ownerOf(tokenByAnchor(attestation.anchor)). See EIP-XXXX
* Emits AnchorTransfer and AttestationUsed
*
* @param attestation Attestation, refer EIP-XXXX for details
*
* @return anchor The anchor, which is mapped to `tokenId`
* @return to The `to` address, where the token with `tokenId` was transferd
* @return tokenId The tokenId, which is mapped to the `anchor` *
*/
function transferAnchor(bytes memory attestation) external returns (bytes32 anchor, address to, uint256 tokenId);

/**
* @notice Approves attestation.to the token mapped to attestation.anchor. Uses ERC721.approve(to, tokenId).
* @dev Permissionless, i.e. anybody invoke and sign a transaction. The transfer is authorized through the oracle-signed attestation.
* When using centralized "transaction-signers" (paying for gas), implement IERCxxxxAttestationLimited!
*
* Throws when attestation invalid or already used
* Throws when ERCxxxxRole.ASSET is not authorized to approve
*
* @param attestation Attestation, refer EIP-XXXX for details
*/
function approveAnchor(bytes memory attestation) external;

/**
* @notice Burns the token mapped to attestation.anchor. Uses ERC721._burn.
* @dev Permissionless, i.e. anybody invoke and sign a transaction. The transfer is authorized through the oracle-signed attestation.
* When using centralized "transaction-signers" (paying for gas), implement IERCxxxxAttestationLimited!
*
* Throws when attestation invalid or already used
* Throws when ERCxxxxRole.ASSET is not authorized to burn
*
* @param attestation Attestation, refer EIP-XXXX for details
*/
function burnAnchor(bytes memory attestation) external;


/// @notice Update the Merkle root containing the valid anchors. Consider salt-leaves!
/// @dev Proof (transferAnchor) needs to provided from this tree.
/// @dev The merkle-tree needs to contain at least one "salt leaf" in order to not publish the complete merkle-tree when all anchors should have been dropped at least once.
/// @param merkleRootNode The root, containing all anchors we want validated.
function updateValidAnchors(bytes32 merkleRootNode) external;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.18;

import "./IERCxxxx.sol";

interface IERCxxxxAttestationLimited is IERCxxxx {
enum AttestedTransferLimitUpdatePolicy {
IMMUTABLE,
INCREASE_ONLY,
DECREASE_ONLY,
FLEXIBLE
}
function updateGlobalAttestedTransferLimit(uint256 _nrTransfers) external;
function attestatedTransfersLeft(bytes32 _anchor) external view returns (uint256 nrTransfersLeft);

event GlobalAttestedTransferLimitUpdate(
uint256 indexed transferLimit,
address updatedBy
);

event AttestedTransferLimitUpdate(
uint256 indexed transferLimit,
bytes32 indexed anchor,
address updatedBy
);
}
Loading