diff --git a/README.md b/README.md index cfbf9cb..583b57d 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,21 @@ ## PBT (Physical Backed Token) NFT collectors enjoy collecting digital assets and sharing them with others online. However, there is currently no such standard for showcasing physical assets as NFTs with verified authenticity and ownership. Existing solutions are fragmented and tend to be susceptible to at least one of the following: -- The NFT cannot proxy as ownership of the physical item. In most current implementations, the NFT and physical item are functionally two decoupled distinct assets after the NFT mint, in which the NFT can be freely traded independently from the physical item. -- Verification of authenticity of the physical item requires action from a trusted entity (e.g. StockX). +- The NFT cannot proxy as ownership of the physical item. In most current implementations, the NFT and physical item are functionally two decoupled distinct assets after the NFT mint, in which the NFT can be freely traded independently from the physical item. + +- Verification of authenticity of the physical item requires action from a trusted entity (e.g. StockX). PBT aims to mitigate these issues in a decentralized way through a new token standard (an extension of EIP-721). From the [Azuki](https://twitter.com/AzukiOfficial) team. **Chiru Labs is not liable for any outcomes as a result of using PBT**, DYOR. Repo still in beta. - ## Resources -- [pbt.io](https://www.pbt.io/) -- [EIP (Draft)](https://github.com/ethereum/EIPs/pull/5791) -- [Blog](https://www.azuki.com/updates/pbt) - +- [pbt.io](https://www.pbt.io/) +- [EIP (Draft)](https://github.com/ethereum/EIPs/pull/5791) +- [Blog](https://www.azuki.com/updates/pbt) ## How does it work? @@ -24,25 +23,27 @@ From the [Azuki](https://twitter.com/AzukiOfficial) team. This approach assumes that the physical item must have a chip attached to it that fulfills the following requirements ([Kong](https://arx.org/) is one such vendor for these chips): -- the ability to securely generate and store an ECDSA secp256k1 asymmetric keypair -- the ability to sign messages from the private key of the asymmetric keypair -- the ability for one to retrieve only the public key of the asymmetric keypair (private key non-extractable) +- The ability to securely generate and store an ECDSA secp256k1 asymmetric keypair +- The ability to sign messages from the private key of the asymmetric keypair +- The ability for one to retrieve only the public key of the asymmetric keypair (private key non-extractable) The approach also requires that the contract uses an account-bound implementation of EIP-721 (where all EIP-721 functions that transfer must throw, e.g. the "read only NFT registry" implementation referenced in EIP-721). This ensures that ownership of the physical item is required to initiate transfers and manage ownership of the NFT, through a new function introduced in `IPBT.sol` (`transferTokenWithChip`). #### Approach On a high level: -- Each NFT is conceptually linked to a physical chip. -- The NFT can only be transferred to a different owner if a signature from the chip is supplied to the contract. -- This guarantees that a token cannot be transferred without consent from the owner of the physical item. + +- Each NFT is conceptually linked to a physical chip. +- The NFT can only be transferred to a different owner if a signature from the chip is supplied to the contract. +- This guarantees that a token cannot be transferred without consent from the owner of the physical item. More details available in the [EIP](https://github.com/ethereum/EIPs/pull/5791) and inlined into `IPBT.sol`. #### Reference Implementation A simple mint for a physical drop could look something like this: -``` + +```solidity import "@openzeppelin/contracts/access/Ownable.sol"; import "@chiru-labs/pbt/src/PBTSimple.sol"; @@ -50,7 +51,7 @@ contract Example is PBTSimple, Ownable { /// @notice Initialize a mapping from chipAddress to tokenId. /// @param chipAddresses The addresses derived from the public keys of the chips - constructor(address[] calldata chipAddresses, uint256[] calldata tokenIds) + constructor(address[] memory chipAddresses, uint256[] memory tokenIds) PBTSimple("Example", "EXAMPLE") { _seedChipToTokenMapping(chipAddresses, tokenIds); @@ -67,8 +68,8 @@ contract Example is PBTSimple, Ownable { } } ``` -As mentioned above, this repo is still in beta and more documentation is on its way. Feel free to contact [@2pmflow](https://twitter.com/2pmflow) if you have any questions. +As mentioned above, this repo is still in beta and more documentation is on its way. Feel free to contact [@2pmflow](https://twitter.com/2pmflow) if you have any questions. ## Contributing diff --git a/gas-snapshot b/gas-snapshot index 03365c5..443a39a 100644 --- a/gas-snapshot +++ b/gas-snapshot @@ -1,21 +1,16 @@ -ERC721ReadOnlyTest:testApprove() (gas: 10677) -ERC721ReadOnlyTest:testGetApproved() (gas: 16236) -ERC721ReadOnlyTest:testIsApprovedForAll() (gas: 10051) -ERC721ReadOnlyTest:testSetApprovalForAll() (gas: 10714) -ERC721ReadOnlyTest:testTransferFunctions() (gas: 19077) -PBTSimpleTest:testGetTokenDataForChipSignature() (gas: 127965) -PBTSimpleTest:testGetTokenDataForChipSignatureBlockNumTooOld() (gas: 122516) -PBTSimpleTest:testGetTokenDataForChipSignatureInvalid() (gas: 131807) -PBTSimpleTest:testGetTokenDataForChipSignatureInvalidBlockNumber() (gas: 122355) -PBTSimpleTest:testIsChipSignatureForToken() (gas: 216733) -PBTSimpleTest:testMintTokenWithChip() (gas: 176613) -PBTSimpleTest:testSeedChipToTokenMapping() (gas: 115251) -PBTSimpleTest:testSeedChipToTokenMappingExistingToken() (gas: 212298) -PBTSimpleTest:testSeedChipToTokenMappingInvalidInput() (gas: 17175) -PBTSimpleTest:testSupportsInterface() (gas: 7059) -PBTSimpleTest:testTokenIdFor() (gas: 117085) -PBTSimpleTest:testTokenIdMappedFor() (gas: 66779) -PBTSimpleTest:testTransferTokenWithChip(bool) (runs: 256, μ: 211915, ~: 211834) -PBTSimpleTest:testUpdateChips() (gas: 171055) -PBTSimpleTest:testUpdateChipsInvalidInput() (gas: 16433) -PBTSimpleTest:testUpdateChipsUnsetChip() (gas: 23430) +testGetTokenDataForChipSignature() (gas: 127752) +testGetTokenDataForChipSignatureBlockNumTooOld() (gas: 122298) +testGetTokenDataForChipSignatureInvalid() (gas: 131594) +testGetTokenDataForChipSignatureInvalidBlockNumber() (gas: 122132) +testIsChipSignatureForToken() (gas: 216535) +testMintTokenWithChip() (gas: 176395) +testSeedChipToTokenMapping() (gas: 115078) +testSeedChipToTokenMappingExistingToken() (gas: 212100) +testSeedChipToTokenMappingInvalidInput() (gas: 17178) +testSupportsInterface() (gas: 7059) +testTokenIdFor() (gas: 116986) +testTokenIdMappedFor() (gas: 66680) +testTransferTokenWithChip(bool) (runs: 256, μ: 211706, ~: 211616) +testUpdateChips() (gas: 170776) +testUpdateChipsInvalidInput() (gas: 16433) +testUpdateChipsUnsetChip() (gas: 23405) \ No newline at end of file diff --git a/src/IPBT.sol b/src/IPBT.sol index c1b4ed7..3f32d45 100644 --- a/src/IPBT.sol +++ b/src/IPBT.sol @@ -49,7 +49,7 @@ interface IPBT { /// @notice Calls transferTokenWithChip as defined above, with useSafeTransferFrom set to false. function transferTokenWithChip(bytes calldata signatureFromChip, uint256 blockNumberUsedInSig) external; - /// @notice Emitted when a token is minted + /// @notice Emitted when a token is minted. event PBTMint(uint256 indexed tokenId, address indexed chipAddress); /// @notice Emitted when a token is mapped to a different chip. diff --git a/src/PBTSimple.sol b/src/PBTSimple.sol index 01fb588..462139f 100644 --- a/src/PBTSimple.sol +++ b/src/PBTSimple.sol @@ -45,17 +45,25 @@ contract PBTSimple is ERC721ReadOnly, IPBT { uint256[] memory tokenIds, bool throwIfTokenAlreadyMinted ) internal { - if (tokenIds.length != chipAddresses.length) { + uint256 tokenIdsLength = tokenIds.length; + if (tokenIdsLength != chipAddresses.length) { revert ArrayLengthMismatch(); } - for (uint256 i = 0; i < tokenIds.length; ++i) { + uint256 i = 0; + do { address chipAddress = chipAddresses[i]; uint256 tokenId = tokenIds[i]; + if (throwIfTokenAlreadyMinted && _exists(tokenId)) { revert SeedingChipDataForExistingToken(); } + _tokenDatas[chipAddress] = TokenData(tokenId, chipAddress, true); - } + + unchecked { + ++i; + } + } while(i < tokenIdsLength); } // Should only be called for tokenIds that have been minted @@ -66,20 +74,29 @@ contract PBTSimple is ERC721ReadOnly, IPBT { if (chipAddressesOld.length != chipAddressesNew.length) { revert ArrayLengthMismatch(); } - for (uint256 i = 0; i < chipAddressesOld.length; ++i) { + uint256 i = 0; + do { address oldChipAddress = chipAddressesOld[i]; TokenData memory oldTokenData = _tokenDatas[oldChipAddress]; - if (!oldTokenData.set) { + + if (!oldTokenData.set) { revert UpdatingChipForUnsetChipMapping(); } + address newChipAddress = chipAddressesNew[i]; uint256 tokenId = oldTokenData.tokenId; _tokenDatas[newChipAddress] = TokenData(tokenId, newChipAddress, true); + if (_exists(tokenId)) { emit PBTChipRemapping(tokenId, oldChipAddress, newChipAddress); } + delete _tokenDatas[oldChipAddress]; - } + + unchecked { + ++i; + } + } while(i < chipAddressesOld.length); } function tokenIdFor(address chipAddress) external view override returns (uint256) { @@ -156,6 +173,7 @@ contract PBTSimple is ERC721ReadOnly, IPBT { function _getTokenDataForChipSignature(bytes calldata signatureFromChip, uint256 blockNumberUsedInSig) internal + view returns (TokenData memory) { // The blockNumberUsedInSig must be in a previous block because the blockhash of the current