Skip to content

Commit

Permalink
Merge branch 'main' into npm-registry-optimize
Browse files Browse the repository at this point in the history
  • Loading branch information
djdabs committed Oct 12, 2024
2 parents c6b8151 + 3f3b3f3 commit 835db86
Show file tree
Hide file tree
Showing 23 changed files with 1,434 additions and 91 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
45 changes: 32 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ NFT collectors enjoy collecting digital assets and sharing them with others onli

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).
Expand Down Expand Up @@ -51,7 +51,7 @@ This approach assumes that the physical item must have a chip attached to it tha
- 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

Expand All @@ -63,6 +63,12 @@ On a high level:

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 @@ -75,20 +81,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 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");
}
}
17 changes: 10 additions & 7 deletions src/IPBT.sol → src/v1/IPBT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import "@openzeppelin/contracts/utils/Strings.sol";
* @dev Contract for PBTs (Physical Backed Tokens).
* NFTs that are backed by a physical asset, through a chip embedded in the physical asset.
*/

interface IPBT {
/// @notice Returns the token id for a given chip address.
/// @dev Throws if there is no existing token for the chip in the collection.
Expand All @@ -26,10 +25,11 @@ interface IPBT {
/// @param payload Arbitrary data that is signed by the chip to produce the signature param.
/// @param signature Chip's signature of the passed-in payload.
/// @return Whether the signature of the payload was signed by the chip linked to the token id.
function isChipSignatureForToken(uint256 tokenId, bytes calldata payload, bytes calldata signature)
external
view
returns (bool);
function isChipSignatureForToken(
uint256 tokenId,
bytes calldata payload,
bytes calldata signature
) external view returns (bool);

/// @notice Transfers the token into the message sender's wallet.
/// @param signatureFromChip An EIP-191 signature of (msgSender, blockhash), where blockhash is the block hash for blockNumberUsedInSig.
Expand All @@ -47,12 +47,15 @@ interface IPBT {
) external;

/// @notice Calls transferTokenWithChip as defined above, with useSafeTransferFrom set to false.
function transferTokenWithChip(bytes calldata signatureFromChip, uint256 blockNumberUsedInSig) external;
function transferTokenWithChip(bytes calldata signatureFromChip, uint256 blockNumberUsedInSig)
external;

/// @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.
/// Chip replacements may be useful in certain scenarios (e.g. chip defect).
event PBTChipRemapping(uint256 indexed tokenId, address indexed oldChipAddress, address indexed newChipAddress);
event PBTChipRemapping(
uint256 indexed tokenId, address indexed oldChipAddress, address indexed newChipAddress
);
}
Loading

0 comments on commit 835db86

Please sign in to comment.