Skip to content

Commit

Permalink
V2 (#39)
Browse files Browse the repository at this point in the history
PBT v2
  • Loading branch information
Vectorized authored Oct 12, 2024
1 parent 7efef94 commit 1cded9e
Show file tree
Hide file tree
Showing 23 changed files with 1,447 additions and 102 deletions.
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@
[submodule "lib/openzeppelin-contracts"]
path = lib/openzeppelin-contracts
url = https://github.com/OpenZeppelin/openzeppelin-contracts
[submodule "lib/solady"]
path = lib/solady
url = https://github.com/vectorized/solady
69 changes: 45 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,45 +2,51 @@

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.
- 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).
- 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.
From the [Azuki](https://twitter.com/Azuki) team.
**Chiru Labs is not liable for any outcomes as a result of using PBT**, DYOR. Repo still in beta.

Note: the frontend library for chip signatures can be found [here](https://github.com/chiru-labs/pbt-chip-client).

## Resources

- [pbt.io](https://www.pbt.io/)
- [Draft EIP](https://eips.ethereum.org/EIPS/eip-5791)
- [Blog](https://www.azuki.com/updates/pbt)
- [pbt.io](https://www.pbt.io/)
- [Draft EIP](https://eips.ethereum.org/EIPS/eip-5791)
- [Blog](https://www.azuki.com/updates/pbt)

## How does PBT work?

#### Requirements

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`).
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` (`transferToken`).

#### 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://eips.ethereum.org/EIPS/eip-5791) and inlined into `IPBT.sol`.

#### v2 versus v1

v2 is a newer implementation of PBT that uses timestamp as the way to determine if a transfer is eligible. When using this version, it's recommended to have a non-deterministic nonce for signatures. An implementation of this can be seen in `/v2/PBTSimple.sol`. You can choose to import PBTSimple directly into your project or build your own.

v1 is considered legacy and uses blockhash instead of timestamp. With the speed of blocks on L2's, moving to timestamp over blockhash makes PBT possible on ever faster chains.

#### Reference Implementation

A simple mint for a physical drop could look something like this:
Expand All @@ -53,20 +59,33 @@ 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[] memory chipAddresses, uint256[] memory tokenIds)
PBTSimple("Example", "EXAMPLE")
/// @param tokenIds The tokenIds to map to the addresses
/// @param maxDurationWindow Maximum duration for a signature to be valid since the timestamp used in the signature.
constructor(
address[] memory chipAddresses,
uint256[] memory tokenIds,
uint256 maxDurationWindow
)
PBTSimple("Example", "EXAMPLE", maxDurationWindow)
{
_seedChipToTokenMapping(chipAddresses, tokenIds);
for (uint256 i = 0; i < chipAddresses.length; i++) {
_setChip(tokenIds[i], chipAddresses[i]);
}
}
/// @param signatureFromChip The signature is an EIP-191 signature of (msgSender, blockhash),
/// where blockhash is the block hash for a recent block (blockNumberUsedInSig).
/// @dev We will soon release a client-side library that helps with signature generation.
function mintPBT(
bytes calldata signatureFromChip,
uint256 blockNumberUsedInSig
) external {
_mintTokenWithChip(signatureFromChip, blockNumberUsedInSig);
/// @param to the address to which the PBT will be minted
/// @param chipId the chip address being minted
/// @param chipSignature the signature generated by the chip
/// @param signatureTimestamp the timestamp used in the signature
/// @param extras misc data, for extensions or custom logic in mint
function mint(
address to,
address chipId,
bytes memory chipSignature,
uint256 signatureTimestamp,
bytes memory extras
) external returns (uint256) {
return _mint(to, chipId, chipSig, sigTimestamp, extras);
}
}
```
Expand All @@ -78,6 +97,7 @@ As mentioned above, this repo is still in beta and more documentation is on its
TODO: flesh this section out more

3 key parts.

- Acquire chips, embed them into the physical items.
- The Azuki hoodies used chips from [kongiscash](https://twitter.com/kongiscash). [Docs for chips](https://docs.arx.org/)
- Before you sell/ship the physicals, make sure you save the public keys of the chips first, since the smart contract you deploy will need to know which chips are applicable to it. For kongiscash chips, you can use their [bulk scanning tool](https://bulk.vrfy.ch/) to do so.
Expand All @@ -92,6 +112,7 @@ TODO: flesh this section out more
- For now, a working end-to-end flow will also require building out a simple frontend for a mobile browser to grab chip signatures to pass into the smart contract. We have open-sourced a [light js lib](https://github.com/chiru-labs/pbt-chip-client) to help with that piece.

## TODO

- [ ] CI pipeline
- [ ] PBT Locking extension (where transfers need to be approved by the current owner first)
- [ ] PBT implementation that doesn't require seeding chip addresses to the contract pre-mint
Expand Down
23 changes: 19 additions & 4 deletions foundry.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
# Foundry Configuration File
# Default definitions: https://github.com/gakonst/foundry/blob/b7917fa8491aedda4dd6db53fbb206ea233cd531/config/src/lib.rs#L782
# See more config options at: https://github.com/gakonst/foundry/tree/master/config

# The Default Profile
[profile.default]
src = 'src'
out = 'out'
libs = ['lib']
auto_detect_solc = false
optimizer = true
optimizer_runs = 1_000
gas_limit = 100_000_000 # ETH is 30M, but we use a higher value.


[fmt]
line_length = 100 # While we allow up to 120, we lint at 100 for readability.

[profile.default.fuzz]
runs = 256

# See more config options https://github.com/foundry-rs/foundry/tree/master/config
[invariant]
depth = 15
runs = 10
123 changes: 123 additions & 0 deletions gas_report.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
No files changed, compilation skipped

Ran 1 test for test/utils/SoladyTest.sol:SoladyTest
[PASS] test__codesize() (gas: 1102)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.83ms (495.71µs CPU time)

Ran 1 test for test/utils/TestPlus.sol:TestPlus
[PASS] test__codesize() (gas: 406)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.81ms (381.29µs CPU time)

Ran 5 tests for test/v2/PBTSimpleTest.sol:PBTSimpleTest
[PASS] testAdvanceBlock(bytes32) (runs: 256, μ: 31391, ~: 31408)
[PASS] testMintAndEverything(bytes32) (runs: 256, μ: 248453, ~: 292672)
[PASS] testSetAndGetChip() (gas: 361565)
[PASS] testSetAndGetChip(bytes32) (runs: 256, μ: 197758, ~: 185631)
[PASS] test__codesize() (gas: 21150)
Suite result: ok. 5 passed; 0 failed; 0 skipped; finished in 157.29ms (203.89ms CPU time)

Ran 16 tests for test/v1/PBTSimpleTest.sol:PBTSimpleTest
[PASS] testGetTokenDataForChipSignature() (gas: 150319)
[PASS] testGetTokenDataForChipSignatureBlockNumTooOld() (gas: 145681)
[PASS] testGetTokenDataForChipSignatureInvalid() (gas: 154943)
[PASS] testGetTokenDataForChipSignatureInvalidBlockNumber() (gas: 145520)
[PASS] testIsChipSignatureForToken() (gas: 286730)
[PASS] testMintTokenWithChip() (gas: 227486)
[PASS] testSeedChipToTokenMapping() (gas: 137831)
[PASS] testSeedChipToTokenMappingExistingToken() (gas: 302573)
[PASS] testSeedChipToTokenMappingInvalidInput() (gas: 39711)
[PASS] testSupportsInterface() (gas: 6962)
[PASS] testTokenIdFor() (gas: 164321)
[PASS] testTokenIdMappedFor() (gas: 89521)
[PASS] testTransferTokenWithChip(bool) (runs: 256, μ: 332865, ~: 332712)
[PASS] testUpdateChips() (gas: 249262)
[PASS] testUpdateChipsInvalidInput() (gas: 39071)
[PASS] testUpdateChipsUnsetChip() (gas: 46416)
Suite result: ok. 16 passed; 0 failed; 0 skipped; finished in 163.40ms (102.25ms CPU time)

Ran 7 tests for test/v1/PBTRandomTest.sol:PBTRandomTest
[PASS] testGetTokenDataForChipSignature() (gas: 261689)
[PASS] testGetTokenDataForChipSignatureInvalid() (gas: 271101)
[PASS] testIsChipSignatureForToken() (gas: 267043)
[PASS] testSupportsInterface() (gas: 6963)
[PASS] testTokenIdFor() (gas: 205466)
[PASS] testTransferTokenWithChip(bool) (runs: 256, μ: 323670, ~: 323526)
[PASS] testUpdateChips() (gas: 514145)
Suite result: ok. 7 passed; 0 failed; 0 skipped; finished in 163.44ms (162.09ms CPU time)

Ran 5 tests for test/v1/ERC721ReadOnlyTest.sol:ERC721ReadOnlyTest
[PASS] testApprove() (gas: 32231)
[PASS] testGetApproved() (gas: 16203)
[PASS] testIsApprovedForAll() (gas: 10045)
[PASS] testSetApprovalForAll() (gas: 32268)
[PASS] testTransferFunctions() (gas: 85044)
Suite result: ok. 5 passed; 0 failed; 0 skipped; finished in 163.42ms (486.46µs CPU time)
| src/v1/mocks/ERC721ReadOnlyMock.sol:ERC721ReadOnlyMock contract | | | | | |
|-----------------------------------------------------------------|-----------------|-------|--------|-------|---------|
| Deployment Cost | Deployment Size | | | | |
| 874309 | 4463 | | | | |
| Function Name | min | avg | median | max | # calls |
| approve | 22044 | 22044 | 22044 | 22044 | 1 |
| getApproved | 2581 | 2583 | 2583 | 2586 | 2 |
| isApprovedForAll | 545 | 545 | 545 | 545 | 1 |
| mint | 68743 | 68743 | 68743 | 68743 | 5 |
| safeTransferFrom(address,address,uint256) | 22583 | 22583 | 22583 | 22583 | 1 |
| safeTransferFrom(address,address,uint256,bytes) | 23114 | 23114 | 23114 | 23114 | 1 |
| setApprovalForAll | 22082 | 22082 | 22082 | 22082 | 1 |
| transferFrom | 22539 | 22539 | 22539 | 22539 | 1 |


| src/v1/mocks/PBTRandomMock.sol:PBTRandomMock contract | | | | | |
|-------------------------------------------------------|-----------------|--------|--------|--------|---------|
| Deployment Cost | Deployment Size | | | | |
| 2137463 | 10294 | | | | |
| Function Name | min | avg | median | max | # calls |
| getTokenData | 1020 | 1020 | 1020 | 1020 | 4 |
| getTokenDataForChipSignature | 970 | 3388 | 3371 | 5840 | 4 |
| isChipSignatureForToken | 3299 | 4523 | 4523 | 5748 | 2 |
| mintTokenWithChip | 134282 | 134282 | 134282 | 134294 | 262 |
| ownerOf | 624 | 624 | 624 | 624 | 512 |
| seedChipAddresses | 47151 | 96996 | 97285 | 97285 | 261 |
| supportsInterface | 458 | 510 | 510 | 563 | 2 |
| tokenIdFor | 830 | 1726 | 1726 | 2622 | 2 |
| transferTokenWithChip | 65258 | 65402 | 65258 | 65567 | 256 |
| updateChips | 26441 | 72919 | 72919 | 119398 | 2 |


| src/v1/mocks/PBTSimpleMock.sol:PBTSimpleMock contract | | | | | |
|-------------------------------------------------------|-----------------|--------|--------|--------|---------|
| Deployment Cost | Deployment Size | | | | |
| 2035062 | 9856 | | | | |
| Function Name | min | avg | median | max | # calls |
| balanceOf | 657 | 657 | 657 | 657 | 513 |
| getTokenData | 1020 | 1020 | 1020 | 1020 | 6 |
| getTokenDataForChipSignature | 800 | 4242 | 3292 | 9586 | 4 |
| isChipSignatureForToken | 5774 | 5774 | 5774 | 5774 | 1 |
| mint | 68747 | 68747 | 68747 | 68747 | 517 |
| mintTokenWithChip | 80629 | 80629 | 80629 | 80629 | 1 |
| seedChipToTokenMapping | 24288 | 117231 | 118291 | 118291 | 269 |
| supportsInterface | 458 | 510 | 510 | 563 | 2 |
| tokenIdFor | 1056 | 1592 | 1082 | 2640 | 3 |
| tokenIdMappedFor | 830 | 1723 | 1723 | 2616 | 2 |
| transferTokenWithChip | 48042 | 48195 | 48042 | 48351 | 256 |
| updateChips | 23542 | 57397 | 28616 | 120033 | 3 |


| src/v2/mocks/PBTSimpleMock.sol:PBTSimpleMock contract | | | | | |
|-------------------------------------------------------|-----------------|-------|--------|--------|---------|
| Deployment Cost | Deployment Size | | | | |
| 1634044 | 8061 | | | | |
| Function Name | min | avg | median | max | # calls |
| chipNonce | 585 | 1269 | 585 | 2585 | 748 |
| isChipSignatureForToken | 1508 | 3303 | 3544 | 5192 | 79 |
| mint | 24897 | 86321 | 102722 | 119026 | 256 |
| ownerOf | 581 | 581 | 581 | 581 | 318 |
| setChip | 30726 | 65186 | 67738 | 68338 | 747 |
| tokenIdFor | 449 | 1201 | 872 | 4848 | 2327 |
| transferToken | 71458 | 74324 | 73668 | 90586 | 144 |
| unsetChip | 23341 | 23409 | 23353 | 23725 | 306 |




Ran 6 test suites in 229.57ms (655.19ms CPU time): 35 tests passed, 0 failed, 0 skipped (35 total tests)
1 change: 1 addition & 0 deletions lib/solady
Submodule solady added at a1f9be
13 changes: 6 additions & 7 deletions src/ERC721ReadOnly.sol → src/v1/ERC721ReadOnly.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@ import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
/**
* An implementation of 721 that's publicly readonly (no approvals or transfers exposed).
*/

contract ERC721ReadOnly is ERC721 {
constructor(string memory name_, string memory symbol_) ERC721(name_, symbol_) {}

function approve(address to, uint256 tokenId) public virtual override {
function approve(address, uint256) public virtual override {
revert("ERC721 public approve not allowed");
}

Expand All @@ -19,23 +18,23 @@ contract ERC721ReadOnly is ERC721 {
return address(0);
}

function setApprovalForAll(address operator, bool approved) public virtual override {
function setApprovalForAll(address, bool) public virtual override {
revert("ERC721 public setApprovalForAll not allowed");
}

function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) {
function isApprovedForAll(address, address) public view virtual override returns (bool) {
return false;
}

function transferFrom(address from, address to, uint256 tokenId) public virtual override {
function transferFrom(address, address, uint256) public virtual override {
revert("ERC721 public transferFrom not allowed");
}

function safeTransferFrom(address from, address to, uint256 tokenId) public virtual override {
function safeTransferFrom(address, address, uint256) public virtual override {
revert("ERC721 public safeTransferFrom not allowed");
}

function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public virtual override {
function safeTransferFrom(address, address, uint256, bytes memory) public virtual override {
revert("ERC721 public safeTransferFrom not allowed");
}
}
Loading

0 comments on commit 1cded9e

Please sign in to comment.