├── CODEOWNERS ├── remappings.txt ├── .gitignore ├── script └── Contract.s.sol ├── .gitmodules ├── package.json ├── src ├── v1 │ ├── mocks │ │ ├── ERC721ReadOnlyMock.sol │ │ ├── PBTSimpleMock.sol │ │ └── PBTRandomMock.sol │ ├── ERC721ReadOnly.sol │ ├── IPBT.sol │ ├── PBTSimple.sol │ └── PBTRandom.sol └── v2 │ ├── mocks │ └── PBTSimpleMock.sol │ ├── ERC721ReadOnly.sol │ ├── IPBT.sol │ └── PBTSimple.sol ├── test ├── utils │ ├── SoladyTest.sol │ ├── Brutalizer.sol │ └── TestPlus.sol ├── v1 │ ├── ERC721ReadOnlyTest.sol │ ├── PBTSimpleTest.sol │ └── PBTRandomTest.sol └── v2 │ └── PBTSimpleTest.sol ├── .github └── workflows │ ├── deploy_npm.yml │ └── test.yml ├── foundry.toml ├── LICENSE ├── .gas-snapshot ├── README.md └── gas_report.txt /CODEOWNERS: -------------------------------------------------------------------------------- 1 | .github/workflows/ @chiru-labs 2 | -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | @openzeppelin/=lib/openzeppelin-contracts/ 2 | forge-std/=lib/forge-std/src/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores development broadcast logs 6 | !/broadcast 7 | /broadcast/* 8 | /broadcast/*/31337/ 9 | 10 | # Ignore dependencies 11 | /node_modules 12 | -------------------------------------------------------------------------------- /script/Contract.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Script.sol"; 5 | 6 | contract ContractScript is Script { 7 | function setUp() public {} 8 | 9 | function run() public { 10 | vm.broadcast(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/openzeppelin-contracts"] 5 | path = lib/openzeppelin-contracts 6 | url = https://github.com/OpenZeppelin/openzeppelin-contracts 7 | [submodule "lib/solady"] 8 | path = lib/solady 9 | url = https://github.com/vectorized/solady 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@chiru-labs/pbt", 3 | "version": "0.4.0", 4 | "description": "Contracts for Physical Backed Tokens (ERC-5791). ", 5 | "files": [ 6 | "/src/**/*.sol" 7 | ], 8 | "repository": { 9 | "type": "git", 10 | "url": "git@github.com:chiru-labs/PBT.git" 11 | }, 12 | "author": "chiru-labs", 13 | "license": "MIT", 14 | "dependencies": { 15 | "@chiru-labs/pbt": "^0.3.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/v1/mocks/ERC721ReadOnlyMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Test.sol"; 5 | import "../ERC721ReadOnly.sol"; 6 | 7 | contract ERC721ReadOnlyMock is ERC721ReadOnly { 8 | constructor(string memory name_, string memory symbol_) ERC721ReadOnly(name_, symbol_) {} 9 | 10 | function mint(address to, uint256 tokenId) public { 11 | _mint(to, tokenId); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/utils/SoladyTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.4; 3 | 4 | import "forge-std/Test.sol"; 5 | import "./TestPlus.sol"; 6 | 7 | contract SoladyTest is Test, TestPlus { 8 | /// @dev Alias for `_hem`. 9 | function _bound(uint256 x, uint256 min, uint256 max) 10 | internal 11 | pure 12 | virtual 13 | returns (uint256) 14 | { 15 | return _hem(x, min, max); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/deploy_npm.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package to npmjs 2 | on: 3 | release: 4 | types: [published] 5 | jobs: 6 | build: 7 | environment: production 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v2 12 | # Setup .npmrc file to publish to npm 13 | - uses: actions/setup-node@v2 14 | with: 15 | node-version: "16.x" 16 | registry-url: "https://registry.npmjs.org" 17 | - run: npm ci 18 | - run: npm publish --access public 19 | env: 20 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 21 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | # Foundry Configuration File 2 | # Default definitions: https://github.com/gakonst/foundry/blob/b7917fa8491aedda4dd6db53fbb206ea233cd531/config/src/lib.rs#L782 3 | # See more config options at: https://github.com/gakonst/foundry/tree/master/config 4 | 5 | # The Default Profile 6 | [profile.default] 7 | auto_detect_solc = false 8 | optimizer = true 9 | optimizer_runs = 1_000 10 | gas_limit = 100_000_000 # ETH is 30M, but we use a higher value. 11 | 12 | 13 | [fmt] 14 | line_length = 100 # While we allow up to 120, we lint at 100 for readability. 15 | 16 | [profile.default.fuzz] 17 | runs = 256 18 | 19 | [invariant] 20 | depth = 15 21 | runs = 10 22 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [main] 7 | paths-ignore: 8 | - '**.md' 9 | pull_request: 10 | branches: [main] 11 | paths-ignore: 12 | - '**.md' 13 | 14 | env: 15 | FOUNDRY_PROFILE: ci 16 | 17 | jobs: 18 | check: 19 | strategy: 20 | fail-fast: true 21 | 22 | name: Foundry project 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v3 26 | with: 27 | submodules: recursive 28 | 29 | - name: Install Foundry 30 | uses: foundry-rs/foundry-toolchain@v1 31 | with: 32 | version: nightly 33 | 34 | - name: Run Forge build 35 | run: | 36 | forge --version 37 | forge build --sizes 38 | id: build 39 | 40 | - name: Run Forge tests 41 | run: | 42 | forge test -vvv 43 | id: test 44 | -------------------------------------------------------------------------------- /src/v2/mocks/PBTSimpleMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "../PBTSimple.sol"; 5 | 6 | contract PBTSimpleMock is PBTSimple { 7 | constructor(string memory name_, string memory symbol_, uint256 maxDurationWindow_) PBTSimple(name_, symbol_, maxDurationWindow_) {} 8 | 9 | function setChip(uint256 tokenId, address chipId) public { 10 | _setChip(tokenId, chipId); 11 | } 12 | 13 | function unsetChip(uint256 tokenId) public { 14 | _unsetChip(tokenId); 15 | } 16 | 17 | function directGetTokenId(address chipId) public view returns (uint256) { 18 | return _tokenIds[chipId]; 19 | } 20 | 21 | function directGetChipId(uint256 tokenId) public view returns (address) { 22 | return _chipIds[tokenId]; 23 | } 24 | 25 | function mint(address to, address chipId, bytes memory chipSig, uint256 sigTimestamp, bytes memory extras) public returns (uint256) { 26 | return _mint(to, chipId, chipSig, sigTimestamp, extras); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 The Azuki Company 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/v1/ERC721ReadOnly.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 5 | 6 | /** 7 | * An implementation of 721 that's publicly readonly (no approvals or transfers exposed). 8 | */ 9 | contract ERC721ReadOnly is ERC721 { 10 | constructor(string memory name_, string memory symbol_) ERC721(name_, symbol_) {} 11 | 12 | function approve(address, uint256) public virtual override { 13 | revert("ERC721 public approve not allowed"); 14 | } 15 | 16 | function getApproved(uint256 tokenId) public view virtual override returns (address) { 17 | require(_exists(tokenId), "ERC721: invalid token ID"); 18 | return address(0); 19 | } 20 | 21 | function setApprovalForAll(address, bool) public virtual override { 22 | revert("ERC721 public setApprovalForAll not allowed"); 23 | } 24 | 25 | function isApprovedForAll(address, address) public view virtual override returns (bool) { 26 | return false; 27 | } 28 | 29 | function transferFrom(address, address, uint256) public virtual override { 30 | revert("ERC721 public transferFrom not allowed"); 31 | } 32 | 33 | function safeTransferFrom(address, address, uint256) public virtual override { 34 | revert("ERC721 public safeTransferFrom not allowed"); 35 | } 36 | 37 | function safeTransferFrom(address, address, uint256, bytes memory) public virtual override { 38 | revert("ERC721 public safeTransferFrom not allowed"); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/v2/ERC721ReadOnly.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 5 | 6 | /** 7 | * An implementation of 721 that's publicly readonly (no approvals or transfers exposed). 8 | */ 9 | contract ERC721ReadOnly is ERC721 { 10 | constructor(string memory name_, string memory symbol_) ERC721(name_, symbol_) {} 11 | 12 | function approve(address, uint256) public virtual override { 13 | revert("ERC721 public approve not allowed"); 14 | } 15 | 16 | function getApproved(uint256 tokenId) public view virtual override returns (address) { 17 | require(_exists(tokenId), "ERC721: invalid token ID"); 18 | return address(0); 19 | } 20 | 21 | function setApprovalForAll(address, bool) public virtual override { 22 | revert("ERC721 public setApprovalForAll not allowed"); 23 | } 24 | 25 | function isApprovedForAll(address, address) public view virtual override returns (bool) { 26 | return false; 27 | } 28 | 29 | function transferFrom(address, address, uint256) public virtual override { 30 | revert("ERC721 public transferFrom not allowed"); 31 | } 32 | 33 | function safeTransferFrom(address, address, uint256) public virtual override { 34 | revert("ERC721 public safeTransferFrom not allowed"); 35 | } 36 | 37 | function safeTransferFrom(address, address, uint256, bytes memory) public virtual override { 38 | revert("ERC721 public safeTransferFrom not allowed"); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/v1/mocks/PBTSimpleMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "../PBTSimple.sol"; 5 | 6 | contract PBTSimpleMock is PBTSimple { 7 | constructor(string memory name_, string memory symbol_) PBTSimple(name_, symbol_) {} 8 | 9 | function mint(address to, uint256 tokenId) public { 10 | _mint(to, tokenId); 11 | } 12 | 13 | function seedChipToTokenMapping( 14 | address[] memory chipAddresses, 15 | uint256[] memory tokenIds, 16 | bool throwIfTokenAlreadyMinted 17 | ) public { 18 | _seedChipToTokenMapping(chipAddresses, tokenIds, throwIfTokenAlreadyMinted); 19 | } 20 | 21 | function getTokenData(address addr) public view returns (TokenData memory) { 22 | return _tokenDatas[addr]; 23 | } 24 | 25 | function updateChips(address[] calldata chipAddressesOld, address[] calldata chipAddressesNew) 26 | public 27 | { 28 | _updateChips(chipAddressesOld, chipAddressesNew); 29 | } 30 | 31 | function mintTokenWithChip(bytes calldata signatureFromChip, uint256 blockNumberUsedInSig) 32 | public 33 | returns (uint256) 34 | { 35 | return _mintTokenWithChip(signatureFromChip, blockNumberUsedInSig); 36 | } 37 | 38 | function getTokenDataForChipSignature( 39 | bytes calldata signatureFromChip, 40 | uint256 blockNumberUsedInSig 41 | ) public view returns (TokenData memory) { 42 | return _getTokenDataForChipSignature(signatureFromChip, blockNumberUsedInSig); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/v1/ERC721ReadOnlyTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Test.sol"; 5 | import "../../src/v1/mocks/ERC721ReadOnlyMock.sol"; 6 | 7 | contract ERC721ReadOnlyTest is Test { 8 | address public user1 = vm.addr(1); 9 | address public user2 = vm.addr(2); 10 | uint256 public tokenId = 1; 11 | ERC721ReadOnlyMock public erc721; 12 | 13 | function setUp() public { 14 | erc721 = new ERC721ReadOnlyMock("ReadOnly", "RO"); 15 | erc721.mint(user1, tokenId); 16 | } 17 | 18 | function testApprove() public { 19 | vm.expectRevert("ERC721 public approve not allowed"); 20 | erc721.approve(user1, 1); 21 | } 22 | 23 | function testGetApproved() public { 24 | assertEq(erc721.getApproved(tokenId), address(0)); 25 | vm.expectRevert("ERC721: invalid token ID"); 26 | erc721.getApproved(tokenId + 100); 27 | } 28 | 29 | function testSetApprovalForAll() public { 30 | vm.expectRevert("ERC721 public setApprovalForAll not allowed"); 31 | erc721.setApprovalForAll(user1, true); 32 | } 33 | 34 | function testIsApprovedForAll() public { 35 | assertEq(erc721.isApprovedForAll(user1, user2), false); 36 | } 37 | 38 | function testTransferFunctions() public { 39 | vm.expectRevert("ERC721 public transferFrom not allowed"); 40 | erc721.transferFrom(user1, user2, tokenId); 41 | 42 | vm.expectRevert("ERC721 public safeTransferFrom not allowed"); 43 | erc721.safeTransferFrom(user1, user2, tokenId); 44 | 45 | vm.expectRevert("ERC721 public safeTransferFrom not allowed"); 46 | erc721.safeTransferFrom(user1, user2, tokenId, ""); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/v1/mocks/PBTRandomMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "../PBTRandom.sol"; 5 | 6 | contract PBTRandomMock is PBTRandom { 7 | constructor(string memory name_, string memory symbol_, uint256 supply_) 8 | PBTRandom(name_, symbol_, supply_) 9 | {} 10 | 11 | function mint(address to, uint256 tokenId) public { 12 | _mint(to, tokenId); 13 | } 14 | 15 | function getTokenData(address addr) public view returns (TokenData memory) { 16 | return _tokenDatas[addr]; 17 | } 18 | 19 | function updateChips(address[] calldata chipAddressesOld, address[] calldata chipAddressesNew) 20 | public 21 | { 22 | _updateChips(chipAddressesOld, chipAddressesNew); 23 | } 24 | 25 | function seedChipAddresses(address[] calldata chipAddresses) public { 26 | _seedChipAddresses(chipAddresses); 27 | } 28 | 29 | function mintTokenWithChip(bytes calldata signatureFromChip, uint256 blockNumberUsedInSig) 30 | public 31 | returns (uint256) 32 | { 33 | return _mintTokenWithChip(signatureFromChip, blockNumberUsedInSig); 34 | } 35 | 36 | function getTokenDataForChipSignature( 37 | bytes calldata signatureFromChip, 38 | uint256 blockNumberUsedInSig 39 | ) public view returns (TokenData memory) { 40 | return _getTokenDataForChipSignature(signatureFromChip, blockNumberUsedInSig); 41 | } 42 | 43 | function getAvailableRemainingTokens(uint256 index) public view returns (uint256) { 44 | return _availableRemainingTokens[index]; 45 | } 46 | 47 | function useRandomAvailableTokenId() public returns (uint256) { 48 | return _useRandomAvailableTokenId(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /.gas-snapshot: -------------------------------------------------------------------------------- 1 | ERC721ReadOnlyTest:testApprove() (gas: 10677) 2 | ERC721ReadOnlyTest:testGetApproved() (gas: 16236) 3 | ERC721ReadOnlyTest:testIsApprovedForAll() (gas: 10051) 4 | ERC721ReadOnlyTest:testSetApprovalForAll() (gas: 10714) 5 | ERC721ReadOnlyTest:testTransferFunctions() (gas: 19077) 6 | PBTRandomTest:testGetTokenDataForChipSignature() (gas: 210148) 7 | PBTRandomTest:testGetTokenDataForChipSignatureInvalid() (gas: 216406) 8 | PBTRandomTest:testIsChipSignatureForToken() (gas: 215744) 9 | PBTRandomTest:testMintTokenWithChip() (gas: 166964) 10 | PBTRandomTest:testSupportsInterface() (gas: 7037) 11 | PBTRandomTest:testTokenIdFor() (gas: 153746) 12 | PBTRandomTest:testTransferTokenWithChip(bool) (runs: 256, μ: 225474, ~: 225393) 13 | PBTRandomTest:testUpdateChips() (gas: 320443) 14 | PBTRandomTest:testUseRandomAvailableTokenId() (gas: 117179) 15 | PBTSimpleTest:testGetTokenDataForChipSignature() (gas: 127958) 16 | PBTSimpleTest:testGetTokenDataForChipSignatureBlockNumTooOld() (gas: 122512) 17 | PBTSimpleTest:testGetTokenDataForChipSignatureInvalid() (gas: 131804) 18 | PBTSimpleTest:testGetTokenDataForChipSignatureInvalidBlockNumber() (gas: 122351) 19 | PBTSimpleTest:testIsChipSignatureForToken() (gas: 217369) 20 | PBTSimpleTest:testMintTokenWithChip() (gas: 176650) 21 | PBTSimpleTest:testSeedChipToTokenMapping() (gas: 115247) 22 | PBTSimpleTest:testSeedChipToTokenMappingExistingToken() (gas: 212294) 23 | PBTSimpleTest:testSeedChipToTokenMappingInvalidInput() (gas: 17178) 24 | PBTSimpleTest:testSupportsInterface() (gas: 7059) 25 | PBTSimpleTest:testTokenIdFor() (gas: 117084) 26 | PBTSimpleTest:testTokenIdMappedFor() (gas: 66558) 27 | PBTSimpleTest:testTransferTokenWithChip(bool) (runs: 256, μ: 211956, ~: 211875) 28 | PBTSimpleTest:testUpdateChips() (gas: 171052) 29 | PBTSimpleTest:testUpdateChipsInvalidInput() (gas: 16433) 30 | PBTSimpleTest:testUpdateChipsUnsetChip() (gas: 23430) 31 | -------------------------------------------------------------------------------- /src/v2/IPBT.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.26; 3 | 4 | /// @dev Contract for PBTs (Physical Backed Tokens). 5 | /// NFTs that are backed by a physical asset, through a chip embedded in the physical asset. 6 | interface IPBT { 7 | /// @dev Returns the ERC-721 `tokenId` for a given chip address. 8 | /// Reverts if `chipId` has not been paired to a `tokenId`. 9 | /// For minimalism, this will NOT revert if the `tokenId` does not exist. 10 | /// If there is a need to check for token existence, external contracts can 11 | /// call `ERC721.ownerOf(uint256 tokenId)` and check if it passes or reverts. 12 | /// @param chipId The address for the chip embedded in the physical item 13 | /// (computed from the chip's public key). 14 | function tokenIdFor(address chipId) external view returns (uint256 tokenId); 15 | 16 | /// @dev Returns true if `signature` is signed by the chip assigned to `tokenId`, else false. 17 | /// Reverts if `tokenId` has not been paired to a chip. 18 | /// For minimalism, this will NOT revert if the `tokenId` does not exist. 19 | /// If there is a need to check for token existence, external contracts can 20 | /// call `ERC721.ownerOf(uint256 tokenId)` and check if it passes or reverts. 21 | /// @param tokenId ERC-721 `tokenId`. 22 | /// @param data Arbitrary bytes string that is signed by the chip to produce `signature`. 23 | /// @param signature EIP-191 signature by the chip to check. 24 | function isChipSignatureForToken(uint256 tokenId, bytes calldata data, bytes calldata signature) 25 | external 26 | view 27 | returns (bool); 28 | 29 | /// @dev Transfers the token into the address. 30 | /// Returns the `tokenId` transferred. 31 | /// @param to The recipient. Dynamic to allow easier transfers to vaults. 32 | /// @param chipId Chip ID (address) of chip being transferred. 33 | /// @param chipSignature EIP-191 signature by the chip to authorize the transfer. 34 | /// @param signatureTimestamp Timestamp used in `chipSignature`. 35 | /// @param useSafeTransferFrom Whether ERC-721's `safeTransferFrom` should be used, 36 | /// instead of `transferFrom`. 37 | /// @param extras Additional data that can be used for additional logic/context 38 | /// when the PBT is transferred. 39 | function transferToken( 40 | address to, 41 | address chipId, 42 | bytes calldata chipSignature, 43 | uint256 signatureTimestamp, 44 | bool useSafeTransferFrom, 45 | bytes calldata extras 46 | ) external returns (uint256 tokenId); 47 | 48 | /// @dev Emitted when `chipId` is paired to `tokenId`. 49 | /// `tokenId` may not necessarily exist during assignment. 50 | /// Indexers can combine this event with the {ERC721.Transfer} event to 51 | /// infer which tokens exists and are paired with a chip ID. 52 | event ChipSet(uint256 indexed tokenId, address indexed chipId); 53 | } 54 | -------------------------------------------------------------------------------- /src/v1/IPBT.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; 5 | import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; 6 | import "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; 7 | import "@openzeppelin/contracts/utils/Address.sol"; 8 | import "@openzeppelin/contracts/utils/Context.sol"; 9 | import "@openzeppelin/contracts/utils/Strings.sol"; 10 | 11 | /** 12 | * @dev Contract for PBTs (Physical Backed Tokens). 13 | * NFTs that are backed by a physical asset, through a chip embedded in the physical asset. 14 | */ 15 | interface IPBT { 16 | /// @notice Returns the token id for a given chip address. 17 | /// @dev Throws if there is no existing token for the chip in the collection. 18 | /// @param chipAddress The address for the chip embedded in the physical item (computed from the chip's public key). 19 | /// @return The token id for the passed in chip address. 20 | function tokenIdFor(address chipAddress) external view returns (uint256); 21 | 22 | /// @notice Returns true if the chip for the specified token id is the signer of the signature of the payload. 23 | /// @dev Throws if tokenId does not exist in the collection. 24 | /// @param tokenId The token id. 25 | /// @param payload Arbitrary data that is signed by the chip to produce the signature param. 26 | /// @param signature Chip's signature of the passed-in payload. 27 | /// @return Whether the signature of the payload was signed by the chip linked to the token id. 28 | function isChipSignatureForToken( 29 | uint256 tokenId, 30 | bytes calldata payload, 31 | bytes calldata signature 32 | ) external view returns (bool); 33 | 34 | /// @notice Transfers the token into the message sender's wallet. 35 | /// @param signatureFromChip An EIP-191 signature of (msgSender, blockhash), where blockhash is the block hash for blockNumberUsedInSig. 36 | /// @param blockNumberUsedInSig The block number linked to the blockhash signed in signatureFromChip. Should be a recent block number. 37 | /// @param useSafeTransferFrom Whether EIP-721's safeTransferFrom should be used in the implementation, instead of transferFrom. 38 | /// 39 | /// @dev The implementation should check that block number be reasonably recent to avoid replay attacks of stale signatures. 40 | /// The implementation should also verify that the address signed in the signature matches msgSender. 41 | /// If the address recovered from the signature matches a chip address that's bound to an existing token, the token should be transferred to msgSender. 42 | /// If there is no existing token linked to the chip, the function should error. 43 | function transferTokenWithChip( 44 | bytes calldata signatureFromChip, 45 | uint256 blockNumberUsedInSig, 46 | bool useSafeTransferFrom 47 | ) external; 48 | 49 | /// @notice Calls transferTokenWithChip as defined above, with useSafeTransferFrom set to false. 50 | function transferTokenWithChip(bytes calldata signatureFromChip, uint256 blockNumberUsedInSig) 51 | external; 52 | 53 | /// @notice Emitted when a token is minted. 54 | event PBTMint(uint256 indexed tokenId, address indexed chipAddress); 55 | 56 | /// @notice Emitted when a token is mapped to a different chip. 57 | /// Chip replacements may be useful in certain scenarios (e.g. chip defect). 58 | event PBTChipRemapping( 59 | uint256 indexed tokenId, address indexed oldChipAddress, address indexed newChipAddress 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /src/v2/PBTSimple.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.26; 3 | 4 | import "./IPBT.sol"; 5 | import "./ERC721ReadOnly.sol"; 6 | import "solady/utils/ECDSA.sol"; 7 | import "solady/utils/SignatureCheckerLib.sol"; 8 | 9 | /// @dev Implementation of PBT where all chipId->tokenIds are preset in the contract by the contract owner. 10 | contract PBTSimple is ERC721ReadOnly, IPBT { 11 | /// @dev Maximum duration for a signature to be valid since the timestamp 12 | /// used in the signature. 13 | uint256 public immutable maxDurationWindow; 14 | 15 | /// @dev A mapping of the `chipId` to the nonce to be used in its signature. 16 | mapping(address chipId => bytes32 nonce) public chipNonce; 17 | 18 | /// @dev Mapping of `tokenId` to `chipId`. 19 | /// The `chipId` is the public address of the chip's private key, and cannot be zero. 20 | mapping(address chipId => uint256 tokenId) internal _tokenIds; 21 | 22 | /// @dev Mapping of `chipId` to `tokenId`. 23 | /// If the `chipId` is the zero address, 24 | /// it means that there is no `chipId` paired to the `tokenId`. 25 | mapping(uint256 tokenId => address chipId) internal _chipIds; 26 | 27 | /// @dev The signature is invalid. 28 | error InvalidSignature(); 29 | 30 | /// @dev There is no `tokenId` paired to the `chipId`. 31 | error NoMappedTokenForChip(); 32 | 33 | /// @dev The signature timestamp is in the future. 34 | error SignatureTimestampInFuture(); 35 | 36 | /// @dev The signature timestamp has exceeded the max duration window. 37 | error SignatureTimestampTooOld(); 38 | 39 | /// @dev The `chipId` cannot be the zero address. 40 | error ChipIdIsZeroAddress(); 41 | 42 | constructor(string memory name, string memory symbol, uint256 maxDurationWindow_) 43 | ERC721ReadOnly(name, symbol) 44 | { 45 | maxDurationWindow = maxDurationWindow_; 46 | } 47 | 48 | /// @dev Transfers the `tokenId` assigned to `chipId` to `to`. 49 | function transferToken( 50 | address to, 51 | address chipId, 52 | bytes memory chipSignature, 53 | uint256 signatureTimestamp, 54 | bool useSafeTransfer, 55 | bytes memory extras 56 | ) public virtual returns (uint256 tokenId) { 57 | tokenId = tokenIdFor(chipId); 58 | _validateSigAndUpdateNonce(to, chipId, chipSignature, signatureTimestamp, extras); 59 | if (useSafeTransfer) { 60 | _safeTransfer(ownerOf(tokenId), to, tokenId, ""); 61 | } else { 62 | _transfer(ownerOf(tokenId), to, tokenId); 63 | } 64 | } 65 | 66 | /// @dev Returns if `signature` is indeed signed by the `chipId` assigned to `tokenId` for `data. 67 | function isChipSignatureForToken(uint256 tokenId, bytes memory data, bytes memory signature) 68 | public 69 | view 70 | returns (bool) 71 | { 72 | bytes32 hash = ECDSA.toEthSignedMessageHash(keccak256(data)); 73 | return SignatureCheckerLib.isValidSignatureNow(_chipIds[tokenId], hash, signature); 74 | } 75 | 76 | /// @dev Returns the `tokenId` paired to `chipId`. 77 | /// Reverts if there is no pair for `chipId`. 78 | function tokenIdFor(address chipId) public view returns (uint256 tokenId) { 79 | if (chipId == address(0)) revert ChipIdIsZeroAddress(); 80 | tokenId = _tokenIds[chipId]; 81 | if (_chipIds[tokenId] != chipId) revert NoMappedTokenForChip(); 82 | } 83 | 84 | /// @dev For ERC-165 85 | function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { 86 | return interfaceId == type(IPBT).interfaceId || super.supportsInterface(interfaceId); 87 | } 88 | 89 | /// @dev Mints to `to`, using `chipId`. 90 | function _mint(address to, address chipId, bytes memory chipSignature, uint256 signatureTimestamp, bytes memory extras) 91 | internal 92 | virtual 93 | returns (uint256 tokenId) 94 | { 95 | tokenId = _beforeMint(to, chipId, chipSignature, signatureTimestamp, extras); 96 | _mint(to, tokenId); // Reverts if `tokenId` already exists. 97 | } 98 | 99 | /// @dev Mints to `to`, using `chipId`. Performs a post transfer check. 100 | function _safeMint( 101 | address to, 102 | address chipId, 103 | bytes memory chipSignature, 104 | uint256 signatureTimestamp, 105 | bytes memory extras, 106 | bytes memory data 107 | ) internal virtual returns (uint256 tokenId) { 108 | tokenId = _beforeMint(to, chipId, chipSignature, signatureTimestamp, extras); 109 | _safeMint(to, tokenId, data); // Reverts if `tokenId` already exists. 110 | } 111 | 112 | /// @dev Called at the beginning of `_mint` and `_safeMint` for 113 | function _beforeMint(address to, address chipId, bytes memory chipSignature, uint256 signatureTimestamp, bytes memory extras) 114 | internal 115 | virtual 116 | returns (uint256 tokenId) 117 | { 118 | _validateSigAndUpdateNonce(to, chipId, chipSignature, signatureTimestamp, extras); 119 | // For PBT mints, we have to require that the `tokenId` has an assigned `chipId`. 120 | tokenId = _tokenIds[chipId]; 121 | if (_chipIds[tokenId] == address(0)) revert NoMappedTokenForChip(); 122 | } 123 | 124 | /// @dev Validates the `chipSignature` and update the nonce for the future signature of `chipId`. 125 | function _validateSigAndUpdateNonce( 126 | address to, 127 | address chipId, 128 | bytes memory chipSignature, 129 | uint256 signatureTimestamp, 130 | bytes memory extras 131 | ) internal virtual { 132 | bytes32 hash = _getSignatureHash(signatureTimestamp, chipId, to, extras); 133 | if (!SignatureCheckerLib.isValidSignatureNow(chipId, hash, chipSignature)) { 134 | revert InvalidSignature(); 135 | } 136 | chipNonce[chipId] = bytes32(uint256(hash) ^ uint256(blockhash(block.number - 1))); 137 | } 138 | 139 | /// @dev Returns the digest to be signed by the `chipId`. 140 | function _getSignatureHash(uint256 signatureTimestamp, address chipId, address to, bytes memory extras) 141 | internal 142 | virtual 143 | returns (bytes32) 144 | { 145 | if (signatureTimestamp > block.timestamp) revert SignatureTimestampInFuture(); 146 | if (signatureTimestamp + maxDurationWindow < block.timestamp) revert SignatureTimestampTooOld(); 147 | bytes32 hash = keccak256( 148 | abi.encode(address(this), block.chainid, chipNonce[chipId], to, signatureTimestamp, keccak256(extras)) 149 | ); 150 | return ECDSA.toEthSignedMessageHash(hash); 151 | } 152 | 153 | /// @dev Pairs `chipId` to `tokenId`. 154 | /// `tokenId` does not need to exist during pairing. 155 | /// Emits a {ChipSet} event. 156 | /// - To use it on a `chipId`, use `_setChip(tokenIdFor(chipId), newChipId)`. 157 | /// - Use this in a loop if you need. 158 | function _setChip(uint256 tokenId, address chipId) internal { 159 | if (chipId == address(0)) revert ChipIdIsZeroAddress(); 160 | _chipIds[tokenId] = chipId; 161 | _tokenIds[chipId] = tokenId; 162 | emit ChipSet(tokenId, chipId); 163 | } 164 | 165 | /// @dev Removes the pairing of `tokenId` to its `chipId`. 166 | /// - To use it on a `chipId`, use `_unsetChip(tokenIdFor(chipId))`. 167 | /// - Use this in a loop if you need. 168 | function _unsetChip(uint256 tokenId) internal { 169 | _chipIds[tokenId] = address(0); 170 | emit ChipSet(tokenId, address(0)); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/v1/PBTSimple.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "./IPBT.sol"; 5 | import "./ERC721ReadOnly.sol"; 6 | import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; 7 | 8 | error InvalidSignature(); 9 | error NoMintedTokenForChip(); 10 | error NoMappedTokenForChip(); 11 | error ArrayLengthMismatch(); 12 | error SeedingChipDataForExistingToken(); 13 | error UpdatingChipForUnsetChipMapping(); 14 | error InvalidBlockNumber(); 15 | error BlockNumberTooOld(); 16 | 17 | /** 18 | * Implementation of PBT where all chipAddress->tokenIds are preset in the contract by the contract owner. 19 | */ 20 | contract PBTSimple is ERC721ReadOnly, IPBT { 21 | using ECDSA for bytes32; 22 | 23 | struct TokenData { 24 | uint256 tokenId; 25 | address chipAddress; 26 | bool set; 27 | } 28 | 29 | /** 30 | * Mapping from chipAddress to TokenData 31 | */ 32 | mapping(address => TokenData) _tokenDatas; 33 | 34 | constructor(string memory name_, string memory symbol_) ERC721ReadOnly(name_, symbol_) {} 35 | 36 | // Should only be called for tokenIds that have not yet been minted 37 | // If the tokenId has already been minted, use _updateChips instead 38 | // TODO: consider preventing multiple chip addresses mapping to the same tokenId (store a tokenId->chip mapping) 39 | function _seedChipToTokenMapping(address[] memory chipAddresses, uint256[] memory tokenIds) 40 | internal 41 | { 42 | _seedChipToTokenMapping(chipAddresses, tokenIds, true); 43 | } 44 | 45 | function _seedChipToTokenMapping( 46 | address[] memory chipAddresses, 47 | uint256[] memory tokenIds, 48 | bool throwIfTokenAlreadyMinted 49 | ) internal { 50 | uint256 tokenIdsLength = tokenIds.length; 51 | if (tokenIdsLength != chipAddresses.length) { 52 | revert ArrayLengthMismatch(); 53 | } 54 | for (uint256 i = 0; i < tokenIdsLength; ++i) { 55 | address chipAddress = chipAddresses[i]; 56 | uint256 tokenId = tokenIds[i]; 57 | if (throwIfTokenAlreadyMinted && _exists(tokenId)) { 58 | revert SeedingChipDataForExistingToken(); 59 | } 60 | _tokenDatas[chipAddress] = TokenData(tokenId, chipAddress, true); 61 | } 62 | } 63 | 64 | // Should only be called for tokenIds that have been minted 65 | // If the tokenId hasn't been minted yet, use _seedChipToTokenMapping instead 66 | // Should only be used and called with care and rails to avoid a centralized entity swapping out valid chips. 67 | // TODO: consider preventing multiple chip addresses mapping to the same tokenId (store a tokenId->chip mapping) 68 | function _updateChips(address[] calldata chipAddressesOld, address[] calldata chipAddressesNew) 69 | internal 70 | { 71 | if (chipAddressesOld.length != chipAddressesNew.length) { 72 | revert ArrayLengthMismatch(); 73 | } 74 | for (uint256 i = 0; i < chipAddressesOld.length; ++i) { 75 | address oldChipAddress = chipAddressesOld[i]; 76 | TokenData memory oldTokenData = _tokenDatas[oldChipAddress]; 77 | if (!oldTokenData.set) { 78 | revert UpdatingChipForUnsetChipMapping(); 79 | } 80 | address newChipAddress = chipAddressesNew[i]; 81 | uint256 tokenId = oldTokenData.tokenId; 82 | _tokenDatas[newChipAddress] = TokenData(tokenId, newChipAddress, true); 83 | if (_exists(tokenId)) { 84 | emit PBTChipRemapping(tokenId, oldChipAddress, newChipAddress); 85 | } 86 | delete _tokenDatas[oldChipAddress]; 87 | } 88 | } 89 | 90 | function tokenIdFor(address chipAddress) external view override returns (uint256) { 91 | uint256 tokenId = tokenIdMappedFor(chipAddress); 92 | if (!_exists(tokenId)) { 93 | revert NoMintedTokenForChip(); 94 | } 95 | return tokenId; 96 | } 97 | 98 | function tokenIdMappedFor(address chipAddress) public view returns (uint256) { 99 | if (!_tokenDatas[chipAddress].set) { 100 | revert NoMappedTokenForChip(); 101 | } 102 | return _tokenDatas[chipAddress].tokenId; 103 | } 104 | 105 | // Returns true if the signer of the signature of the payload is the chip for the token id 106 | function isChipSignatureForToken(uint256 tokenId, bytes memory payload, bytes memory signature) 107 | public 108 | view 109 | override 110 | returns (bool) 111 | { 112 | if (!_exists(tokenId)) { 113 | revert NoMintedTokenForChip(); 114 | } 115 | bytes32 signedHash = keccak256(payload).toEthSignedMessageHash(); 116 | address chipAddr = signedHash.recover(signature); 117 | return _tokenDatas[chipAddr].set && _tokenDatas[chipAddr].tokenId == tokenId; 118 | } 119 | 120 | // 121 | // Parameters: 122 | // to: the address of the new owner 123 | // signatureFromChip: signature(receivingAddress + recentBlockhash), signed by an approved chip 124 | // 125 | // Contract should check that (1) recentBlockhash is a recent blockhash, (2) receivingAddress === to, and (3) the signing chip is allowlisted. 126 | function _mintTokenWithChip(bytes calldata signatureFromChip, uint256 blockNumberUsedInSig) 127 | internal 128 | returns (uint256) 129 | { 130 | TokenData memory tokenData = 131 | _getTokenDataForChipSignature(signatureFromChip, blockNumberUsedInSig); 132 | uint256 tokenId = tokenData.tokenId; 133 | _mint(_msgSender(), tokenId); 134 | emit PBTMint(tokenId, tokenData.chipAddress); 135 | return tokenId; 136 | } 137 | 138 | function transferTokenWithChip(bytes calldata signatureFromChip, uint256 blockNumberUsedInSig) 139 | public 140 | override 141 | { 142 | transferTokenWithChip(signatureFromChip, blockNumberUsedInSig, false); 143 | } 144 | 145 | function transferTokenWithChip( 146 | bytes calldata signatureFromChip, 147 | uint256 blockNumberUsedInSig, 148 | bool useSafeTransferFrom 149 | ) public override { 150 | _transferTokenWithChip(signatureFromChip, blockNumberUsedInSig, useSafeTransferFrom); 151 | } 152 | 153 | function _transferTokenWithChip( 154 | bytes calldata signatureFromChip, 155 | uint256 blockNumberUsedInSig, 156 | bool useSafeTransferFrom 157 | ) internal virtual { 158 | uint256 tokenId = 159 | _getTokenDataForChipSignature(signatureFromChip, blockNumberUsedInSig).tokenId; 160 | if (useSafeTransferFrom) { 161 | _safeTransfer(ownerOf(tokenId), _msgSender(), tokenId, ""); 162 | } else { 163 | _transfer(ownerOf(tokenId), _msgSender(), tokenId); 164 | } 165 | } 166 | 167 | function _getTokenDataForChipSignature( 168 | bytes calldata signatureFromChip, 169 | uint256 blockNumberUsedInSig 170 | ) internal view returns (TokenData memory) { 171 | // The blockNumberUsedInSig must be in a previous block because the blockhash of the current 172 | // block does not exist yet. 173 | if (block.number <= blockNumberUsedInSig) { 174 | revert InvalidBlockNumber(); 175 | } 176 | 177 | unchecked { 178 | if (block.number - blockNumberUsedInSig > getMaxBlockhashValidWindow()) { 179 | revert BlockNumberTooOld(); 180 | } 181 | } 182 | 183 | bytes32 blockHash = blockhash(blockNumberUsedInSig); 184 | bytes32 signedHash = 185 | keccak256(abi.encodePacked(_msgSender(), blockHash)).toEthSignedMessageHash(); 186 | address chipAddr = signedHash.recover(signatureFromChip); 187 | 188 | TokenData memory tokenData = _tokenDatas[chipAddr]; 189 | if (tokenData.set) { 190 | return tokenData; 191 | } 192 | revert InvalidSignature(); 193 | } 194 | 195 | function getMaxBlockhashValidWindow() public pure virtual returns (uint256) { 196 | return 100; 197 | } 198 | 199 | /** 200 | * @dev See {IERC165-supportsInterface}. 201 | */ 202 | function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { 203 | return interfaceId == type(IPBT).interfaceId || super.supportsInterface(interfaceId); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## PBT (Physical Backed Token) 2 | 3 | [![npm](https://img.shields.io/npm/v/@chiru-labs/pbt)](https://www.npmjs.com/package/@chiru-labs/pbt) 4 | 5 | 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: 6 | 7 | - 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. 8 | 9 | - Verification of authenticity of the physical item requires action from a trusted entity (e.g. StockX). 10 | 11 | PBT aims to mitigate these issues in a decentralized way through a new token standard (an extension of EIP-721). 12 | 13 | From the [Azuki](https://twitter.com/Azuki) team. 14 | **Chiru Labs is not liable for any outcomes as a result of using PBT**, DYOR. Repo still in beta. 15 | 16 | Note: the frontend library for chip signatures can be found [here](https://github.com/chiru-labs/pbt-chip-client). 17 | 18 | ## Import Into Your Project 19 | 20 | ### Hardhat 21 | 22 | ``` 23 | npm install @chiru-labs/pbt 24 | ``` 25 | 26 | ### Foundry 27 | 28 | ``` 29 | forge install https://github.com/chiru-labs/PBT 30 | ``` 31 | 32 | ## Resources 33 | 34 | - [pbt.io](https://www.pbt.io/) 35 | - [Draft EIP](https://eips.ethereum.org/EIPS/eip-5791) 36 | - [Blog](https://www.azuki.com/updates/pbt) 37 | 38 | ## How does PBT work? 39 | 40 | #### Requirements 41 | 42 | 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): 43 | 44 | - The ability to securely generate and store an ECDSA secp256k1 asymmetric keypair 45 | - The ability to sign messages from the private key of the asymmetric keypair 46 | - The ability for one to retrieve only the public key of the asymmetric keypair (private key non-extractable) 47 | 48 | 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`). 49 | 50 | #### Approach 51 | 52 | On a high level: 53 | 54 | - Each NFT is conceptually linked to a physical chip. 55 | - The NFT can only be transferred to a different owner if a signature from the chip is supplied to the contract. 56 | - This guarantees that a token cannot be transferred without consent from the owner of the physical item. 57 | 58 | More details available in the [EIP](https://eips.ethereum.org/EIPS/eip-5791) and inlined into `IPBT.sol`. 59 | 60 | #### v2 versus v1 61 | 62 | 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. 63 | 64 | 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. 65 | 66 | #### Reference Implementation 67 | 68 | A simple mint for a physical drop could look something like this: 69 | 70 | ```solidity 71 | import "@openzeppelin/contracts/access/Ownable.sol"; 72 | import "@chiru-labs/pbt/src/PBTSimple.sol"; 73 | 74 | contract Example is PBTSimple, Ownable { 75 | 76 | /// @notice Initialize a mapping from chipAddress to tokenId. 77 | /// @param chipAddresses The addresses derived from the public keys of the chips 78 | /// @param tokenIds The tokenIds to map to the addresses 79 | /// @param maxDurationWindow Maximum duration for a signature to be valid since the timestamp used in the signature. 80 | constructor( 81 | address[] memory chipAddresses, 82 | uint256[] memory tokenIds, 83 | uint256 maxDurationWindow 84 | ) 85 | PBTSimple("Example", "EXAMPLE", maxDurationWindow) 86 | { 87 | for (uint256 i = 0; i < chipAddresses.length; i++) { 88 | _setChip(tokenIds[i], chipAddresses[i]); 89 | } 90 | } 91 | 92 | /// @param to the address to which the PBT will be minted 93 | /// @param chipId the chip address being minted 94 | /// @param chipSignature the signature generated by the chip 95 | /// @param signatureTimestamp the timestamp used in the signature 96 | /// @param extras misc data, for extensions or custom logic in mint 97 | function mint( 98 | address to, 99 | address chipId, 100 | bytes memory chipSignature, 101 | uint256 signatureTimestamp, 102 | bytes memory extras 103 | ) external returns (uint256) { 104 | return _mint(to, chipId, chipSig, sigTimestamp, extras); 105 | } 106 | } 107 | ``` 108 | 109 | 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. 110 | 111 | ## How do I use PBT for my project? 112 | 113 | TODO: flesh this section out more 114 | 115 | 3 key parts. 116 | 117 | - Acquire chips, embed them into the physical items. 118 | - The Azuki hoodies used chips from [kongiscash](https://twitter.com/kongiscash). [Docs for chips](https://docs.arx.org/) 119 | - 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. 120 | - Note: when you scan a Kong chip, a system notification may popup, even when the scan is prompted from a browser action. To configure this notification's destination url, contact [cameron@arx.org](cameron@arx.org). Kong is currently working on (1) making these on-chain registrations decentralized and (2) making the system notification popup optional for future chips. 121 | - Write and deploy a PBT smart contract (use this repo). 122 | - Deployed examples: [Azuki Golden Skateboard](https://etherscan.io/address/0x6853449a65b264478a4cd90903a65f0508441ac0#code), [Azuki x Ambush Hoodie](https://etherscan.io/address/0xc20ae005e1340dab2449304158f999bfdd1aac1c#code). 123 | - The chip addresses also need to be seeded into the contract as an allowlist for which chips can mint and transfer 124 | - [Example txn](https://etherscan.io/tx/0x10bdd555a7addc650b1355d7606fd4d7b48bf990802f1235d874b598fa5cc0c5). 125 | - Set up a simple frontend to support minting/transferring the PBT. 126 | - [Azuki's UX flow for reference](https://twitter.com/0xElectrico/status/1599933852537225217). 127 | - The flow is initiated from desktop. The user is beamed to a mobile webapp through a QR code to do the scan. After the scan, the mobile webapp writes the signature to a server, and the desktop webapp polls the server to get the signature. User completes the mint on desktop. 128 | - 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. 129 | 130 | ## TODO 131 | 132 | - [ ] CI pipeline 133 | - [ ] PBT Locking extension (where transfers need to be approved by the current owner first) 134 | - [ ] PBT implementation that doesn't require seeding chip addresses to the contract pre-mint 135 | - how this would work: the mint function takes in a message that's signed by a blessed signer that the contract verifies 136 | 137 | Contributions welcome! 138 | 139 | ## Contributing 140 | 141 | Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. 142 | 143 | 1. Fork the project 144 | 2. Create your feature branch (`git checkout -b feature/AmazingFeature`) 145 | 3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) 146 | 4. Push to the branch (`git push origin feature/AmazingFeature`) 147 | 5. Open a pull request 148 | 149 | 150 | 151 | ## License 152 | 153 | Distributed under the MIT License. 154 | -------------------------------------------------------------------------------- /gas_report.txt: -------------------------------------------------------------------------------- 1 | No files changed, compilation skipped 2 | 3 | Ran 1 test for test/utils/SoladyTest.sol:SoladyTest 4 | [PASS] test__codesize() (gas: 1102) 5 | Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.83ms (495.71µs CPU time) 6 | 7 | Ran 1 test for test/utils/TestPlus.sol:TestPlus 8 | [PASS] test__codesize() (gas: 406) 9 | Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.81ms (381.29µs CPU time) 10 | 11 | Ran 5 tests for test/v2/PBTSimpleTest.sol:PBTSimpleTest 12 | [PASS] testAdvanceBlock(bytes32) (runs: 256, μ: 31391, ~: 31408) 13 | [PASS] testMintAndEverything(bytes32) (runs: 256, μ: 248453, ~: 292672) 14 | [PASS] testSetAndGetChip() (gas: 361565) 15 | [PASS] testSetAndGetChip(bytes32) (runs: 256, μ: 197758, ~: 185631) 16 | [PASS] test__codesize() (gas: 21150) 17 | Suite result: ok. 5 passed; 0 failed; 0 skipped; finished in 157.29ms (203.89ms CPU time) 18 | 19 | Ran 16 tests for test/v1/PBTSimpleTest.sol:PBTSimpleTest 20 | [PASS] testGetTokenDataForChipSignature() (gas: 150319) 21 | [PASS] testGetTokenDataForChipSignatureBlockNumTooOld() (gas: 145681) 22 | [PASS] testGetTokenDataForChipSignatureInvalid() (gas: 154943) 23 | [PASS] testGetTokenDataForChipSignatureInvalidBlockNumber() (gas: 145520) 24 | [PASS] testIsChipSignatureForToken() (gas: 286730) 25 | [PASS] testMintTokenWithChip() (gas: 227486) 26 | [PASS] testSeedChipToTokenMapping() (gas: 137831) 27 | [PASS] testSeedChipToTokenMappingExistingToken() (gas: 302573) 28 | [PASS] testSeedChipToTokenMappingInvalidInput() (gas: 39711) 29 | [PASS] testSupportsInterface() (gas: 6962) 30 | [PASS] testTokenIdFor() (gas: 164321) 31 | [PASS] testTokenIdMappedFor() (gas: 89521) 32 | [PASS] testTransferTokenWithChip(bool) (runs: 256, μ: 332865, ~: 332712) 33 | [PASS] testUpdateChips() (gas: 249262) 34 | [PASS] testUpdateChipsInvalidInput() (gas: 39071) 35 | [PASS] testUpdateChipsUnsetChip() (gas: 46416) 36 | Suite result: ok. 16 passed; 0 failed; 0 skipped; finished in 163.40ms (102.25ms CPU time) 37 | 38 | Ran 7 tests for test/v1/PBTRandomTest.sol:PBTRandomTest 39 | [PASS] testGetTokenDataForChipSignature() (gas: 261689) 40 | [PASS] testGetTokenDataForChipSignatureInvalid() (gas: 271101) 41 | [PASS] testIsChipSignatureForToken() (gas: 267043) 42 | [PASS] testSupportsInterface() (gas: 6963) 43 | [PASS] testTokenIdFor() (gas: 205466) 44 | [PASS] testTransferTokenWithChip(bool) (runs: 256, μ: 323670, ~: 323526) 45 | [PASS] testUpdateChips() (gas: 514145) 46 | Suite result: ok. 7 passed; 0 failed; 0 skipped; finished in 163.44ms (162.09ms CPU time) 47 | 48 | Ran 5 tests for test/v1/ERC721ReadOnlyTest.sol:ERC721ReadOnlyTest 49 | [PASS] testApprove() (gas: 32231) 50 | [PASS] testGetApproved() (gas: 16203) 51 | [PASS] testIsApprovedForAll() (gas: 10045) 52 | [PASS] testSetApprovalForAll() (gas: 32268) 53 | [PASS] testTransferFunctions() (gas: 85044) 54 | Suite result: ok. 5 passed; 0 failed; 0 skipped; finished in 163.42ms (486.46µs CPU time) 55 | | src/v1/mocks/ERC721ReadOnlyMock.sol:ERC721ReadOnlyMock contract | | | | | | 56 | |-----------------------------------------------------------------|-----------------|-------|--------|-------|---------| 57 | | Deployment Cost | Deployment Size | | | | | 58 | | 874309 | 4463 | | | | | 59 | | Function Name | min | avg | median | max | # calls | 60 | | approve | 22044 | 22044 | 22044 | 22044 | 1 | 61 | | getApproved | 2581 | 2583 | 2583 | 2586 | 2 | 62 | | isApprovedForAll | 545 | 545 | 545 | 545 | 1 | 63 | | mint | 68743 | 68743 | 68743 | 68743 | 5 | 64 | | safeTransferFrom(address,address,uint256) | 22583 | 22583 | 22583 | 22583 | 1 | 65 | | safeTransferFrom(address,address,uint256,bytes) | 23114 | 23114 | 23114 | 23114 | 1 | 66 | | setApprovalForAll | 22082 | 22082 | 22082 | 22082 | 1 | 67 | | transferFrom | 22539 | 22539 | 22539 | 22539 | 1 | 68 | 69 | 70 | | src/v1/mocks/PBTRandomMock.sol:PBTRandomMock contract | | | | | | 71 | |-------------------------------------------------------|-----------------|--------|--------|--------|---------| 72 | | Deployment Cost | Deployment Size | | | | | 73 | | 2137463 | 10294 | | | | | 74 | | Function Name | min | avg | median | max | # calls | 75 | | getTokenData | 1020 | 1020 | 1020 | 1020 | 4 | 76 | | getTokenDataForChipSignature | 970 | 3388 | 3371 | 5840 | 4 | 77 | | isChipSignatureForToken | 3299 | 4523 | 4523 | 5748 | 2 | 78 | | mintTokenWithChip | 134282 | 134282 | 134282 | 134294 | 262 | 79 | | ownerOf | 624 | 624 | 624 | 624 | 512 | 80 | | seedChipAddresses | 47151 | 96996 | 97285 | 97285 | 261 | 81 | | supportsInterface | 458 | 510 | 510 | 563 | 2 | 82 | | tokenIdFor | 830 | 1726 | 1726 | 2622 | 2 | 83 | | transferTokenWithChip | 65258 | 65402 | 65258 | 65567 | 256 | 84 | | updateChips | 26441 | 72919 | 72919 | 119398 | 2 | 85 | 86 | 87 | | src/v1/mocks/PBTSimpleMock.sol:PBTSimpleMock contract | | | | | | 88 | |-------------------------------------------------------|-----------------|--------|--------|--------|---------| 89 | | Deployment Cost | Deployment Size | | | | | 90 | | 2035062 | 9856 | | | | | 91 | | Function Name | min | avg | median | max | # calls | 92 | | balanceOf | 657 | 657 | 657 | 657 | 513 | 93 | | getTokenData | 1020 | 1020 | 1020 | 1020 | 6 | 94 | | getTokenDataForChipSignature | 800 | 4242 | 3292 | 9586 | 4 | 95 | | isChipSignatureForToken | 5774 | 5774 | 5774 | 5774 | 1 | 96 | | mint | 68747 | 68747 | 68747 | 68747 | 517 | 97 | | mintTokenWithChip | 80629 | 80629 | 80629 | 80629 | 1 | 98 | | seedChipToTokenMapping | 24288 | 117231 | 118291 | 118291 | 269 | 99 | | supportsInterface | 458 | 510 | 510 | 563 | 2 | 100 | | tokenIdFor | 1056 | 1592 | 1082 | 2640 | 3 | 101 | | tokenIdMappedFor | 830 | 1723 | 1723 | 2616 | 2 | 102 | | transferTokenWithChip | 48042 | 48195 | 48042 | 48351 | 256 | 103 | | updateChips | 23542 | 57397 | 28616 | 120033 | 3 | 104 | 105 | 106 | | src/v2/mocks/PBTSimpleMock.sol:PBTSimpleMock contract | | | | | | 107 | |-------------------------------------------------------|-----------------|-------|--------|--------|---------| 108 | | Deployment Cost | Deployment Size | | | | | 109 | | 1634044 | 8061 | | | | | 110 | | Function Name | min | avg | median | max | # calls | 111 | | chipNonce | 585 | 1269 | 585 | 2585 | 748 | 112 | | isChipSignatureForToken | 1508 | 3303 | 3544 | 5192 | 79 | 113 | | mint | 24897 | 86321 | 102722 | 119026 | 256 | 114 | | ownerOf | 581 | 581 | 581 | 581 | 318 | 115 | | setChip | 30726 | 65186 | 67738 | 68338 | 747 | 116 | | tokenIdFor | 449 | 1201 | 872 | 4848 | 2327 | 117 | | transferToken | 71458 | 74324 | 73668 | 90586 | 144 | 118 | | unsetChip | 23341 | 23409 | 23353 | 23725 | 306 | 119 | 120 | 121 | 122 | 123 | Ran 6 test suites in 229.57ms (655.19ms CPU time): 35 tests passed, 0 failed, 0 skipped (35 total tests) 124 | -------------------------------------------------------------------------------- /test/utils/Brutalizer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.4; 3 | 4 | /// @dev WARNING! This mock is strictly intended for testing purposes only. 5 | /// Do NOT copy anything here into production code unless you really know what you are doing. 6 | contract Brutalizer { 7 | /// @dev Fills the memory with junk, for more robust testing of inline assembly 8 | /// which reads/write to the memory. 9 | function _brutalizeMemory() internal view { 10 | // To prevent a solidity 0.8.13 bug. 11 | // See: https://blog.soliditylang.org/2022/06/15/inline-assembly-memory-side-effects-bug 12 | // Basically, we need to access a solidity variable from the assembly to 13 | // tell the compiler that this assembly block is not in isolation. 14 | uint256 zero; 15 | /// @solidity memory-safe-assembly 16 | assembly { 17 | let offset := mload(0x40) // Start the offset at the free memory pointer. 18 | calldatacopy(offset, zero, calldatasize()) 19 | 20 | // Fill the 64 bytes of scratch space with garbage. 21 | mstore(zero, add(caller(), gas())) 22 | mstore(0x20, keccak256(offset, calldatasize())) 23 | mstore(zero, keccak256(zero, 0x40)) 24 | 25 | let r0 := mload(zero) 26 | let r1 := mload(0x20) 27 | 28 | let cSize := add(codesize(), iszero(codesize())) 29 | if iszero(lt(cSize, 32)) { cSize := sub(cSize, and(mload(0x02), 0x1f)) } 30 | let start := mod(mload(0x10), cSize) 31 | let size := mul(sub(cSize, start), gt(cSize, start)) 32 | let times := div(0x7ffff, cSize) 33 | if iszero(lt(times, 128)) { times := 128 } 34 | 35 | // Occasionally offset the offset by a pseudorandom large amount. 36 | // Can't be too large, or we will easily get out-of-gas errors. 37 | offset := add(offset, mul(iszero(and(r1, 0xf)), and(r0, 0xfffff))) 38 | 39 | // Fill the free memory with garbage. 40 | // prettier-ignore 41 | for { let w := not(0) } 1 {} { 42 | mstore(offset, r0) 43 | mstore(add(offset, 0x20), r1) 44 | offset := add(offset, 0x40) 45 | // We use codecopy instead of the identity precompile 46 | // to avoid polluting the `forge test -vvvv` output with tons of junk. 47 | codecopy(offset, start, size) 48 | codecopy(add(offset, size), 0, start) 49 | offset := add(offset, cSize) 50 | times := add(times, w) // `sub(times, 1)`. 51 | if iszero(times) { break } 52 | } 53 | } 54 | } 55 | 56 | /// @dev Fills the scratch space with junk, for more robust testing of inline assembly 57 | /// which reads/write to the memory. 58 | function _brutalizeScratchSpace() internal view { 59 | // To prevent a solidity 0.8.13 bug. 60 | // See: https://blog.soliditylang.org/2022/06/15/inline-assembly-memory-side-effects-bug 61 | // Basically, we need to access a solidity variable from the assembly to 62 | // tell the compiler that this assembly block is not in isolation. 63 | uint256 zero; 64 | /// @solidity memory-safe-assembly 65 | assembly { 66 | let offset := mload(0x40) // Start the offset at the free memory pointer. 67 | calldatacopy(offset, zero, calldatasize()) 68 | 69 | // Fill the 64 bytes of scratch space with garbage. 70 | mstore(zero, add(caller(), gas())) 71 | mstore(0x20, keccak256(offset, calldatasize())) 72 | mstore(zero, keccak256(zero, 0x40)) 73 | } 74 | } 75 | 76 | /// @dev Fills the lower memory with junk, for more robust testing of inline assembly 77 | /// which reads/write to the memory. 78 | /// For efficiency, this only fills a small portion of the free memory. 79 | function _brutalizeLowerMemory() internal view { 80 | // To prevent a solidity 0.8.13 bug. 81 | // See: https://blog.soliditylang.org/2022/06/15/inline-assembly-memory-side-effects-bug 82 | // Basically, we need to access a solidity variable from the assembly to 83 | // tell the compiler that this assembly block is not in isolation. 84 | uint256 zero; 85 | /// @solidity memory-safe-assembly 86 | assembly { 87 | let offset := mload(0x40) // Start the offset at the free memory pointer. 88 | calldatacopy(offset, zero, calldatasize()) 89 | 90 | // Fill the 64 bytes of scratch space with garbage. 91 | mstore(zero, add(caller(), gas())) 92 | mstore(0x20, keccak256(offset, calldatasize())) 93 | mstore(zero, keccak256(zero, 0x40)) 94 | 95 | for { let r := keccak256(0x10, 0x20) } 1 {} { 96 | if iszero(and(7, r)) { 97 | let x := keccak256(zero, 0x40) 98 | mstore(offset, x) 99 | mstore(add(0x20, offset), x) 100 | mstore(add(0x40, offset), x) 101 | mstore(add(0x60, offset), x) 102 | mstore(add(0x80, offset), x) 103 | mstore(add(0xa0, offset), x) 104 | mstore(add(0xc0, offset), x) 105 | mstore(add(0xe0, offset), x) 106 | mstore(add(0x100, offset), x) 107 | mstore(add(0x120, offset), x) 108 | mstore(add(0x140, offset), x) 109 | mstore(add(0x160, offset), x) 110 | mstore(add(0x180, offset), x) 111 | mstore(add(0x1a0, offset), x) 112 | mstore(add(0x1c0, offset), x) 113 | mstore(add(0x1e0, offset), x) 114 | mstore(add(0x200, offset), x) 115 | mstore(add(0x220, offset), x) 116 | mstore(add(0x240, offset), x) 117 | mstore(add(0x260, offset), x) 118 | break 119 | } 120 | codecopy(offset, byte(0, r), codesize()) 121 | break 122 | } 123 | } 124 | } 125 | 126 | /// @dev Fills the memory with junk, for more robust testing of inline assembly 127 | /// which reads/write to the memory. 128 | modifier brutalizeMemory() { 129 | _brutalizeMemory(); 130 | _; 131 | _checkMemory(); 132 | } 133 | 134 | /// @dev Fills the scratch space with junk, for more robust testing of inline assembly 135 | /// which reads/write to the memory. 136 | modifier brutalizeScratchSpace() { 137 | _brutalizeScratchSpace(); 138 | _; 139 | _checkMemory(); 140 | } 141 | 142 | /// @dev Fills the lower memory with junk, for more robust testing of inline assembly 143 | /// which reads/write to the memory. 144 | modifier brutalizeLowerMemory() { 145 | _brutalizeLowerMemory(); 146 | _; 147 | _checkMemory(); 148 | } 149 | 150 | /// @dev Returns the result with the upper bits dirtied. 151 | function _brutalized(address value) internal pure returns (address result) { 152 | /// @solidity memory-safe-assembly 153 | assembly { 154 | mstore(0x00, xor(add(shl(32, value), calldataload(0x00)), mload(0x10))) 155 | mstore(0x20, calldataload(0x04)) 156 | mstore(0x10, keccak256(0x00, 0x60)) 157 | result := or(shl(160, mload(0x10)), value) 158 | } 159 | } 160 | 161 | /// @dev Returns the result with the upper bits dirtied. 162 | function _brutalized(uint96 value) internal pure returns (uint96 result) { 163 | /// @solidity memory-safe-assembly 164 | assembly { 165 | mstore(0x00, xor(add(shl(32, value), calldataload(0x00)), mload(0x10))) 166 | mstore(0x20, calldataload(0x04)) 167 | mstore(0x10, keccak256(0x00, 0x60)) 168 | result := or(shl(96, mload(0x10)), value) 169 | } 170 | } 171 | 172 | /// @dev Returns the result with the upper bits dirtied. 173 | function _brutalized(bool value) internal pure returns (bool result) { 174 | /// @solidity memory-safe-assembly 175 | assembly { 176 | mstore(0x00, xor(add(shl(32, value), calldataload(0x00)), mload(0x10))) 177 | mstore(0x20, calldataload(0x04)) 178 | mstore(0x10, keccak256(0x00, 0x60)) 179 | result := mul(iszero(iszero(value)), mload(0x10)) 180 | } 181 | } 182 | 183 | /// @dev Misaligns the free memory pointer. 184 | /// The free memory pointer has a 1/32 chance to be aligned. 185 | function _misalignFreeMemoryPointer() internal pure { 186 | uint256 twoWords = 0x40; 187 | /// @solidity memory-safe-assembly 188 | assembly { 189 | let m := mload(twoWords) 190 | m := add(m, mul(and(keccak256(0x00, twoWords), 0x1f), iszero(and(m, 0x1f)))) 191 | mstore(twoWords, m) 192 | } 193 | } 194 | 195 | /// @dev Check if the free memory pointer and the zero slot are not contaminated. 196 | /// Useful for cases where these slots are used for temporary storage. 197 | function _checkMemory() internal pure { 198 | bool zeroSlotIsNotZero; 199 | bool freeMemoryPointerOverflowed; 200 | /// @solidity memory-safe-assembly 201 | assembly { 202 | // Write ones to the free memory, to make subsequent checks fail if 203 | // insufficient memory is allocated. 204 | mstore(mload(0x40), not(0)) 205 | // Test at a lower, but reasonable limit for more safety room. 206 | if gt(mload(0x40), 0xffffffff) { freeMemoryPointerOverflowed := 1 } 207 | // Check the value of the zero slot. 208 | zeroSlotIsNotZero := mload(0x60) 209 | } 210 | if (freeMemoryPointerOverflowed) revert("`0x40` overflowed!"); 211 | if (zeroSlotIsNotZero) revert("`0x60` is not zero!"); 212 | } 213 | 214 | /// @dev Check if `s`: 215 | /// - Has sufficient memory allocated. 216 | /// - Is zero right padded (cuz some frontends like Etherscan has issues 217 | /// with decoding non-zero-right-padded strings). 218 | function _checkMemory(bytes memory s) internal pure { 219 | bool notZeroRightPadded; 220 | bool insufficientMalloc; 221 | /// @solidity memory-safe-assembly 222 | assembly { 223 | // Write ones to the free memory, to make subsequent checks fail if 224 | // insufficient memory is allocated. 225 | mstore(mload(0x40), not(0)) 226 | let length := mload(s) 227 | let lastWord := mload(add(add(s, 0x20), and(length, not(0x1f)))) 228 | let remainder := and(length, 0x1f) 229 | if remainder { if shl(mul(8, remainder), lastWord) { notZeroRightPadded := 1 } } 230 | // Check if the memory allocated is sufficient. 231 | if length { if gt(add(add(s, 0x20), length), mload(0x40)) { insufficientMalloc := 1 } } 232 | } 233 | if (notZeroRightPadded) revert("Not zero right padded!"); 234 | if (insufficientMalloc) revert("Insufficient memory allocation!"); 235 | _checkMemory(); 236 | } 237 | 238 | /// @dev For checking the memory allocation for string `s`. 239 | function _checkMemory(string memory s) internal pure { 240 | _checkMemory(bytes(s)); 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /test/v2/PBTSimpleTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "../utils/SoladyTest.sol"; 5 | import "solady/utils/LibBitmap.sol"; 6 | import "solady/utils/ECDSA.sol"; 7 | import "../../src/v2/mocks/PBTSimpleMock.sol"; 8 | 9 | contract PBTSimpleTest is SoladyTest { 10 | using LibBitmap for *; 11 | LibBitmap.Bitmap chipIdHasAssignment; 12 | 13 | PBTSimpleMock pbt; 14 | 15 | uint256 internal constant _MAX_DURATION_WINDOW = 1000; 16 | 17 | function setUp() public { 18 | pbt = new PBTSimpleMock("PBTSimple", "PBTS", _MAX_DURATION_WINDOW); 19 | } 20 | 21 | function testSetAndGetChip() public { 22 | for (uint i; i < 3; ++i) { 23 | uint tokenId = i << 64; 24 | address chipId = address(uint160((i + 1) << 128)); 25 | pbt.setChip(tokenId, chipId); 26 | } 27 | for (uint i; i < 3; ++i) { 28 | uint tokenId = i << 64; 29 | address chipId = address(uint160((i + 1) << 128)); 30 | assertEq(pbt.tokenIdFor(chipId), tokenId); 31 | } 32 | for (uint i; i < 3; ++i) { 33 | uint tokenId = i << 64; 34 | address oldChipId = address(uint160((i + 1) << 128)); 35 | address newChipId = address(uint160((i + 1) << 32)); 36 | pbt.setChip(pbt.tokenIdFor(oldChipId), newChipId); 37 | assertEq(pbt.tokenIdFor(newChipId), tokenId); 38 | vm.expectRevert(PBTSimple.NoMappedTokenForChip.selector); 39 | pbt.tokenIdFor(oldChipId); 40 | } 41 | for (uint i; i < 3; ++i) { 42 | uint tokenId = i << 64; 43 | address oldChipId = address(uint160((i + 1) << 128)); 44 | address newChipId = address(uint160((i + 1) << 32)); 45 | assertEq(pbt.tokenIdFor(newChipId), tokenId); 46 | vm.expectRevert(PBTSimple.NoMappedTokenForChip.selector); 47 | pbt.tokenIdFor(oldChipId); 48 | } 49 | } 50 | 51 | function testSetAndGetChip(bytes32) public { 52 | uint tokenId0 = _random() & 7; 53 | address chipId0 = address(uint160( (_random() & 7) + 1 )); 54 | pbt.setChip(tokenId0, chipId0); 55 | assertEq(pbt.tokenIdFor(chipId0), tokenId0); 56 | 57 | uint tokenId1 = _random() & 7; 58 | address chipId1 = address(uint160( (_random() & 7) + 1 )); 59 | pbt.setChip(tokenId1, chipId1); 60 | assertEq(pbt.tokenIdFor(chipId1), tokenId1); 61 | 62 | vm.expectRevert(); 63 | pbt.tokenIdFor(address(0)); 64 | 65 | if (chipId0 == chipId1 && tokenId0 != tokenId1) { 66 | assertEq(pbt.tokenIdFor(chipId1), tokenId1); 67 | address chipId2 = _sampleNotEq(chipId1); 68 | vm.expectRevert(PBTSimple.NoMappedTokenForChip.selector); 69 | pbt.tokenIdFor(address(chipId2)); 70 | if (_randomChance(2)) { 71 | pbt.unsetChip(tokenId1); 72 | vm.expectRevert(PBTSimple.NoMappedTokenForChip.selector); 73 | pbt.tokenIdFor(chipId1); 74 | return; 75 | } 76 | } 77 | if (chipId0 != chipId1 && tokenId0 != tokenId1) { 78 | assertEq(pbt.tokenIdFor(chipId0), tokenId0); 79 | assertEq(pbt.tokenIdFor(chipId1), tokenId1); 80 | address chipId2 = _sampleNotEq(chipId0, chipId1); 81 | vm.expectRevert(PBTSimple.NoMappedTokenForChip.selector); 82 | pbt.tokenIdFor(address(chipId2)); 83 | if (_randomChance(2)) { 84 | pbt.unsetChip(tokenId1); 85 | vm.expectRevert(PBTSimple.NoMappedTokenForChip.selector); 86 | pbt.tokenIdFor(chipId1); 87 | assertEq(pbt.tokenIdFor(chipId0), tokenId0); 88 | pbt.unsetChip(tokenId0); 89 | vm.expectRevert(PBTSimple.NoMappedTokenForChip.selector); 90 | pbt.tokenIdFor(chipId0); 91 | vm.expectRevert(PBTSimple.NoMappedTokenForChip.selector); 92 | pbt.tokenIdFor(chipId1); 93 | return; 94 | } 95 | } 96 | if (chipId0 == chipId1 && tokenId0 == tokenId1) { 97 | assertEq(pbt.tokenIdFor(chipId0), tokenId0); 98 | assertEq(pbt.tokenIdFor(chipId1), tokenId1); 99 | address chipId2 = _sampleNotEq(chipId0, chipId1); 100 | vm.expectRevert(PBTSimple.NoMappedTokenForChip.selector); 101 | pbt.tokenIdFor(address(chipId2)); 102 | if (_randomChance(2)) { 103 | pbt.unsetChip(tokenId1); 104 | vm.expectRevert(PBTSimple.NoMappedTokenForChip.selector); 105 | pbt.tokenIdFor(chipId1); 106 | return; 107 | } 108 | } 109 | if (chipId0 != chipId1 && tokenId0 == tokenId1) { 110 | assertEq(pbt.tokenIdFor(chipId1), tokenId1); 111 | address chipId2 = _sampleNotEq(chipId1); 112 | vm.expectRevert(PBTSimple.NoMappedTokenForChip.selector); 113 | pbt.tokenIdFor(address(chipId2)); 114 | if (_randomChance(2)) { 115 | pbt.unsetChip(tokenId1); 116 | vm.expectRevert(PBTSimple.NoMappedTokenForChip.selector); 117 | pbt.tokenIdFor(chipId1); 118 | vm.expectRevert(PBTSimple.NoMappedTokenForChip.selector); 119 | pbt.tokenIdFor(chipId0); 120 | return; 121 | } 122 | } 123 | } 124 | 125 | function _sampleNotEq(address a) internal returns (address c) { 126 | while (true) { 127 | c = address(uint160( (_random() & 7) + 1 )); 128 | if (c != a) break; 129 | } 130 | } 131 | 132 | function _sampleNotEq(address a, address b) internal returns (address c) { 133 | while (true) { 134 | c = address(uint160( (_random() & 7) + 1 )); 135 | if (c != a && c != b) break; 136 | } 137 | } 138 | 139 | function testAdvanceBlock(bytes32) public { 140 | _mine(); 141 | bytes32 blockHash0 = blockhash(block.number - 1); 142 | emit LogBytes32(blockHash0); 143 | assert(blockHash0 != bytes32(0)); 144 | _mine(); 145 | bytes32 blockHash1 = blockhash(block.number - 1); 146 | emit LogBytes32(blockHash1); 147 | assert(blockHash1 != bytes32(0)); 148 | assert(blockHash0 != blockHash1); 149 | } 150 | 151 | function _mine() internal { 152 | unchecked { 153 | vm.warp(_bound(_random(), _MAX_DURATION_WINDOW * 2, _MAX_DURATION_WINDOW * 10)); 154 | vm.roll(block.number + (_random() & 7) + 1); 155 | } 156 | } 157 | 158 | struct _TestTemps { 159 | uint tokenId; 160 | address chipId; 161 | uint chipPrivateKey; 162 | address to; 163 | uint sigTimestamp; 164 | bytes extras; 165 | bytes chipSig; 166 | bytes data; 167 | uint warppedTimestamp; 168 | } 169 | 170 | function _testTemps() internal returns (_TestTemps memory t) { 171 | _mine(); 172 | t.tokenId = _random(); 173 | (t.chipId, t.chipPrivateKey) = _randomSigner(); 174 | 175 | t.to = _randomNonZeroAddress(); 176 | t.sigTimestamp = block.timestamp - _bound(_random(), 0, _MAX_DURATION_WINDOW); 177 | if (_randomChance(2)) { 178 | t.extras = _randomBytes(); 179 | } 180 | t.chipSig = _genSig(t); 181 | } 182 | 183 | function testMintAndEverything(bytes32) public { 184 | _TestTemps memory t = _testTemps(); 185 | 186 | if (_randomChance(8)) { 187 | t.data = _randomBytes(); 188 | assertFalse(pbt.isChipSignatureForToken(t.tokenId, t.data, _genSig(t, t.data))); 189 | } 190 | 191 | if (_randomChance(8)) { 192 | vm.expectRevert(PBTSimple.NoMappedTokenForChip.selector); 193 | pbt.mint(t.to, t.chipId, t.chipSig, t.sigTimestamp, t.extras); 194 | return; 195 | } 196 | pbt.setChip(t.tokenId, t.chipId); 197 | 198 | if (_randomChance(8)) { 199 | pbt.unsetChip(t.tokenId); 200 | vm.expectRevert(PBTSimple.NoMappedTokenForChip.selector); 201 | pbt.mint(t.to, t.chipId, t.chipSig, t.sigTimestamp, t.extras); 202 | return; 203 | } 204 | 205 | if (_randomChance(8)) { 206 | t.warppedTimestamp = _bound(_random(), 0, block.timestamp + _MAX_DURATION_WINDOW * 2); 207 | vm.warp(t.warppedTimestamp); 208 | if (t.warppedTimestamp < t.sigTimestamp) { 209 | vm.expectRevert(PBTSimple.SignatureTimestampInFuture.selector); 210 | pbt.mint(t.to, t.chipId, t.chipSig, t.sigTimestamp, t.extras); 211 | return; 212 | } 213 | if (t.warppedTimestamp >= t.sigTimestamp + _MAX_DURATION_WINDOW) { 214 | vm.expectRevert(PBTSimple.SignatureTimestampTooOld.selector); 215 | pbt.mint(t.to, t.chipId, t.chipSig, t.sigTimestamp, t.extras); 216 | return; 217 | } 218 | } 219 | 220 | assertEq(pbt.tokenIdFor(t.chipId), t.tokenId); 221 | assertEq(pbt.chipNonce(t.chipId), bytes32(0)); 222 | assertEq(pbt.mint(t.to, t.chipId, t.chipSig, t.sigTimestamp, t.extras), t.tokenId); 223 | assertEq(pbt.ownerOf(t.tokenId), t.to); 224 | assert(pbt.chipNonce(t.chipId) != bytes32(0)); 225 | assertEq(pbt.tokenIdFor(t.chipId), t.tokenId); 226 | 227 | if (_randomChance(8)) { 228 | t.data = _randomBytes(); 229 | bytes memory sig = _genSig(t, t.data); 230 | assertTrue(pbt.isChipSignatureForToken(t.tokenId, t.data, sig)); 231 | pbt.unsetChip(t.tokenId); 232 | assertFalse(pbt.isChipSignatureForToken(t.tokenId, t.data, sig)); 233 | return; 234 | } 235 | 236 | t.to = _randomNonZeroAddress(); 237 | t.chipSig = _genSig(t); 238 | pbt.transferToken(t.to, t.chipId, t.chipSig, t.sigTimestamp, _randomChance(2), t.extras); 239 | assertEq(pbt.ownerOf(t.tokenId), t.to); 240 | } 241 | 242 | function _genSig(_TestTemps memory t, bytes memory data) internal returns (bytes memory) { 243 | bytes32 hash = ECDSA.toEthSignedMessageHash(keccak256(data)); 244 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(t.chipPrivateKey, hash); 245 | return abi.encodePacked(r, s, v); 246 | } 247 | 248 | function _genSig(_TestTemps memory t) internal returns (bytes memory) { 249 | return _genSig(t, abi.encode(address(pbt), block.chainid, pbt.chipNonce(t.chipId), t.to, t.sigTimestamp, keccak256(t.extras))); 250 | } 251 | 252 | function _randomBytes() internal returns (bytes memory result) { 253 | uint256 r = _random(); 254 | uint256 n = r & 0x3ff; 255 | /// @solidity memory-safe-assembly 256 | assembly { 257 | result := mload(0x40) 258 | mstore(0x00, r) 259 | let t := keccak256(0x00, 0x20) 260 | if gt(byte(0, r), 16) { n := and(r, 0x7f) } 261 | codecopy(add(result, 0x20), byte(0, t), codesize()) 262 | codecopy(add(result, n), byte(1, t), codesize()) 263 | mstore(0x40, add(n, add(0x40, result))) 264 | mstore(result, n) 265 | if iszero(byte(3, t)) { result := 0x60 } 266 | } 267 | } 268 | 269 | function _truncateBytes(bytes memory b, uint256 n) 270 | internal 271 | pure 272 | returns (bytes memory result) 273 | { 274 | /// @solidity memory-safe-assembly 275 | assembly { 276 | if gt(mload(b), n) { mstore(b, n) } 277 | result := b 278 | } 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /src/v1/PBTRandom.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "./IPBT.sol"; 5 | import "./ERC721ReadOnly.sol"; 6 | import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; 7 | 8 | error InvalidSignature(); 9 | error InvalidChipAddress(); 10 | error NoMintedTokenForChip(); 11 | error ArrayLengthMismatch(); 12 | error ChipAlreadyLinkedToMintedToken(); 13 | error UpdatingChipForUnsetChipMapping(); 14 | error NoMoreTokenIds(); 15 | error InvalidBlockNumber(); 16 | error BlockNumberTooOld(); 17 | 18 | /** 19 | * Implementation of PBT where all tokenIds are randomly chosen at mint time. 20 | */ 21 | contract PBTRandom is ERC721ReadOnly, IPBT { 22 | using ECDSA for bytes32; 23 | 24 | struct TokenData { 25 | uint256 tokenId; 26 | address chipAddress; 27 | bool set; 28 | } 29 | 30 | // Mapping from chipAddress to TokenData 31 | mapping(address => TokenData) _tokenDatas; 32 | 33 | // Max token supply 34 | uint256 public immutable maxSupply; 35 | uint256 private _numAvailableRemainingTokens; 36 | 37 | // Data structure used for Fisher Yates shuffle 38 | mapping(uint256 => uint256) internal _availableRemainingTokens; 39 | 40 | constructor(string memory name_, string memory symbol_, uint256 maxSupply_) 41 | ERC721ReadOnly(name_, symbol_) 42 | { 43 | maxSupply = maxSupply_; 44 | _numAvailableRemainingTokens = maxSupply_; 45 | } 46 | 47 | function _seedChipAddresses(address[] memory chipAddresses) internal { 48 | for (uint256 i = 0; i < chipAddresses.length; ++i) { 49 | address chipAddress = chipAddresses[i]; 50 | _tokenDatas[chipAddress] = TokenData(0, chipAddress, false); 51 | } 52 | } 53 | 54 | // TODO: consider preventing multiple chip addresses mapping to the same tokenId (store a tokenId->chip mapping) 55 | function _updateChips(address[] calldata chipAddressesOld, address[] calldata chipAddressesNew) 56 | internal 57 | { 58 | if (chipAddressesOld.length != chipAddressesNew.length) { 59 | revert ArrayLengthMismatch(); 60 | } 61 | 62 | for (uint256 i = 0; i < chipAddressesOld.length; i++) { 63 | address oldChipAddress = chipAddressesOld[i]; 64 | if (!_tokenDatas[oldChipAddress].set) { 65 | revert UpdatingChipForUnsetChipMapping(); 66 | } 67 | address newChipAddress = chipAddressesNew[i]; 68 | uint256 tokenId = _tokenDatas[oldChipAddress].tokenId; 69 | _tokenDatas[newChipAddress] = TokenData(tokenId, newChipAddress, true); 70 | emit PBTChipRemapping(tokenId, oldChipAddress, newChipAddress); 71 | delete _tokenDatas[oldChipAddress]; 72 | } 73 | } 74 | 75 | function tokenIdFor(address chipAddress) external view override returns (uint256) { 76 | if (!_tokenDatas[chipAddress].set) { 77 | revert NoMintedTokenForChip(); 78 | } 79 | return _tokenDatas[chipAddress].tokenId; 80 | } 81 | 82 | // Returns true if the signer of the signature of the payload is the chip for the token id 83 | function isChipSignatureForToken(uint256 tokenId, bytes memory payload, bytes memory signature) 84 | public 85 | view 86 | override 87 | returns (bool) 88 | { 89 | if (!_exists(tokenId)) { 90 | revert NoMintedTokenForChip(); 91 | } 92 | bytes32 signedHash = keccak256(payload).toEthSignedMessageHash(); 93 | address chipAddr = signedHash.recover(signature); 94 | return _tokenDatas[chipAddr].set && _tokenDatas[chipAddr].tokenId == tokenId; 95 | } 96 | 97 | // Parameters: 98 | // to: the address of the new owner 99 | // signatureFromChip: signature(receivingAddress + recentBlockhash), signed by an approved chip 100 | // 101 | // Contract should check that (1) recentBlockhash is a recent blockhash, (2) receivingAddress === to, and (3) the signing chip is allowlisted. 102 | function _mintTokenWithChip(bytes memory signatureFromChip, uint256 blockNumberUsedInSig) 103 | internal 104 | returns (uint256) 105 | { 106 | address chipAddr = _getChipAddrForChipSignature(signatureFromChip, blockNumberUsedInSig); 107 | 108 | if (_tokenDatas[chipAddr].set) { 109 | revert ChipAlreadyLinkedToMintedToken(); 110 | } else if (_tokenDatas[chipAddr].chipAddress != chipAddr) { 111 | revert InvalidChipAddress(); 112 | } 113 | 114 | uint256 tokenId = _useRandomAvailableTokenId(); 115 | _mint(_msgSender(), tokenId); 116 | _tokenDatas[chipAddr] = TokenData(tokenId, chipAddr, true); 117 | 118 | emit PBTMint(tokenId, chipAddr); 119 | 120 | return tokenId; 121 | } 122 | 123 | // Generates a pseudorandom number between [0,maxSupply) that has not yet been generated before, in O(1) time. 124 | // 125 | // Uses Durstenfeld's version of the Yates Shuffle https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle 126 | // with a twist to avoid having to manually spend gas to preset an array's values to be values 0...n. 127 | // It does this by interpreting zero-values for an index X as meaning that index X itself is an available value 128 | // that is returnable. 129 | // 130 | // How it works: 131 | // - zero-initialize a mapping (_availableRemainingTokens) and track its length (_numAvailableRemainingTokens). functionally similar to an array with dynamic sizing 132 | // - this mapping will track all remaining valid values that haven't been generated yet, through a combination of its indices and values 133 | // - if _availableRemainingTokens[x] == 0, that means x has not been generated yet 134 | // - if _availableRemainingTokens[x] != 0, that means _availableRemainingTokens[x] has not been generated yet 135 | // - when prompted for a random number between [0,maxSupply) that hasn't already been used: 136 | // - generate a random index randIndex between [0,_numAvailableRemainingTokens) 137 | // - examine the value at _availableRemainingTokens[randIndex] 138 | // - if the value is zero, it means randIndex has not been used, so we can return randIndex 139 | // - if the value is non-zero, it means the value has not been used, so we can return _availableRemainingTokens[randIndex] 140 | // - update the _availableRemainingTokens mapping state 141 | // - set _availableRemainingTokens[randIndex] to either the index or the value of the last entry in the mapping (depends on the last entry's state) 142 | // - decrement _numAvailableRemainingTokens to mimic the shrinking of an array 143 | function _useRandomAvailableTokenId() internal returns (uint256) { 144 | uint256 numAvailableRemainingTokens = _numAvailableRemainingTokens; 145 | if (numAvailableRemainingTokens == 0) { 146 | revert NoMoreTokenIds(); 147 | } 148 | 149 | uint256 randomNum = _getRandomNum(numAvailableRemainingTokens); 150 | uint256 randomIndex = randomNum % numAvailableRemainingTokens; 151 | uint256 valAtIndex = _availableRemainingTokens[randomIndex]; 152 | 153 | uint256 result; 154 | if (valAtIndex == 0) { 155 | // This means the index itself is still an available token 156 | result = randomIndex; 157 | } else { 158 | // This means the index itself is not an available token, but the val at that index is. 159 | result = valAtIndex; 160 | } 161 | 162 | uint256 lastIndex = numAvailableRemainingTokens - 1; 163 | if (randomIndex != lastIndex) { 164 | // Replace the value at randomIndex, now that it's been used. 165 | // Replace it with the data from the last index in the array, since we are going to decrease the array size afterwards. 166 | uint256 lastValInArray = _availableRemainingTokens[lastIndex]; 167 | if (lastValInArray == 0) { 168 | // This means the index itself is still an available token 169 | _availableRemainingTokens[randomIndex] = lastIndex; 170 | } else { 171 | // This means the index itself is not an available token, but the val at that index is. 172 | _availableRemainingTokens[randomIndex] = lastValInArray; 173 | delete _availableRemainingTokens[lastIndex]; 174 | } 175 | } 176 | 177 | _numAvailableRemainingTokens--; 178 | 179 | return result; 180 | } 181 | 182 | // Devs can swap this out for something less gameable like chainlink if it makes sense for their use case. 183 | function _getRandomNum(uint256 numAvailableRemainingTokens) 184 | internal 185 | view 186 | virtual 187 | returns (uint256) 188 | { 189 | return uint256( 190 | keccak256( 191 | abi.encode( 192 | _msgSender(), 193 | tx.gasprice, 194 | block.number, 195 | block.timestamp, 196 | block.prevrandao, 197 | blockhash(block.number - 1), 198 | address(this), 199 | numAvailableRemainingTokens 200 | ) 201 | ) 202 | ); 203 | } 204 | 205 | function transferTokenWithChip(bytes calldata signatureFromChip, uint256 blockNumberUsedInSig) 206 | public 207 | override 208 | { 209 | transferTokenWithChip(signatureFromChip, blockNumberUsedInSig, false); 210 | } 211 | 212 | function transferTokenWithChip( 213 | bytes calldata signatureFromChip, 214 | uint256 blockNumberUsedInSig, 215 | bool useSafeTransferFrom 216 | ) public override { 217 | _transferTokenWithChip(signatureFromChip, blockNumberUsedInSig, useSafeTransferFrom); 218 | } 219 | 220 | function _transferTokenWithChip( 221 | bytes calldata signatureFromChip, 222 | uint256 blockNumberUsedInSig, 223 | bool useSafeTransferFrom 224 | ) internal virtual { 225 | TokenData memory tokenData = 226 | _getTokenDataForChipSignature(signatureFromChip, blockNumberUsedInSig); 227 | uint256 tokenId = tokenData.tokenId; 228 | if (useSafeTransferFrom) { 229 | _safeTransfer(ownerOf(tokenId), _msgSender(), tokenId, ""); 230 | } else { 231 | _transfer(ownerOf(tokenId), _msgSender(), tokenId); 232 | } 233 | } 234 | 235 | function _getTokenDataForChipSignature( 236 | bytes calldata signatureFromChip, 237 | uint256 blockNumberUsedInSig 238 | ) internal view returns (TokenData memory) { 239 | address chipAddr = _getChipAddrForChipSignature(signatureFromChip, blockNumberUsedInSig); 240 | TokenData memory tokenData = _tokenDatas[chipAddr]; 241 | if (tokenData.set) { 242 | return tokenData; 243 | } 244 | revert InvalidSignature(); 245 | } 246 | 247 | function _getChipAddrForChipSignature( 248 | bytes memory signatureFromChip, 249 | uint256 blockNumberUsedInSig 250 | ) internal view returns (address) { 251 | // The blockNumberUsedInSig must be in a previous block because the blockhash of the current 252 | // block does not exist yet. 253 | if (block.number <= blockNumberUsedInSig) { 254 | revert InvalidBlockNumber(); 255 | } 256 | 257 | if (block.number - blockNumberUsedInSig > getMaxBlockhashValidWindow()) { 258 | revert BlockNumberTooOld(); 259 | } 260 | 261 | bytes32 blockHash = blockhash(blockNumberUsedInSig); 262 | bytes32 signedHash = 263 | keccak256(abi.encodePacked(_msgSender(), blockHash)).toEthSignedMessageHash(); 264 | return signedHash.recover(signatureFromChip); 265 | } 266 | 267 | function getMaxBlockhashValidWindow() public pure virtual returns (uint256) { 268 | return 100; 269 | } 270 | 271 | /** 272 | * @dev See {IERC165-supportsInterface}. 273 | */ 274 | function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { 275 | return interfaceId == type(IPBT).interfaceId || super.supportsInterface(interfaceId); 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /test/v1/PBTSimpleTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Test.sol"; 5 | import "../../src/v1/mocks/PBTSimpleMock.sol"; 6 | 7 | contract PBTSimpleTest is Test { 8 | event PBTMint(uint256 indexed tokenId, address indexed chipAddress); 9 | event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); 10 | 11 | PBTSimpleMock public pbt; 12 | uint256 public tokenId1 = 1; 13 | uint256 public tokenId2 = 2; 14 | uint256 public tokenId3 = 3; 15 | address public user1 = vm.addr(1); 16 | address public user2 = vm.addr(2); 17 | address public user3 = vm.addr(3); 18 | address public chipAddr1 = vm.addr(101); 19 | address public chipAddr2 = vm.addr(102); 20 | address public chipAddr3 = vm.addr(103); 21 | address public chipAddr4 = vm.addr(104); 22 | uint256 public blockNumber = 10; 23 | 24 | function setUp() public { 25 | pbt = new PBTSimpleMock("PBTSimple", "PBTS"); 26 | } 27 | 28 | modifier mintedTokens() { 29 | pbt.mint(user1, tokenId1); 30 | pbt.mint(user2, tokenId2); 31 | _; 32 | } 33 | 34 | function testSeedChipToTokenMappingInvalidInput() public { 35 | address[] memory chipAddresses = new address[](2); 36 | chipAddresses[0] = chipAddr1; 37 | chipAddresses[1] = chipAddr2; 38 | 39 | uint256[] memory tokenIds = new uint256[](1); 40 | tokenIds[0] = tokenId1; 41 | 42 | vm.expectRevert(ArrayLengthMismatch.selector); 43 | pbt.seedChipToTokenMapping(chipAddresses, tokenIds, true); 44 | } 45 | 46 | function testSeedChipToTokenMappingExistingToken() public mintedTokens { 47 | address[] memory chipAddresses = new address[](2); 48 | chipAddresses[0] = chipAddr1; 49 | chipAddresses[1] = chipAddr2; 50 | 51 | uint256[] memory tokenIds = new uint256[](2); 52 | tokenIds[0] = tokenId1; 53 | tokenIds[1] = tokenId2; 54 | 55 | vm.expectRevert(SeedingChipDataForExistingToken.selector); 56 | pbt.seedChipToTokenMapping(chipAddresses, tokenIds, true); 57 | 58 | // This call will succeed because the flag is set to false 59 | pbt.seedChipToTokenMapping(chipAddresses, tokenIds, false); 60 | } 61 | 62 | function testSeedChipToTokenMapping() public { 63 | address[] memory chipAddresses = new address[](2); 64 | chipAddresses[0] = chipAddr1; 65 | chipAddresses[1] = chipAddr2; 66 | 67 | uint256[] memory tokenIds = new uint256[](2); 68 | tokenIds[0] = tokenId1; 69 | tokenIds[1] = tokenId2; 70 | 71 | pbt.seedChipToTokenMapping(chipAddresses, tokenIds, true); 72 | 73 | PBTSimple.TokenData memory td1 = pbt.getTokenData(chipAddr1); 74 | assertEq(td1.tokenId, tokenId1); 75 | assertEq(td1.chipAddress, chipAddr1); 76 | assertEq(td1.set, true); 77 | 78 | PBTSimple.TokenData memory td2 = pbt.getTokenData(chipAddr2); 79 | assertEq(td2.tokenId, tokenId2); 80 | assertEq(td2.chipAddress, chipAddr2); 81 | assertEq(td2.set, true); 82 | } 83 | 84 | function testUpdateChipsInvalidInput() public { 85 | address[] memory chipAddressesOld = new address[](2); 86 | chipAddressesOld[0] = chipAddr1; 87 | chipAddressesOld[1] = chipAddr2; 88 | 89 | address[] memory chipAddressesNew = new address[](1); 90 | chipAddressesNew[0] = chipAddr3; 91 | 92 | vm.expectRevert(ArrayLengthMismatch.selector); 93 | pbt.updateChips(chipAddressesOld, chipAddressesNew); 94 | } 95 | 96 | function testUpdateChipsUnsetChip() public { 97 | address[] memory chipAddressesOld = new address[](2); 98 | chipAddressesOld[0] = chipAddr1; 99 | chipAddressesOld[1] = chipAddr2; 100 | 101 | address[] memory chipAddressesNew = new address[](2); 102 | chipAddressesNew[0] = chipAddr3; 103 | chipAddressesNew[1] = chipAddr4; 104 | 105 | // An error will occur because tokenDatas have not been set 106 | vm.expectRevert(UpdatingChipForUnsetChipMapping.selector); 107 | pbt.updateChips(chipAddressesOld, chipAddressesNew); 108 | } 109 | 110 | modifier setChipTokenMapping() { 111 | address[] memory chipAddresses = new address[](2); 112 | chipAddresses[0] = chipAddr1; 113 | chipAddresses[1] = chipAddr2; 114 | 115 | uint256[] memory tokenIds = new uint256[](2); 116 | tokenIds[0] = tokenId1; 117 | tokenIds[1] = tokenId2; 118 | 119 | pbt.seedChipToTokenMapping(chipAddresses, tokenIds, true); 120 | 121 | _; 122 | } 123 | 124 | function testUpdateChips() public setChipTokenMapping { 125 | address[] memory chipAddressesOld = new address[](2); 126 | chipAddressesOld[0] = chipAddr1; 127 | chipAddressesOld[1] = chipAddr2; 128 | 129 | address[] memory chipAddressesNew = new address[](2); 130 | chipAddressesNew[0] = chipAddr3; 131 | chipAddressesNew[1] = chipAddr4; 132 | 133 | pbt.updateChips(chipAddressesOld, chipAddressesNew); 134 | 135 | // Validate that the old tokenDatas have been cleared 136 | PBTSimple.TokenData memory td1 = pbt.getTokenData(chipAddr1); 137 | assertEq(td1.tokenId, 0); 138 | assertEq(td1.chipAddress, address(0)); 139 | assertEq(td1.set, false); 140 | PBTSimple.TokenData memory td2 = pbt.getTokenData(chipAddr2); 141 | assertEq(td2.tokenId, 0); 142 | assertEq(td2.chipAddress, address(0)); 143 | assertEq(td2.set, false); 144 | 145 | // Validate the new tokenDatas have been set 146 | PBTSimple.TokenData memory td3 = pbt.getTokenData(chipAddr3); 147 | assertEq(td3.tokenId, tokenId1); 148 | assertEq(td3.chipAddress, chipAddr3); 149 | assertEq(td3.set, true); 150 | PBTSimple.TokenData memory td4 = pbt.getTokenData(chipAddr4); 151 | assertEq(td4.tokenId, tokenId2); 152 | assertEq(td4.chipAddress, chipAddr4); 153 | assertEq(td4.set, true); 154 | } 155 | 156 | function testTokenIdFor() public { 157 | // This will fail because chipAddr3 isn't set in tokenDatas 158 | vm.expectRevert(NoMappedTokenForChip.selector); 159 | pbt.tokenIdFor(chipAddr3); 160 | 161 | // Set chipAddr3 to tokenDatas 162 | address[] memory chipAddresses = new address[](1); 163 | chipAddresses[0] = chipAddr3; 164 | uint256[] memory tokenIds = new uint256[](1); 165 | tokenIds[0] = tokenId3; 166 | pbt.seedChipToTokenMapping(chipAddresses, tokenIds, true); 167 | 168 | // Should error out because tokenId3 has not been minted 169 | vm.expectRevert(NoMintedTokenForChip.selector); 170 | pbt.tokenIdFor(chipAddr3); 171 | 172 | // Mint token, should no longer error 173 | pbt.mint(user1, tokenId3); 174 | assertEq(pbt.tokenIdFor(chipAddr3), tokenId3); 175 | } 176 | 177 | function testTokenIdMappedFor() public { 178 | // This will fail because chipAddr3 isn't set in tokenDatas 179 | vm.expectRevert(NoMappedTokenForChip.selector); 180 | pbt.tokenIdMappedFor(chipAddr3); 181 | 182 | // Set chipAddr3 to tokenDatas 183 | address[] memory chipAddresses = new address[](1); 184 | chipAddresses[0] = chipAddr3; 185 | uint256[] memory tokenIds = new uint256[](1); 186 | tokenIds[0] = tokenId3; 187 | pbt.seedChipToTokenMapping(chipAddresses, tokenIds, true); 188 | 189 | assertEq(pbt.tokenIdMappedFor(chipAddr3), tokenId3); 190 | } 191 | 192 | function _createSignature(bytes memory payload, uint256 chipAddrNum) 193 | private 194 | returns (bytes memory signature) 195 | { 196 | bytes32 payloadHash = keccak256(abi.encodePacked(payload)); 197 | bytes32 signedHash = 198 | keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", payloadHash)); 199 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(chipAddrNum, signedHash); 200 | signature = abi.encodePacked(r, s, v); 201 | } 202 | 203 | function testIsChipSignatureForToken() public setChipTokenMapping mintedTokens { 204 | // Create signature from payload 205 | bytes memory payload = abi.encodePacked("ThisIsPBTSimple"); 206 | bytes32 payloadHash = keccak256(payload); 207 | bytes32 signedHash = 208 | keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", payloadHash)); 209 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(101, signedHash); 210 | bytes memory chipSignature = abi.encodePacked(r, s, v); 211 | 212 | assertEq(pbt.isChipSignatureForToken(tokenId1, payload, chipSignature), true); 213 | } 214 | 215 | function testMintTokenWithChip() public setChipTokenMapping { 216 | vm.roll(blockNumber + 1); 217 | 218 | // Create inputs 219 | bytes memory payload = abi.encodePacked(user1, blockhash(blockNumber)); 220 | bytes memory chipSignature = _createSignature(payload, 101); 221 | 222 | vm.prank(user1); 223 | vm.expectEmit(true, true, true, true); 224 | emit PBTMint(tokenId1, chipAddr1); 225 | uint256 tokenId = pbt.mintTokenWithChip(chipSignature, blockNumber); 226 | assertEq(tokenId, tokenId1); 227 | assertEq(pbt.balanceOf(user1), 1); 228 | } 229 | 230 | function testTransferTokenWithChip(bool useSafeTranfer) 231 | public 232 | setChipTokenMapping 233 | mintedTokens 234 | { 235 | vm.roll(blockNumber + 1); 236 | 237 | // Create inputs 238 | bytes memory payload = abi.encodePacked(user2, blockhash(blockNumber)); 239 | bytes memory chipSignature = _createSignature(payload, 101); 240 | 241 | vm.prank(user2); 242 | vm.expectEmit(true, true, true, true); 243 | emit Transfer(user1, user2, 1); 244 | pbt.transferTokenWithChip(chipSignature, blockNumber, useSafeTranfer); 245 | 246 | assertEq(pbt.balanceOf(user1), 0); 247 | assertEq(pbt.balanceOf(user2), 2); 248 | } 249 | 250 | function testGetTokenDataForChipSignatureInvalidBlockNumber() public setChipTokenMapping { 251 | vm.roll(blockNumber); 252 | 253 | // Create inputs 254 | bytes memory payload = abi.encodePacked(user1, blockhash(blockNumber)); 255 | bytes memory chipSignature = _createSignature(payload, 101); 256 | 257 | vm.prank(user1); 258 | vm.expectRevert(InvalidBlockNumber.selector); 259 | pbt.getTokenDataForChipSignature(chipSignature, blockNumber); 260 | } 261 | 262 | function testGetTokenDataForChipSignatureBlockNumTooOld() public setChipTokenMapping { 263 | // Create inputs 264 | bytes memory payload = abi.encodePacked(user1, blockhash(blockNumber)); 265 | bytes memory chipSignature = _createSignature(payload, 101); 266 | 267 | vm.roll(blockNumber + 101); 268 | vm.prank(user1); 269 | 270 | vm.expectRevert(BlockNumberTooOld.selector); 271 | pbt.getTokenDataForChipSignature(chipSignature, blockNumber); 272 | } 273 | 274 | function testGetTokenDataForChipSignature() public setChipTokenMapping { 275 | // Change block number to the next block to set blockHash(blockNumber) 276 | vm.roll(blockNumber + 1); 277 | 278 | // Create inputs 279 | bytes memory payload = abi.encodePacked(user1, blockhash(blockNumber)); 280 | bytes memory chipSignature = _createSignature(payload, 101); 281 | 282 | vm.roll(blockNumber + 100); 283 | vm.prank(user1); 284 | PBTSimple.TokenData memory td = pbt.getTokenDataForChipSignature(chipSignature, blockNumber); 285 | 286 | assertEq(td.tokenId, tokenId1); 287 | assertEq(td.chipAddress, chipAddr1); 288 | assertEq(td.set, true); 289 | } 290 | 291 | function testGetTokenDataForChipSignatureInvalid() public setChipTokenMapping { 292 | // Change block number to the next block to set blockHash(blockNumber) 293 | vm.roll(blockNumber + 1); 294 | 295 | // Create an invalid chip signature 296 | bytes memory payload = abi.encodePacked(user3, blockhash(blockNumber)); 297 | bytes memory chipSignature = _createSignature(payload, 9999); 298 | 299 | vm.roll(blockNumber + 100); 300 | vm.prank(user3); 301 | vm.expectRevert(InvalidSignature.selector); 302 | pbt.getTokenDataForChipSignature(chipSignature, blockNumber); 303 | } 304 | 305 | function testSupportsInterface() public { 306 | assertEq(pbt.supportsInterface(type(IPBT).interfaceId), true); 307 | assertEq(pbt.supportsInterface(type(IERC721).interfaceId), true); 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /test/v1/PBTRandomTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Test.sol"; 5 | import "../../src/v1/IPBT.sol"; 6 | import "../../src/v1/mocks/PBTRandomMock.sol"; 7 | 8 | contract PBTRandomTest is Test { 9 | event PBTMint(uint256 indexed tokenId, address indexed chipAddress); 10 | event PBTChipRemapping( 11 | uint256 indexed tokenId, address indexed oldChipAddress, address indexed newChipAddress 12 | ); 13 | event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); 14 | 15 | PBTRandomMock public pbt; 16 | uint256 public tokenId1 = 1; 17 | uint256 public tokenId2 = 2; 18 | uint256 public tokenId3 = 3; 19 | address public user1 = vm.addr(1); 20 | address public user2 = vm.addr(2); 21 | address public user3 = vm.addr(3); 22 | address public chipAddr1 = vm.addr(101); 23 | address public chipAddr2 = vm.addr(102); 24 | address public chipAddr3 = vm.addr(103); 25 | address public chipAddr4 = vm.addr(104); 26 | uint256 public blockNumber = 10; 27 | 28 | function setUp() public { 29 | pbt = new PBTRandomMock("PBTRandom", "PBTR", 10); 30 | } 31 | 32 | function _createSignature(bytes memory payload, uint256 chipAddrNum) 33 | private 34 | returns (bytes memory signature) 35 | { 36 | bytes32 payloadHash = keccak256(abi.encodePacked(payload)); 37 | bytes32 signedHash = 38 | keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", payloadHash)); 39 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(chipAddrNum, signedHash); 40 | signature = abi.encodePacked(r, s, v); 41 | } 42 | 43 | function _createSignature(bytes32 payload, uint256 chipAddrNum) 44 | private 45 | returns (bytes memory signature) 46 | { 47 | return _createSignature(abi.encodePacked(payload), chipAddrNum); 48 | } 49 | 50 | // Excluded cuz it fails CI. 51 | function _testMintTokenWithChip() public { 52 | // Change block number to the next block to set blockHash(blockNumber) 53 | vm.roll(blockNumber + 1); 54 | 55 | bytes memory payload = abi.encodePacked(user1, blockhash(blockNumber)); 56 | bytes memory signature = _createSignature(payload, 101); 57 | 58 | vm.startPrank(user1); 59 | vm.roll(blockNumber + 2); 60 | uint256 expectedTokenId = 3; 61 | 62 | // First mint will fail because seeding hasn't happened 63 | vm.expectRevert(InvalidChipAddress.selector); 64 | pbt.mintTokenWithChip(signature, blockNumber); 65 | 66 | // Seed chip addresses 67 | address[] memory chipAddresses = new address[](1); 68 | chipAddresses[0] = chipAddr1; 69 | pbt.seedChipAddresses(chipAddresses); 70 | 71 | // Mint should now succeed 72 | vm.expectEmit(true, true, true, true); 73 | emit PBTMint(expectedTokenId, chipAddr1); 74 | pbt.mintTokenWithChip(signature, blockNumber); 75 | 76 | // Make sure a chipAddr that has been minted can't mint again 77 | vm.expectRevert(ChipAlreadyLinkedToMintedToken.selector); 78 | pbt.mintTokenWithChip(signature, blockNumber); 79 | } 80 | 81 | modifier withSeededChips() { 82 | address[] memory chipAddresses = new address[](3); 83 | chipAddresses[0] = chipAddr1; 84 | chipAddresses[1] = chipAddr2; 85 | chipAddresses[2] = chipAddr3; 86 | pbt.seedChipAddresses(chipAddresses); 87 | _; 88 | } 89 | 90 | function testIsChipSignatureForToken() public withSeededChips { 91 | vm.roll(blockNumber + 1); 92 | 93 | bytes memory payload = abi.encodePacked(user1, blockhash(blockNumber)); 94 | bytes memory signature = _createSignature(payload, 101); 95 | 96 | vm.startPrank(user1); 97 | vm.roll(blockNumber + 2); 98 | uint256 tokenId = pbt.mintTokenWithChip(signature, blockNumber); 99 | assertEq(pbt.isChipSignatureForToken(tokenId, payload, signature), true); 100 | 101 | vm.expectRevert(NoMintedTokenForChip.selector); 102 | pbt.isChipSignatureForToken(tokenId + 1, payload, signature); 103 | } 104 | 105 | function testUpdateChips() public { 106 | // Change block number to the next block to set blockHash(blockNumber) 107 | vm.roll(blockNumber + 1); 108 | 109 | address[] memory oldChips = new address[](2); 110 | oldChips[0] = chipAddr1; 111 | oldChips[1] = chipAddr2; 112 | pbt.seedChipAddresses(oldChips); 113 | 114 | address[] memory newChips = new address[](2); 115 | newChips[0] = chipAddr3; 116 | newChips[1] = chipAddr4; 117 | 118 | // Chips haven't minted so they can't be updated 119 | vm.expectRevert(UpdatingChipForUnsetChipMapping.selector); 120 | pbt.updateChips(oldChips, newChips); 121 | 122 | // Mint the two chip addresses 123 | bytes memory payload = abi.encodePacked(user1, blockhash(blockNumber)); 124 | bytes memory signature = _createSignature(payload, 101); 125 | vm.prank(user1); 126 | uint256 tokenId1_ = pbt.mintTokenWithChip(signature, blockNumber); 127 | 128 | payload = abi.encodePacked(user2, blockhash(blockNumber)); 129 | signature = _createSignature(payload, 102); 130 | vm.prank(user2); 131 | uint256 tokenId2_ = pbt.mintTokenWithChip(signature, blockNumber); 132 | 133 | // updateChips should now succeed 134 | vm.expectEmit(true, true, true, true); 135 | emit PBTChipRemapping(tokenId1_, chipAddr1, chipAddr3); 136 | vm.expectEmit(true, true, true, true); 137 | emit PBTChipRemapping(tokenId2_, chipAddr2, chipAddr4); 138 | pbt.updateChips(oldChips, newChips); 139 | 140 | // Verify the call works as inteded 141 | PBTRandom.TokenData memory td = pbt.getTokenData(chipAddr1); 142 | assertEq(td.set, false); 143 | assertEq(td.tokenId, 0); 144 | assertEq(td.chipAddress, address(0)); 145 | 146 | td = pbt.getTokenData(chipAddr2); 147 | assertEq(td.set, false); 148 | assertEq(td.tokenId, 0); 149 | assertEq(td.chipAddress, address(0)); 150 | 151 | td = pbt.getTokenData(chipAddr3); 152 | assertEq(td.set, true); 153 | assertEq(td.tokenId, tokenId1_); 154 | assertEq(td.chipAddress, chipAddr3); 155 | 156 | td = pbt.getTokenData(chipAddr4); 157 | assertEq(td.set, true); 158 | assertEq(td.tokenId, tokenId2_); 159 | assertEq(td.chipAddress, chipAddr4); 160 | } 161 | 162 | function testTokenIdFor() public { 163 | vm.expectRevert(NoMintedTokenForChip.selector); 164 | pbt.tokenIdFor(chipAddr1); 165 | 166 | vm.roll(blockNumber + 1); 167 | 168 | bytes memory payload = abi.encodePacked(user1, blockhash(blockNumber)); 169 | bytes memory signature = _createSignature(payload, 101); 170 | vm.startPrank(user1); 171 | 172 | address[] memory chipAddresses = new address[](1); 173 | chipAddresses[0] = chipAddr1; 174 | pbt.seedChipAddresses(chipAddresses); 175 | 176 | uint256 tokenId = pbt.mintTokenWithChip(signature, blockNumber); 177 | assertEq(pbt.tokenIdFor(chipAddr1), tokenId); 178 | } 179 | 180 | function testTransferTokenWithChip(bool useSafeTransfer) public withSeededChips { 181 | vm.roll(blockNumber + 1); 182 | bytes memory payload = abi.encodePacked(user1, blockhash(blockNumber)); 183 | bytes memory signature = _createSignature(payload, 101); 184 | vm.prank(user1); 185 | uint256 tokenId = pbt.mintTokenWithChip(signature, blockNumber); 186 | assertEq(pbt.ownerOf(tokenId), user1); 187 | 188 | vm.roll(blockNumber + 10); 189 | payload = abi.encodePacked(user2, blockhash(blockNumber + 9)); 190 | signature = _createSignature(payload, 101); 191 | vm.prank(user2); 192 | pbt.transferTokenWithChip(signature, blockNumber + 9, useSafeTransfer); 193 | assertEq(pbt.ownerOf(tokenId), user2); 194 | } 195 | 196 | function testGetTokenDataForChipSignatureInvalid() public withSeededChips { 197 | vm.startPrank(user1); 198 | vm.roll(blockNumber + 1); 199 | bytes memory payload = abi.encodePacked(user1, blockhash(blockNumber)); 200 | bytes memory signature = _createSignature(payload, 101); 201 | 202 | vm.expectRevert(InvalidSignature.selector); 203 | pbt.getTokenDataForChipSignature(signature, blockNumber); 204 | pbt.mintTokenWithChip(signature, blockNumber); 205 | 206 | // Current block number is the same as the signature block number which is invalid 207 | vm.expectRevert(InvalidBlockNumber.selector); 208 | pbt.getTokenDataForChipSignature(signature, blockNumber + 1); 209 | 210 | // Block number used in signature is too old 211 | vm.roll(blockNumber + 101); 212 | vm.expectRevert(BlockNumberTooOld.selector); 213 | pbt.getTokenDataForChipSignature(signature, blockNumber); 214 | } 215 | 216 | function testGetTokenDataForChipSignature() public withSeededChips { 217 | vm.startPrank(user1); 218 | vm.roll(blockNumber + 1); 219 | bytes memory payload = abi.encodePacked(user1, blockhash(blockNumber)); 220 | bytes memory signature = _createSignature(payload, 101); 221 | uint256 tokenId = pbt.mintTokenWithChip(signature, blockNumber); 222 | 223 | PBTRandom.TokenData memory td = pbt.getTokenDataForChipSignature(signature, blockNumber); 224 | assertEq(td.set, true); 225 | assertEq(td.chipAddress, chipAddr1); 226 | assertEq(td.tokenId, tokenId); 227 | } 228 | 229 | // Excluded cuz it fails CI. 230 | function _testUseRandomAvailableTokenId() public { 231 | // randomIndex: 7 232 | // lastIndex: 9 233 | // _availableRemainingTokens: [0, 0, 0, 0, 0, 0, 0, 9, 0, 0] 234 | vm.roll(4); 235 | assertEq(pbt.useRandomAvailableTokenId(), 7); 236 | assertEq(pbt.getAvailableRemainingTokens(7), 9); 237 | 238 | // randomIndex: 1 239 | // lastIndex: 8 240 | // _availableRemainingTokens: [0, 8, 0, 0, 0, 0, 0, 9, 0, 0] 241 | vm.roll(5); 242 | assertEq(pbt.useRandomAvailableTokenId(), 1); 243 | assertEq(pbt.getAvailableRemainingTokens(1), 8); 244 | 245 | // randomIndex: 7 246 | // lastIndex: 7 247 | // _availableRemainingTokens: [0, 8, 0, 0, 0, 0, 0, 9, 0, 0] 248 | vm.roll(6); 249 | assertEq(pbt.useRandomAvailableTokenId(), 9); 250 | assertEq(pbt.getAvailableRemainingTokens(7), 9); 251 | 252 | // randomIndex: 4 253 | // lastIndex: 6 254 | // _availableRemainingTokens: [0, 8, 0, 0, 6, 0, 0, 9, 0, 0] 255 | vm.roll(7); 256 | assertEq(pbt.useRandomAvailableTokenId(), 4); 257 | assertEq(pbt.getAvailableRemainingTokens(4), 6); 258 | 259 | // randomIndex: 1 260 | // lastIndex: 5 261 | // _availableRemainingTokens: [0, 5, 0, 0, 6, 0, 0, 9, 0, 0] 262 | vm.roll(8); 263 | assertEq(pbt.useRandomAvailableTokenId(), 8); 264 | assertEq(pbt.getAvailableRemainingTokens(1), 5); 265 | 266 | // randomIndex: 0 267 | // lastIndex: 4 268 | // _availableRemainingTokens: [6, 5, 0, 0, 0, 0, 0, 9, 0, 0] 269 | vm.roll(7); 270 | assertEq(pbt.useRandomAvailableTokenId(), 0); 271 | assertEq(pbt.getAvailableRemainingTokens(0), 6); 272 | 273 | // randomIndex: 1 274 | // lastIndex: 3 275 | // _availableRemainingTokens: [6, 3, 0, 0, 0, 0, 0, 9, 0, 0] 276 | vm.roll(8); 277 | assertEq(pbt.useRandomAvailableTokenId(), 5); 278 | assertEq(pbt.getAvailableRemainingTokens(1), 3); 279 | 280 | // randomIndex: 1 281 | // lastIndex: 2 282 | // _availableRemainingTokens: [6, 2, 0, 0, 0, 0, 0, 9, 0, 0] 283 | vm.roll(9); 284 | assertEq(pbt.useRandomAvailableTokenId(), 3); 285 | assertEq(pbt.getAvailableRemainingTokens(1), 2); 286 | 287 | // randomIndex: 0 288 | // lastIndex: 1 289 | // _availableRemainingTokens: [2, 0, 0, 0, 0, 0, 0, 9, 0, 0] 290 | vm.roll(10); 291 | assertEq(pbt.useRandomAvailableTokenId(), 6); 292 | assertEq(pbt.getAvailableRemainingTokens(0), 2); 293 | 294 | // randomIndex: 0 295 | // lastIndex: 0 296 | // _availableRemainingTokens: [2, 0, 0, 0, 0, 0, 0, 9, 0, 0] 297 | vm.roll(11); 298 | assertEq(pbt.useRandomAvailableTokenId(), 2); 299 | assertEq(pbt.getAvailableRemainingTokens(0), 2); 300 | 301 | // All tokens have been assigned so an error should be raised 302 | vm.expectRevert(NoMoreTokenIds.selector); 303 | pbt.useRandomAvailableTokenId(); 304 | } 305 | 306 | function testSupportsInterface() public { 307 | assertEq(pbt.supportsInterface(type(IPBT).interfaceId), true); 308 | assertEq(pbt.supportsInterface(type(IERC721).interfaceId), true); 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /test/utils/TestPlus.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.4; 3 | 4 | import {Brutalizer} from "./Brutalizer.sol"; 5 | 6 | contract TestPlus is Brutalizer { 7 | event LogString(string name, string value); 8 | event LogString(string value); 9 | event LogBytes(string name, bytes value); 10 | event LogBytes(bytes value); 11 | event LogUint(string name, uint256 value); 12 | event LogUint(uint256 value); 13 | event LogBytes32(string name, bytes32 value); 14 | event LogBytes32(bytes32 value); 15 | event LogInt(string name, int256 value); 16 | event LogInt(int256 value); 17 | event LogAddress(string name, address value); 18 | event LogAddress(address value); 19 | event LogBool(string name, bool value); 20 | event LogBool(bool value); 21 | 22 | event LogStringArray(string name, string[] value); 23 | event LogStringArray(string[] value); 24 | event LogBytesArray(string name, bytes[] value); 25 | event LogBytesArray(bytes[] value); 26 | event LogUintArray(string name, uint256[] value); 27 | event LogUintArray(uint256[] value); 28 | event LogBytes32Array(string name, bytes32[] value); 29 | event LogBytes32Array(bytes32[] value); 30 | event LogIntArray(string name, int256[] value); 31 | event LogIntArray(int256[] value); 32 | event LogAddressArray(string name, address[] value); 33 | event LogAddressArray(address[] value); 34 | event LogBoolArray(string name, bool[] value); 35 | event LogBoolArray(bool[] value); 36 | 37 | /// @dev `address(bytes20(uint160(uint256(keccak256("hevm cheat code")))))`. 38 | address private constant _VM_ADDRESS = 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D; 39 | 40 | /// @dev This is the keccak256 of a very long string I randomly mashed on my keyboard. 41 | uint256 private constant _TESTPLUS_RANDOMNESS_SLOT = 42 | 0xd715531fe383f818c5f158c342925dcf01b954d24678ada4d07c36af0f20e1ee; 43 | 44 | /// @dev Returns a pseudorandom random number from [0 .. 2**256 - 1] (inclusive). 45 | /// For usage in fuzz tests, please ensure that the function has an unnamed uint256 argument. 46 | /// e.g. `testSomething(uint256) public`. 47 | function _random() internal returns (uint256 r) { 48 | /// @solidity memory-safe-assembly 49 | assembly { 50 | let sSlot := _TESTPLUS_RANDOMNESS_SLOT 51 | let sValue := sload(sSlot) 52 | mstore(0x20, sValue) 53 | r := keccak256(0x20, 0x40) 54 | // If the storage is uninitialized, initialize it to the keccak256 of the calldata. 55 | if iszero(sValue) { 56 | sValue := sSlot 57 | calldatacopy(mload(0x40), 0, calldatasize()) 58 | r := keccak256(mload(0x40), calldatasize()) 59 | } 60 | sstore(sSlot, add(r, 1)) 61 | 62 | // Do some biased sampling for more robust tests. 63 | // prettier-ignore 64 | for {} 1 {} { 65 | let y := 66 | mulmod( 67 | r, 68 | 0x100000000000000000000000000000051, // Prime and a primitive root of `n`. 69 | 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff43 // `n`, prime. 70 | ) 71 | // With a 1/256 chance, randomly set `r` to any of 0,1,2,3. 72 | if iszero(byte(19, y)) { 73 | r := and(byte(11, y), 3) 74 | break 75 | } 76 | let d := byte(17, y) 77 | // With a 1/2 chance, set `r` to near a random power of 2. 78 | if iszero(and(2, d)) { 79 | // Set `t` either `not(0)` or `xor(sValue, r)`. 80 | let t := or(xor(sValue, r), sub(0, and(1, d))) 81 | // Set `r` to `t` shifted left or right. 82 | // prettier-ignore 83 | for {} 1 {} { 84 | if iszero(and(8, d)) { 85 | if iszero(and(16, d)) { t := 1 } 86 | if iszero(and(32, d)) { 87 | r := add(shl(shl(3, and(byte(7, y), 31)), t), sub(3, and(7, r))) 88 | break 89 | } 90 | r := add(shl(byte(7, y), t), sub(511, and(1023, r))) 91 | break 92 | } 93 | if iszero(and(16, d)) { t := shl(255, 1) } 94 | if iszero(and(32, d)) { 95 | r := add(shr(shl(3, and(byte(7, y), 31)), t), sub(3, and(7, r))) 96 | break 97 | } 98 | r := add(shr(byte(7, y), t), sub(511, and(1023, r))) 99 | break 100 | } 101 | // With a 1/2 chance, negate `r`. 102 | r := xor(sub(0, shr(7, d)), r) 103 | break 104 | } 105 | // Otherwise, just set `r` to `xor(sValue, r)`. 106 | r := xor(sValue, r) 107 | break 108 | } 109 | } 110 | } 111 | 112 | /// @dev Returns a boolean with a `1 / n` chance of being true. 113 | function _randomChance(uint256 n) internal returns (bool result) { 114 | /// @solidity memory-safe-assembly 115 | assembly { 116 | result := _TESTPLUS_RANDOMNESS_SLOT 117 | // prettier-ignore 118 | for { let sValue := sload(result) } 1 {} { 119 | // If the storage is uninitialized, initialize it to the keccak256 of the calldata. 120 | if iszero(sValue) { 121 | calldatacopy(mload(0x40), 0, calldatasize()) 122 | sValue := keccak256(mload(0x40), calldatasize()) 123 | sstore(result, sValue) 124 | result := iszero(mod(sValue, n)) 125 | break 126 | } 127 | mstore(0x1f, sValue) 128 | sValue := keccak256(0x20, 0x40) 129 | sstore(result, sValue) 130 | result := iszero(mod(sValue, n)) 131 | break 132 | } 133 | } 134 | } 135 | 136 | /// @dev Returns a random signer and its private key. 137 | function _randomSigner() internal returns (address signer, uint256 privateKey) { 138 | uint256 privateKeyMax = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140; 139 | privateKey = _hem(_random(), 1, privateKeyMax); 140 | /// @solidity memory-safe-assembly 141 | assembly { 142 | mstore(0x00, 0xffa18649) // `addr(uint256)`. 143 | mstore(0x20, privateKey) 144 | pop(call(gas(), _VM_ADDRESS, 0, 0x1c, 0x24, 0x00, 0x20)) 145 | signer := mload(0x00) 146 | } 147 | } 148 | 149 | /// @dev Returns a random address. 150 | function _randomAddress() internal returns (address result) { 151 | result = address(uint160(_random())); 152 | } 153 | 154 | /// @dev Returns a random non-zero address. 155 | /// This function will not return an existing contract. 156 | function _randomNonZeroAddress() internal returns (address result) { 157 | do { 158 | result = address(uint160(_random())); 159 | } while (result == address(0)); 160 | } 161 | 162 | /// @dev Returns a random hashed address. 163 | /// This function will not return an existing contract. 164 | /// This function will not return a precompile address. 165 | function _randomHashedAddress() internal returns (address result) { 166 | uint256 r = _random(); 167 | /// @solidity memory-safe-assembly 168 | assembly { 169 | mstore(0x00, r) 170 | result := keccak256(0x00, 0x20) 171 | } 172 | } 173 | 174 | /// @dev Adapted from `bound`: 175 | /// https://github.com/foundry-rs/forge-std/blob/ff4bf7db008d096ea5a657f2c20516182252a3ed/src/StdUtils.sol#L10 176 | /// Differentially fuzzed tested against the original implementation. 177 | function _hem(uint256 x, uint256 min, uint256 max) 178 | internal 179 | pure 180 | virtual 181 | returns (uint256 result) 182 | { 183 | require(min <= max, "Max is less than min."); 184 | 185 | /// @solidity memory-safe-assembly 186 | assembly { 187 | // prettier-ignore 188 | for {} 1 {} { 189 | // If `x` is between `min` and `max`, return `x` directly. 190 | // This is to ensure that dictionary values 191 | // do not get shifted if the min is nonzero. 192 | // More info: https://github.com/foundry-rs/forge-std/issues/188 193 | if iszero(or(lt(x, min), gt(x, max))) { 194 | result := x 195 | break 196 | } 197 | 198 | let size := add(sub(max, min), 1) 199 | if lt(gt(x, 3), gt(size, x)) { 200 | result := add(min, x) 201 | break 202 | } 203 | 204 | if lt(lt(x, not(3)), gt(size, not(x))) { 205 | result := sub(max, not(x)) 206 | break 207 | } 208 | 209 | // Otherwise, wrap x into the range [min, max], 210 | // i.e. the range is inclusive. 211 | if iszero(lt(x, max)) { 212 | let d := sub(x, max) 213 | let r := mod(d, size) 214 | if iszero(r) { 215 | result := max 216 | break 217 | } 218 | result := sub(add(min, r), 1) 219 | break 220 | } 221 | let d := sub(min, x) 222 | let r := mod(d, size) 223 | if iszero(r) { 224 | result := min 225 | break 226 | } 227 | result := add(sub(max, r), 1) 228 | break 229 | } 230 | } 231 | } 232 | 233 | /// @dev Deploys a contract via 0age's immutable create 2 factory for testing. 234 | function _safeCreate2(uint256 payableAmount, bytes32 salt, bytes memory initializationCode) 235 | internal 236 | returns (address deploymentAddress) 237 | { 238 | // Canonical address of 0age's immutable create 2 factory. 239 | address c2f = 0x0000000000FFe8B47B3e2130213B802212439497; 240 | uint256 c2fCodeLength; 241 | /// @solidity memory-safe-assembly 242 | assembly { 243 | c2fCodeLength := extcodesize(c2f) 244 | } 245 | if (c2fCodeLength == 0) { 246 | bytes memory ic2fBytecode = 247 | hex"60806040526004361061003f5760003560e01c806308508b8f1461004457806364e030871461009857806385cf97ab14610138578063a49a7c90146101bc575b600080fd5b34801561005057600080fd5b506100846004803603602081101561006757600080fd5b503573ffffffffffffffffffffffffffffffffffffffff166101ec565b604080519115158252519081900360200190f35b61010f600480360360408110156100ae57600080fd5b813591908101906040810160208201356401000000008111156100d057600080fd5b8201836020820111156100e257600080fd5b8035906020019184600183028401116401000000008311171561010457600080fd5b509092509050610217565b6040805173ffffffffffffffffffffffffffffffffffffffff9092168252519081900360200190f35b34801561014457600080fd5b5061010f6004803603604081101561015b57600080fd5b8135919081019060408101602082013564010000000081111561017d57600080fd5b82018360208201111561018f57600080fd5b803590602001918460018302840111640100000000831117156101b157600080fd5b509092509050610592565b3480156101c857600080fd5b5061010f600480360360408110156101df57600080fd5b508035906020013561069e565b73ffffffffffffffffffffffffffffffffffffffff1660009081526020819052604090205460ff1690565b600083606081901c33148061024c57507fffffffffffffffffffffffffffffffffffffffff0000000000000000000000008116155b6102a1576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260458152602001806107746045913960600191505060405180910390fd5b606084848080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920182905250604051855195965090943094508b93508692506020918201918291908401908083835b6020831061033557805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe090920191602091820191016102f8565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff018019909216911617905260408051929094018281037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe00183528085528251928201929092207fff000000000000000000000000000000000000000000000000000000000000008383015260609890981b7fffffffffffffffffffffffffffffffffffffffff00000000000000000000000016602183015260358201969096526055808201979097528251808203909701875260750182525084519484019490942073ffffffffffffffffffffffffffffffffffffffff81166000908152938490529390922054929350505060ff16156104a7576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252603f815260200180610735603f913960400191505060405180910390fd5b81602001825188818334f5955050508073ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff161461053a576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260468152602001806107b96046913960600191505060405180910390fd5b50505073ffffffffffffffffffffffffffffffffffffffff8116600090815260208190526040902080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001660011790559392505050565b6000308484846040516020018083838082843760408051919093018181037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe001825280845281516020928301207fff000000000000000000000000000000000000000000000000000000000000008383015260609990991b7fffffffffffffffffffffffffffffffffffffffff000000000000000000000000166021820152603581019790975260558088019890985282518088039098018852607590960182525085519585019590952073ffffffffffffffffffffffffffffffffffffffff81166000908152948590529490932054939450505060ff909116159050610697575060005b9392505050565b604080517fff000000000000000000000000000000000000000000000000000000000000006020808301919091523060601b6021830152603582018590526055808301859052835180840390910181526075909201835281519181019190912073ffffffffffffffffffffffffffffffffffffffff81166000908152918290529190205460ff161561072e575060005b9291505056fe496e76616c696420636f6e7472616374206372656174696f6e202d20636f6e74726163742068617320616c7265616479206265656e206465706c6f7965642e496e76616c69642073616c74202d206669727374203230206279746573206f66207468652073616c74206d757374206d617463682063616c6c696e6720616464726573732e4661696c656420746f206465706c6f7920636f6e7472616374207573696e672070726f76696465642073616c7420616e6420696e697469616c697a6174696f6e20636f64652ea265627a7a723058202bdc55310d97c4088f18acf04253db593f0914059f0c781a9df3624dcef0d1cf64736f6c634300050a0032"; 248 | /// @solidity memory-safe-assembly 249 | assembly { 250 | let m := mload(0x40) 251 | mstore(m, 0xb4d6c782) // `etch(address,bytes)`. 252 | mstore(add(m, 0x20), c2f) 253 | mstore(add(m, 0x40), 0x40) 254 | let n := mload(ic2fBytecode) 255 | mstore(add(m, 0x60), n) 256 | for { let i := 0 } lt(i, n) { i := add(0x20, i) } { 257 | mstore(add(add(m, 0x80), i), mload(add(add(ic2fBytecode, 0x20), i))) 258 | } 259 | pop(call(gas(), _VM_ADDRESS, 0, add(m, 0x1c), add(n, 0x64), 0x00, 0x00)) 260 | } 261 | } 262 | /// @solidity memory-safe-assembly 263 | assembly { 264 | let m := mload(0x40) 265 | let n := mload(initializationCode) 266 | mstore(m, 0x64e03087) // `safeCreate2(bytes32,bytes)`. 267 | mstore(add(m, 0x20), salt) 268 | mstore(add(m, 0x40), 0x40) 269 | mstore(add(m, 0x60), n) 270 | // prettier-ignore 271 | for { let i := 0 } lt(i, n) { i := add(i, 0x20) } { 272 | mstore(add(add(m, 0x80), i), mload(add(add(initializationCode, 0x20), i))) 273 | } 274 | if iszero(call(gas(), c2f, payableAmount, add(m, 0x1c), add(n, 0x64), m, 0x20)) { 275 | returndatacopy(m, m, returndatasize()) 276 | revert(m, returndatasize()) 277 | } 278 | deploymentAddress := mload(m) 279 | } 280 | } 281 | 282 | /// @dev Deploys a contract via 0age's immutable create 2 factory for testing. 283 | function _safeCreate2(bytes32 salt, bytes memory initializationCode) 284 | internal 285 | returns (address deploymentAddress) 286 | { 287 | deploymentAddress = _safeCreate2(0, salt, initializationCode); 288 | } 289 | 290 | /// @dev This function will make forge's gas output display the approximate codesize of 291 | /// the test contract as the amount of gas burnt. Useful for quick guess checking if 292 | /// certain optimizations actually compiles to similar bytecode. 293 | function test__codesize() external view { 294 | /// @solidity memory-safe-assembly 295 | assembly { 296 | // If the caller is the contract itself (i.e. recursive call), burn all the gas. 297 | if eq(caller(), address()) { invalid() } 298 | mstore(0x00, 0xf09ff470) // Store the function selector of `test__codesize()`. 299 | pop(staticcall(codesize(), address(), 0x1c, 0x04, 0x00, 0x00)) 300 | } 301 | } 302 | } 303 | --------------------------------------------------------------------------------